Files
poimen-memory/crates/mem-cli/src/idempotency.rs
T

130 lines
3.4 KiB
Rust

use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[cfg(test)]
use serde_json::json;
/// Cached ingest response with expiry
#[derive(Clone, Debug)]
struct CachedResponse {
response: serde_json::Value,
inserted_at: Instant,
ttl: Duration,
}
impl CachedResponse {
fn is_expired(&self) -> bool {
self.inserted_at.elapsed() > self.ttl
}
}
/// Idempotency store for ingest operations
pub struct IdempotencyStore {
cache: Arc<Mutex<HashMap<String, CachedResponse>>>,
ttl: Duration,
}
impl IdempotencyStore {
pub fn new(ttl_seconds: u64) -> Self {
Self {
cache: Arc::new(Mutex::new(HashMap::new())),
ttl: Duration::from_secs(ttl_seconds),
}
}
/// Get cached response for ingest_id. Returns None if not found or expired.
pub fn get(&self, ingest_id: &str) -> Option<serde_json::Value> {
let mut cache = self.cache.lock().unwrap();
if let Some(cached) = cache.get(ingest_id) {
if !cached.is_expired() {
return Some(cached.response.clone());
}
}
// Clean up expired entry
cache.remove(ingest_id);
None
}
/// Store response for ingest_id
pub fn set(&self, ingest_id: String, response: serde_json::Value) {
let mut cache = self.cache.lock().unwrap();
cache.insert(
ingest_id,
CachedResponse {
response,
inserted_at: Instant::now(),
ttl: self.ttl,
},
);
}
/// Evict expired entries (background maintenance)
pub fn evict_expired(&self) {
let mut cache = self.cache.lock().unwrap();
cache.retain(|_, v| !v.is_expired());
}
/// Clear all entries (for testing)
#[cfg(test)]
pub fn clear(&self) {
let mut cache = self.cache.lock().unwrap();
cache.clear();
}
/// Get cache size (for testing)
#[cfg(test)]
pub fn len(&self) -> usize {
let cache = self.cache.lock().unwrap();
cache.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_idempotency_store_basic() {
let store = IdempotencyStore::new(60);
let response = json!({"ingest_id": "test-123", "status": "pending"});
store.set("test-123".to_string(), response.clone());
assert_eq!(store.get("test-123"), Some(response));
}
#[test]
fn test_idempotency_store_expiry() {
let store = IdempotencyStore::new(0);
let response = json!({"ingest_id": "test-123", "status": "pending"});
store.set("test-123".to_string(), response);
std::thread::sleep(Duration::from_millis(10));
assert_eq!(store.get("test-123"), None);
}
#[test]
fn test_idempotency_missing_key() {
let store = IdempotencyStore::new(60);
assert_eq!(store.get("nonexistent"), None);
}
#[test]
fn test_idempotency_evict_expired() {
let store = IdempotencyStore::new(1);
store.set("key1".to_string(), json!({"data": "value1"}));
store.set("key2".to_string(), json!({"data": "value2"}));
assert_eq!(store.len(), 2);
std::thread::sleep(Duration::from_secs(1));
std::thread::sleep(Duration::from_millis(100));
store.evict_expired();
assert_eq!(store.len(), 0);
}
}