Skip to main content

pforge_runtime/
state.rs

1use crate::Result;
2use async_trait::async_trait;
3use std::time::Duration;
4
5/// State management trait for pforge handlers.
6///
7/// Provides a simple key-value interface for persistent or ephemeral state.
8/// Current implementation: `MemoryStateManager` (in-memory, non-persistent).
9///
10/// Future: Will integrate with `trueno-db` KV module (Phase 6) for:
11/// - SIMD-optimized key hashing
12/// - GPU batch operations
13/// - Parquet persistence
14/// - WASM browser storage
15#[async_trait]
16pub trait StateManager: Send + Sync {
17    /// Get a value by key
18    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>>;
19
20    /// Set a value with optional TTL
21    async fn set(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> Result<()>;
22
23    /// Delete a key
24    async fn delete(&self, key: &str) -> Result<()>;
25
26    /// Check if key exists
27    async fn exists(&self, key: &str) -> Result<bool>;
28}
29
30/// Entry with optional expiration time
31struct StateEntry {
32    value: Vec<u8>,
33    expires_at: Option<tokio::time::Instant>,
34}
35
36/// In-memory state manager using DashMap for concurrent access.
37///
38/// This is the default state backend. Data is lost on process restart.
39/// Supports TTL (time-to-live) for automatic key expiration.
40pub struct MemoryStateManager {
41    store: dashmap::DashMap<String, StateEntry>,
42}
43
44impl MemoryStateManager {
45    /// Create a new in-memory state manager
46    #[must_use]
47    pub fn new() -> Self {
48        Self {
49            store: dashmap::DashMap::new(),
50        }
51    }
52}
53
54impl Default for MemoryStateManager {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60#[async_trait]
61impl StateManager for MemoryStateManager {
62    async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
63        if let Some(entry) = self.store.get(key) {
64            // Check if expired
65            if let Some(expires_at) = entry.expires_at {
66                if tokio::time::Instant::now() >= expires_at {
67                    // Key expired, remove it
68                    drop(entry); // Release lock before removing
69                    self.store.remove(key);
70                    return Ok(None);
71                }
72            }
73            Ok(Some(entry.value.clone()))
74        } else {
75            Ok(None)
76        }
77    }
78
79    async fn set(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> Result<()> {
80        let expires_at = ttl.map(|d| tokio::time::Instant::now() + d);
81        self.store
82            .insert(key.to_string(), StateEntry { value, expires_at });
83        Ok(())
84    }
85
86    async fn delete(&self, key: &str) -> Result<()> {
87        self.store.remove(key);
88        Ok(())
89    }
90
91    async fn exists(&self, key: &str) -> Result<bool> {
92        if let Some(entry) = self.store.get(key) {
93            // Check if expired
94            if let Some(expires_at) = entry.expires_at {
95                if tokio::time::Instant::now() >= expires_at {
96                    drop(entry);
97                    self.store.remove(key);
98                    return Ok(false);
99                }
100            }
101            Ok(true)
102        } else {
103            Ok(false)
104        }
105    }
106}
107
108// KV backend (Phase 6), now via the aprender-db package
109#[cfg(feature = "persistence")]
110pub use trueno_kv::TruenoKvStateManager;
111
112#[cfg(feature = "persistence")]
113mod trueno_kv {
114    use super::*;
115    use crate::Error;
116    use tokio::time::Instant;
117    // Crate is `trueno_db`, package is `aprender-db`: the aprender monorepo
118    // keeps the original lib names across the APR-MONO consolidation
119    // (`aprender-db/Cargo.toml` declares `[lib] name = "trueno_db"`), so the
120    // dependency moved but the `use` path did not.
121    use trueno_db::kv::{KvStore, MemoryKvStore};
122
123    /// State manager backed by the trueno_db KV store (aprender-db package).
124    ///
125    /// Provides SIMD-optimized key hashing via `trueno::hash` module.
126    /// Currently uses in-memory storage; future versions will support
127    /// Parquet persistence and WASM browser storage.
128    ///
129    /// TTL support is implemented via a separate expiration tracker.
130    pub struct TruenoKvStateManager {
131        store: MemoryKvStore,
132        /// Tracks expiration times for keys with TTL
133        expirations: dashmap::DashMap<String, Instant>,
134    }
135
136    impl TruenoKvStateManager {
137        /// Create a new trueno-db backed state manager
138        #[must_use]
139        pub fn new() -> Self {
140            Self {
141                store: MemoryKvStore::new(),
142                expirations: dashmap::DashMap::new(),
143            }
144        }
145
146        /// Create with pre-allocated capacity
147        #[must_use]
148        pub fn with_capacity(capacity: usize) -> Self {
149            Self {
150                store: MemoryKvStore::with_capacity(capacity),
151                expirations: dashmap::DashMap::new(),
152            }
153        }
154
155        /// Check if a key has expired and clean up if so
156        fn is_expired(&self, key: &str) -> bool {
157            // First check if expired (read lock only)
158            let expired = if let Some(expires_at) = self.expirations.get(key) {
159                Instant::now() >= *expires_at
160            } else {
161                return false;
162            };
163            // Drop the read lock before attempting write lock to avoid deadlock
164            if expired {
165                self.expirations.remove(key);
166            }
167            expired
168        }
169
170        /// Get the number of stored keys
171        #[must_use]
172        pub fn len(&self) -> usize {
173            self.store.len()
174        }
175
176        /// Check if the store is empty
177        #[must_use]
178        pub fn is_empty(&self) -> bool {
179            self.store.is_empty()
180        }
181
182        /// Clear all stored keys
183        pub fn clear(&self) {
184            self.store.clear();
185        }
186
187        /// Test-only: Directly set an expiration time for a key
188        /// This allows testing expiration without real time delays
189        #[cfg(test)]
190        pub(crate) fn set_expiration_for_test(&self, key: &str, expires_at: Instant) {
191            self.expirations.insert(key.to_string(), expires_at);
192        }
193    }
194
195    impl Default for TruenoKvStateManager {
196        fn default() -> Self {
197            Self::new()
198        }
199    }
200
201    #[async_trait]
202    impl StateManager for TruenoKvStateManager {
203        async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
204            // Check expiration first
205            if self.is_expired(key) {
206                // Key expired - we already cleaned up the expiration tracker in is_expired()
207                // The store entry will be lazily cleaned up on next set() call
208                return Ok(None);
209            }
210
211            self.store
212                .get(key)
213                .await
214                .map_err(|e| Error::StateError(e.to_string()))
215        }
216
217        async fn set(&self, key: &str, value: Vec<u8>, ttl: Option<Duration>) -> Result<()> {
218            // Set expiration time if TTL provided
219            if let Some(duration) = ttl {
220                let expires_at = Instant::now() + duration;
221                self.expirations.insert(key.to_string(), expires_at);
222            } else {
223                // Remove any existing expiration
224                self.expirations.remove(key);
225            }
226
227            self.store
228                .set(key, value)
229                .await
230                .map_err(|e| Error::StateError(e.to_string()))
231        }
232
233        async fn delete(&self, key: &str) -> Result<()> {
234            // Also remove expiration tracking
235            self.expirations.remove(key);
236
237            self.store
238                .delete(key)
239                .await
240                .map_err(|e| Error::StateError(e.to_string()))
241        }
242
243        async fn exists(&self, key: &str) -> Result<bool> {
244            // Check expiration first
245            if self.is_expired(key) {
246                // Key expired - we already cleaned up the expiration tracker in is_expired()
247                // The store entry will be lazily cleaned up on next set() call
248                return Ok(false);
249            }
250
251            self.store
252                .exists(key)
253                .await
254                .map_err(|e| Error::StateError(e.to_string()))
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[tokio::test]
264    async fn test_memory_state_basic() {
265        let state = MemoryStateManager::new();
266
267        // Set and get
268        state.set("key1", b"value1".to_vec(), None).await.unwrap();
269        let value = state.get("key1").await.unwrap();
270        assert_eq!(value, Some(b"value1".to_vec()));
271
272        // Exists
273        assert!(state.exists("key1").await.unwrap());
274        assert!(!state.exists("key2").await.unwrap());
275
276        // Delete
277        state.delete("key1").await.unwrap();
278        assert!(!state.exists("key1").await.unwrap());
279    }
280
281    #[tokio::test]
282    async fn test_memory_state_overwrite() {
283        let state = MemoryStateManager::new();
284
285        state.set("key", b"value1".to_vec(), None).await.unwrap();
286        state.set("key", b"value2".to_vec(), None).await.unwrap();
287
288        let value = state.get("key").await.unwrap();
289        assert_eq!(value, Some(b"value2".to_vec()));
290    }
291
292    #[tokio::test]
293    async fn test_memory_state_concurrent() {
294        use std::sync::Arc;
295
296        let state = Arc::new(MemoryStateManager::new());
297        let mut handles = vec![];
298
299        for i in 0..10 {
300            let state = Arc::clone(&state);
301            handles.push(tokio::spawn(async move {
302                let key = format!("key{i}");
303                let value = format!("value{i}").into_bytes();
304                state.set(&key, value, None).await.unwrap();
305            }));
306        }
307
308        for handle in handles {
309            handle.await.unwrap();
310        }
311
312        for i in 0..10 {
313            let key = format!("key{i}");
314            assert!(state.exists(&key).await.unwrap());
315        }
316    }
317
318    #[tokio::test(start_paused = true)]
319    async fn test_memory_state_ttl_expiration() {
320        let state = MemoryStateManager::new();
321
322        // Set with 50ms TTL
323        state
324            .set(
325                "ttl_key",
326                b"value".to_vec(),
327                Some(Duration::from_millis(50)),
328            )
329            .await
330            .unwrap();
331
332        // Should exist immediately
333        assert!(state.exists("ttl_key").await.unwrap());
334        assert_eq!(state.get("ttl_key").await.unwrap(), Some(b"value".to_vec()));
335
336        // Advance time past expiration (instant with time mocking)
337        tokio::time::advance(Duration::from_millis(60)).await;
338
339        // Should be expired now
340        assert!(!state.exists("ttl_key").await.unwrap());
341        assert_eq!(state.get("ttl_key").await.unwrap(), None);
342    }
343
344    #[tokio::test(start_paused = true)]
345    async fn test_memory_state_ttl_no_expiration() {
346        let state = MemoryStateManager::new();
347
348        // Set without TTL
349        state.set("no_ttl", b"value".to_vec(), None).await.unwrap();
350
351        // Advance time (instant with time mocking)
352        tokio::time::advance(Duration::from_millis(10)).await;
353
354        // Should still exist
355        assert!(state.exists("no_ttl").await.unwrap());
356        assert_eq!(state.get("no_ttl").await.unwrap(), Some(b"value".to_vec()));
357    }
358
359    #[tokio::test(start_paused = true)]
360    async fn test_memory_state_ttl_overwrite_extends() {
361        let state = MemoryStateManager::new();
362
363        // Set with short TTL
364        state
365            .set("key", b"v1".to_vec(), Some(Duration::from_millis(30)))
366            .await
367            .unwrap();
368
369        // Advance time (instant with time mocking)
370        tokio::time::advance(Duration::from_millis(20)).await;
371
372        // Overwrite with longer TTL
373        state
374            .set("key", b"v2".to_vec(), Some(Duration::from_millis(100)))
375            .await
376            .unwrap();
377
378        // Advance past original expiration (instant with time mocking)
379        tokio::time::advance(Duration::from_millis(20)).await;
380
381        // Should still exist with new value
382        assert_eq!(state.get("key").await.unwrap(), Some(b"v2".to_vec()));
383    }
384
385    // trueno-db KV backend tests (Phase 6)
386    #[cfg(feature = "persistence")]
387    mod trueno_kv_tests {
388        use super::*;
389        use crate::state::TruenoKvStateManager;
390
391        #[tokio::test]
392        async fn test_trueno_kv_basic() {
393            let state = TruenoKvStateManager::new();
394
395            // Set and get
396            state.set("key1", b"value1".to_vec(), None).await.unwrap();
397            let value = state.get("key1").await.unwrap();
398            assert_eq!(value, Some(b"value1".to_vec()));
399
400            // Exists
401            assert!(state.exists("key1").await.unwrap());
402            assert!(!state.exists("key2").await.unwrap());
403
404            // Delete
405            state.delete("key1").await.unwrap();
406            assert!(!state.exists("key1").await.unwrap());
407        }
408
409        #[tokio::test]
410        async fn test_trueno_kv_overwrite() {
411            let state = TruenoKvStateManager::new();
412
413            state.set("key", b"value1".to_vec(), None).await.unwrap();
414            state.set("key", b"value2".to_vec(), None).await.unwrap();
415
416            let value = state.get("key").await.unwrap();
417            assert_eq!(value, Some(b"value2".to_vec()));
418        }
419
420        #[tokio::test]
421        async fn test_trueno_kv_with_capacity() {
422            let state = TruenoKvStateManager::with_capacity(100);
423            state.set("key", b"value".to_vec(), None).await.unwrap();
424            assert_eq!(state.get("key").await.unwrap(), Some(b"value".to_vec()));
425        }
426
427        #[tokio::test]
428        async fn test_trueno_kv_len_and_clear() {
429            let state = TruenoKvStateManager::new();
430
431            assert!(state.is_empty());
432            assert_eq!(state.len(), 0);
433
434            state.set("key1", b"value1".to_vec(), None).await.unwrap();
435            assert!(!state.is_empty());
436            assert_eq!(state.len(), 1);
437
438            state.set("key2", b"value2".to_vec(), None).await.unwrap();
439            assert_eq!(state.len(), 2);
440
441            state.clear();
442            assert!(state.is_empty());
443        }
444
445        #[test]
446        fn test_trueno_kv_default() {
447            let state: TruenoKvStateManager = Default::default();
448            assert!(state.is_empty());
449        }
450
451        #[tokio::test]
452        async fn test_trueno_kv_ttl_expiration() {
453            use tokio::time::Instant;
454
455            let state = TruenoKvStateManager::new();
456
457            // Set a key without TTL first (TTL will be set via test helper)
458            state
459                .set("ttl_key", b"value".to_vec(), None)
460                .await
461                .expect("set should succeed");
462
463            // Should exist initially
464            assert!(state
465                .exists("ttl_key")
466                .await
467                .expect("exists check should succeed"));
468
469            // Set expiration to current time (should be considered expired immediately
470            // since is_expired uses >= comparison)
471            state.set_expiration_for_test("ttl_key", Instant::now());
472
473            // Small yield to ensure time has advanced past the expiration
474            tokio::task::yield_now().await;
475
476            // Should be expired now - just check exists (get would try to access
477            // store after expiration is already cleaned up, which has different semantics)
478            assert!(!state
479                .exists("ttl_key")
480                .await
481                .expect("exists check should succeed"));
482        }
483
484        #[tokio::test]
485        async fn test_trueno_kv_ttl_no_expiration() {
486            use tokio::time::Instant;
487
488            let state = TruenoKvStateManager::new();
489
490            // Set without TTL
491            state
492                .set("no_ttl", b"value".to_vec(), None)
493                .await
494                .expect("set should succeed");
495
496            // Set expiration to a time in the future (should not expire)
497            let future = Instant::now() + Duration::from_secs(3600);
498            state.set_expiration_for_test("no_ttl", future);
499
500            // Should still exist
501            assert!(state
502                .exists("no_ttl")
503                .await
504                .expect("exists check should succeed"));
505            assert_eq!(
506                state.get("no_ttl").await.expect("get should succeed"),
507                Some(b"value".to_vec())
508            );
509        }
510    }
511}