use std::collections::HashMap;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub fn now_millis() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ExecutionId(pub Uuid);
impl ExecutionId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
pub fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
pub fn as_bytes(&self) -> &[u8; 16] {
self.0.as_bytes()
}
}
impl Default for ExecutionId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for ExecutionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(thiserror::Error, Debug, Clone, PartialEq)]
pub enum StoreError {
#[error("Execution not found: {0}")]
NotFound(ExecutionId),
#[error("Storage error: {0}")]
Backend(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PausedRecord {
pub id: ExecutionId,
pub data: Vec<u8>,
pub paused_at: u64,
}
#[async_trait::async_trait]
pub trait Store: Send + Sync {
async fn save(&self, id: ExecutionId, data: Vec<u8>) -> Result<(), StoreError>;
async fn get(&self, id: ExecutionId) -> Result<PausedRecord, StoreError>;
async fn delete(&self, id: ExecutionId) -> Result<(), StoreError>;
async fn exists(&self, id: ExecutionId) -> Result<bool, StoreError>;
}
#[derive(Debug, Default)]
pub struct InMemoryStore {
records: RwLock<HashMap<ExecutionId, PausedRecord>>,
}
impl InMemoryStore {
pub fn new() -> Self {
Self {
records: RwLock::new(HashMap::new()),
}
}
pub fn len(&self) -> usize {
self.records.read().len()
}
pub fn is_empty(&self) -> bool {
self.records.read().is_empty()
}
}
#[async_trait::async_trait]
impl Store for InMemoryStore {
async fn save(&self, id: ExecutionId, data: Vec<u8>) -> Result<(), StoreError> {
let record = PausedRecord {
id,
data,
paused_at: now_millis(),
};
self.records.write().insert(id, record);
Ok(())
}
async fn get(&self, id: ExecutionId) -> Result<PausedRecord, StoreError> {
self.records
.read()
.get(&id)
.cloned()
.ok_or(StoreError::NotFound(id))
}
async fn delete(&self, id: ExecutionId) -> Result<(), StoreError> {
self.records
.write()
.remove(&id)
.map(|_| ())
.ok_or(StoreError::NotFound(id))
}
async fn exists(&self, id: ExecutionId) -> Result<bool, StoreError> {
Ok(self.records.read().contains_key(&id))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn execution_id_display() {
let id = ExecutionId::new();
let display = format!("{}", id);
assert!(!display.is_empty());
}
#[tokio::test]
async fn in_memory_store_save_and_get() {
let store = InMemoryStore::new();
let id = ExecutionId::new();
let data = vec![1, 2, 3, 4];
store
.save(id, data.clone())
.await
.expect("save should succeed");
let record = store.get(id).await.expect("get should succeed");
assert_eq!(record.id, id);
assert_eq!(record.data, data);
assert!(record.paused_at > 0);
}
#[tokio::test]
async fn in_memory_store_get_not_found() {
let store = InMemoryStore::new();
let id = ExecutionId::new();
let result = store.get(id).await;
assert!(matches!(result, Err(StoreError::NotFound(i)) if i == id));
}
#[tokio::test]
async fn in_memory_store_delete() {
let store = InMemoryStore::new();
let id = ExecutionId::new();
store
.save(id, vec![1, 2, 3])
.await
.expect("save should succeed");
assert!(store.exists(id).await.expect("exists should succeed"));
store.delete(id).await.expect("delete should succeed");
assert!(!store.exists(id).await.expect("exists should succeed"));
}
#[tokio::test]
async fn in_memory_store_delete_not_found() {
let store = InMemoryStore::new();
let id = ExecutionId::new();
let result = store.delete(id).await;
assert!(matches!(result, Err(StoreError::NotFound(i)) if i == id));
}
#[tokio::test]
async fn in_memory_store_exists() {
let store = InMemoryStore::new();
let id = ExecutionId::new();
assert!(!store.exists(id).await.expect("exists should succeed"));
store.save(id, vec![]).await.expect("save should succeed");
assert!(store.exists(id).await.expect("exists should succeed"));
}
#[test]
fn in_memory_store_len_and_is_empty() {
let store = InMemoryStore::new();
assert!(store.is_empty());
assert_eq!(store.len(), 0);
let id = ExecutionId::new();
store.records.write().insert(
id,
PausedRecord {
id,
data: vec![],
paused_at: now_millis(),
},
);
assert!(!store.is_empty());
assert_eq!(store.len(), 1);
}
}