use crate::error::{LaneError, Result};
use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tokio::fs;
use tokio::sync::RwLock;
pub type StoredCommandId = String;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredCommand {
pub id: StoredCommandId,
pub command_type: String,
pub lane_id: String,
pub payload: serde_json::Value,
pub retry_count: u32,
pub created_at: DateTime<Utc>,
pub last_attempt_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoredDeadLetter {
pub command_id: String,
pub command_type: String,
pub lane_id: String,
pub error: String,
pub attempts: u32,
pub failed_at: DateTime<Utc>,
}
#[async_trait]
pub trait Storage: Send + Sync {
async fn save_command(&self, command: StoredCommand) -> Result<()>;
async fn load_commands(&self) -> Result<Vec<StoredCommand>>;
async fn remove_command(&self, id: &str) -> Result<()>;
async fn save_dead_letter(&self, letter: StoredDeadLetter) -> Result<()>;
async fn load_dead_letters(&self) -> Result<Vec<StoredDeadLetter>>;
async fn clear_dead_letters(&self) -> Result<()>;
async fn clear_all(&self) -> Result<()>;
}
pub struct LocalStorage {
storage_dir: PathBuf,
commands: RwLock<HashMap<String, StoredCommand>>,
dead_letters: RwLock<Vec<StoredDeadLetter>>,
}
impl LocalStorage {
pub async fn new(storage_dir: PathBuf) -> Result<Self> {
fs::create_dir_all(&storage_dir)
.await
.map_err(|e| LaneError::Other(format!("Failed to create storage directory: {}", e)))?;
let storage = Self {
storage_dir,
commands: RwLock::new(HashMap::new()),
dead_letters: RwLock::new(Vec::new()),
};
storage.load_from_disk().await?;
Ok(storage)
}
fn commands_path(&self) -> PathBuf {
self.storage_dir.join("commands.json")
}
fn dead_letters_path(&self) -> PathBuf {
self.storage_dir.join("dead_letters.json")
}
async fn load_from_disk(&self) -> Result<()> {
if let Ok(data) = fs::read_to_string(self.commands_path()).await {
if let Ok(commands) = serde_json::from_str::<Vec<StoredCommand>>(&data) {
let mut cache = self.commands.write().await;
for cmd in commands {
cache.insert(cmd.id.clone(), cmd);
}
}
}
if let Ok(data) = fs::read_to_string(self.dead_letters_path()).await {
if let Ok(letters) = serde_json::from_str::<Vec<StoredDeadLetter>>(&data) {
let mut cache = self.dead_letters.write().await;
*cache = letters;
}
}
Ok(())
}
async fn persist_commands(&self) -> Result<()> {
let commands = self.commands.read().await;
let commands_vec: Vec<_> = commands.values().cloned().collect();
let json = serde_json::to_string_pretty(&commands_vec)
.map_err(|e| LaneError::Other(format!("Failed to serialize commands: {}", e)))?;
fs::write(self.commands_path(), json)
.await
.map_err(|e| LaneError::Other(format!("Failed to write commands file: {}", e)))?;
Ok(())
}
async fn persist_dead_letters(&self) -> Result<()> {
let letters = self.dead_letters.read().await;
let json = serde_json::to_string_pretty(&*letters)
.map_err(|e| LaneError::Other(format!("Failed to serialize dead letters: {}", e)))?;
fs::write(self.dead_letters_path(), json)
.await
.map_err(|e| LaneError::Other(format!("Failed to write dead letters file: {}", e)))?;
Ok(())
}
}
#[async_trait]
impl Storage for LocalStorage {
async fn save_command(&self, command: StoredCommand) -> Result<()> {
let mut commands = self.commands.write().await;
commands.insert(command.id.clone(), command);
drop(commands);
self.persist_commands().await
}
async fn load_commands(&self) -> Result<Vec<StoredCommand>> {
let commands = self.commands.read().await;
Ok(commands.values().cloned().collect())
}
async fn remove_command(&self, id: &str) -> Result<()> {
let mut commands = self.commands.write().await;
commands.remove(id);
drop(commands);
self.persist_commands().await
}
async fn save_dead_letter(&self, letter: StoredDeadLetter) -> Result<()> {
let mut letters = self.dead_letters.write().await;
letters.push(letter);
drop(letters);
self.persist_dead_letters().await
}
async fn load_dead_letters(&self) -> Result<Vec<StoredDeadLetter>> {
let letters = self.dead_letters.read().await;
Ok(letters.clone())
}
async fn clear_dead_letters(&self) -> Result<()> {
let mut letters = self.dead_letters.write().await;
letters.clear();
drop(letters);
self.persist_dead_letters().await
}
async fn clear_all(&self) -> Result<()> {
{
let mut commands = self.commands.write().await;
commands.clear();
}
{
let mut letters = self.dead_letters.write().await;
letters.clear();
}
let _ = fs::remove_file(self.commands_path()).await;
let _ = fs::remove_file(self.dead_letters_path()).await;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[tokio::test]
async fn test_local_storage_save_and_load_commands() {
let temp_dir = TempDir::new().unwrap();
let storage = LocalStorage::new(temp_dir.path().to_path_buf())
.await
.unwrap();
let cmd = StoredCommand {
id: "cmd1".to_string(),
command_type: "test".to_string(),
lane_id: "query".to_string(),
payload: serde_json::json!({"data": "test"}),
retry_count: 0,
created_at: Utc::now(),
last_attempt_at: None,
};
storage.save_command(cmd.clone()).await.unwrap();
let loaded = storage.load_commands().await.unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].id, "cmd1");
assert_eq!(loaded[0].command_type, "test");
let storage2 = LocalStorage::new(temp_dir.path().to_path_buf())
.await
.unwrap();
let loaded2 = storage2.load_commands().await.unwrap();
assert_eq!(loaded2.len(), 1);
assert_eq!(loaded2[0].id, "cmd1");
}
#[tokio::test]
async fn test_local_storage_remove_command() {
let temp_dir = TempDir::new().unwrap();
let storage = LocalStorage::new(temp_dir.path().to_path_buf())
.await
.unwrap();
let cmd1 = StoredCommand {
id: "cmd1".to_string(),
command_type: "test".to_string(),
lane_id: "query".to_string(),
payload: serde_json::json!({}),
retry_count: 0,
created_at: Utc::now(),
last_attempt_at: None,
};
let cmd2 = StoredCommand {
id: "cmd2".to_string(),
command_type: "test".to_string(),
lane_id: "query".to_string(),
payload: serde_json::json!({}),
retry_count: 0,
created_at: Utc::now(),
last_attempt_at: None,
};
storage.save_command(cmd1).await.unwrap();
storage.save_command(cmd2).await.unwrap();
storage.remove_command("cmd1").await.unwrap();
let loaded = storage.load_commands().await.unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].id, "cmd2");
}
#[tokio::test]
async fn test_local_storage_dead_letters() {
let temp_dir = TempDir::new().unwrap();
let storage = LocalStorage::new(temp_dir.path().to_path_buf())
.await
.unwrap();
let letter = StoredDeadLetter {
command_id: "cmd1".to_string(),
command_type: "test".to_string(),
lane_id: "query".to_string(),
error: "timeout".to_string(),
attempts: 3,
failed_at: Utc::now(),
};
storage.save_dead_letter(letter.clone()).await.unwrap();
let loaded = storage.load_dead_letters().await.unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].command_id, "cmd1");
assert_eq!(loaded[0].error, "timeout");
storage.clear_dead_letters().await.unwrap();
let loaded = storage.load_dead_letters().await.unwrap();
assert_eq!(loaded.len(), 0);
}
#[tokio::test]
async fn test_local_storage_clear_all() {
let temp_dir = TempDir::new().unwrap();
let storage = LocalStorage::new(temp_dir.path().to_path_buf())
.await
.unwrap();
let cmd = StoredCommand {
id: "cmd1".to_string(),
command_type: "test".to_string(),
lane_id: "query".to_string(),
payload: serde_json::json!({}),
retry_count: 0,
created_at: Utc::now(),
last_attempt_at: None,
};
let letter = StoredDeadLetter {
command_id: "cmd1".to_string(),
command_type: "test".to_string(),
lane_id: "query".to_string(),
error: "failed".to_string(),
attempts: 1,
failed_at: Utc::now(),
};
storage.save_command(cmd).await.unwrap();
storage.save_dead_letter(letter).await.unwrap();
storage.clear_all().await.unwrap();
let commands = storage.load_commands().await.unwrap();
let letters = storage.load_dead_letters().await.unwrap();
assert_eq!(commands.len(), 0);
assert_eq!(letters.len(), 0);
}
}