use crate::error::{ForgeError, Result};
use async_trait::async_trait;
use serde::{de::DeserializeOwned, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, info};
#[async_trait]
pub trait StateStore: Send + Sync {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
async fn set(&self, key: &str, value: Vec<u8>) -> Result<()>;
async fn delete(&self, key: &str) -> Result<()>;
async fn list_prefix(&self, prefix: &str) -> Result<Vec<String>>;
fn name(&self) -> &str;
}
pub async fn store_exists(store: &dyn StateStore, key: &str) -> Result<bool> {
Ok(store.get(key).await?.is_some())
}
pub async fn store_get_json<T: DeserializeOwned>(store: &dyn StateStore, key: &str) -> Result<Option<T>> {
match store.get(key).await? {
Some(bytes) => {
let value = serde_json::from_slice(&bytes)?;
Ok(Some(value))
}
None => Ok(None),
}
}
pub async fn store_set_json<T: Serialize>(store: &dyn StateStore, key: &str, value: &T) -> Result<()> {
let bytes = serde_json::to_vec(value)?;
store.set(key, bytes).await
}
#[derive(Debug, Default)]
pub struct MemoryStore {
data: RwLock<HashMap<String, Vec<u8>>>,
}
impl MemoryStore {
pub fn new() -> Self {
Self {
data: RwLock::new(HashMap::new()),
}
}
}
#[async_trait]
impl StateStore for MemoryStore {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
let data = self.data.read().await;
Ok(data.get(key).cloned())
}
async fn set(&self, key: &str, value: Vec<u8>) -> Result<()> {
let mut data = self.data.write().await;
data.insert(key.to_string(), value);
Ok(())
}
async fn delete(&self, key: &str) -> Result<()> {
let mut data = self.data.write().await;
data.remove(key);
Ok(())
}
async fn list_prefix(&self, prefix: &str) -> Result<Vec<String>> {
let data = self.data.read().await;
Ok(data
.keys()
.filter(|k| k.starts_with(prefix))
.cloned()
.collect())
}
fn name(&self) -> &str {
"memory"
}
}
pub struct FileStore {
path: PathBuf,
data: RwLock<HashMap<String, Vec<u8>>>,
}
impl FileStore {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref().to_path_buf();
let data = if path.exists() {
let contents = std::fs::read_to_string(&path)
.map_err(|e| ForgeError::storage(format!("Failed to read store: {}", e)))?;
serde_json::from_str(&contents).unwrap_or_default()
} else {
HashMap::new()
};
info!(path = %path.display(), "File store opened");
Ok(Self {
path,
data: RwLock::new(data),
})
}
pub async fn flush(&self) -> Result<()> {
let data = self.data.read().await;
let contents = serde_json::to_string_pretty(&*data)?;
if let Some(parent) = self.path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| ForgeError::storage(format!("Failed to create dir: {}", e)))?;
}
std::fs::write(&self.path, contents)
.map_err(|e| ForgeError::storage(format!("Failed to write store: {}", e)))?;
debug!(path = %self.path.display(), "File store flushed");
Ok(())
}
}
#[async_trait]
impl StateStore for FileStore {
async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
let data = self.data.read().await;
Ok(data.get(key).cloned())
}
async fn set(&self, key: &str, value: Vec<u8>) -> Result<()> {
let mut data = self.data.write().await;
data.insert(key.to_string(), value);
Ok(())
}
async fn delete(&self, key: &str) -> Result<()> {
let mut data = self.data.write().await;
data.remove(key);
Ok(())
}
async fn list_prefix(&self, prefix: &str) -> Result<Vec<String>> {
let data = self.data.read().await;
Ok(data
.keys()
.filter(|k| k.starts_with(prefix))
.cloned()
.collect())
}
fn name(&self) -> &str {
"file"
}
}
#[cfg(feature = "raft")]
#[path = "storage/raft.rs"]
pub mod raft;
#[cfg(feature = "raft")]
pub use raft::RaftStateStore;
pub type BoxedStateStore = Arc<dyn StateStore>;
pub fn memory_store() -> BoxedStateStore {
Arc::new(MemoryStore::new()) as BoxedStateStore
}
pub mod keys {
pub const JOBS: &str = "forge/jobs";
pub const SHARDS: &str = "forge/shards";
pub const EXPERTS: &str = "forge/experts";
pub const NODES: &str = "forge/nodes";
pub const CONFIG: &str = "forge/config";
pub const METRICS: &str = "forge/metrics";
pub const ASSIGNMENTS: &str = "forge/assignments";
pub const SIMCELLS: &str = "forge/simcells";
pub const SIM_ASSIGNMENTS: &str = "forge/sim_assignments";
pub fn job(id: &str) -> String {
format!("{}/{}", JOBS, id)
}
pub fn shard(id: u64) -> String {
format!("{}/{}", SHARDS, id)
}
pub fn expert(index: usize) -> String {
format!("{}/{}", EXPERTS, index)
}
pub fn node(id: &str) -> String {
format!("{}/{}", NODES, id)
}
pub fn simcell(id: &str) -> String {
format!("{}/{}", SIMCELLS, id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_memory_store_basic() {
let store = MemoryStore::new();
store.set("key1", b"value1".to_vec()).await.unwrap();
let value = store.get("key1").await.unwrap();
assert_eq!(value, Some(b"value1".to_vec()));
store.delete("key1").await.unwrap();
let value = store.get("key1").await.unwrap();
assert!(value.is_none());
}
#[tokio::test]
async fn test_memory_store_prefix() {
let store = MemoryStore::new();
store.set("prefix/a", b"1".to_vec()).await.unwrap();
store.set("prefix/b", b"2".to_vec()).await.unwrap();
store.set("other/c", b"3".to_vec()).await.unwrap();
let keys = store.list_prefix("prefix/").await.unwrap();
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"prefix/a".to_string()));
assert!(keys.contains(&"prefix/b".to_string()));
}
#[tokio::test]
async fn test_memory_store_json() {
let store = MemoryStore::new();
#[derive(Debug, PartialEq, Serialize, serde::Deserialize)]
struct TestData {
name: String,
value: i32,
}
let data = TestData {
name: "test".to_string(),
value: 42,
};
store_set_json(&store, "json_key", &data).await.unwrap();
let loaded: Option<TestData> = store_get_json(&store, "json_key").await.unwrap();
assert_eq!(loaded, Some(data));
}
#[test]
fn test_key_builders() {
assert_eq!(keys::job("my-job"), "forge/jobs/my-job");
assert_eq!(keys::shard(42), "forge/shards/42");
assert_eq!(keys::expert(0), "forge/experts/0");
}
}