llm_optimizer_processor/state/
memory.rs

1//! In-memory state backend implementation
2//!
3//! This module provides a high-performance in-memory state backend using DashMap
4//! for concurrent access. It supports optional TTL (time-to-live) for automatic
5//! state cleanup and provides checkpoint/restore capabilities.
6
7use async_trait::async_trait;
8use chrono::{DateTime, Utc};
9use dashmap::DashMap;
10use std::sync::Arc;
11use std::time::Duration;
12use tokio::sync::RwLock;
13use tracing::{debug, trace, warn};
14
15use super::backend::StateBackend;
16use crate::error::StateResult;
17
18/// Entry in the memory state backend with TTL support
19#[derive(Debug, Clone)]
20struct StateEntry {
21    /// The actual value stored
22    value: Vec<u8>,
23    /// When this entry was created
24    created_at: DateTime<Utc>,
25    /// When this entry was last accessed
26    accessed_at: DateTime<Utc>,
27    /// When this entry was last modified
28    modified_at: DateTime<Utc>,
29}
30
31impl StateEntry {
32    /// Create a new state entry
33    fn new(value: Vec<u8>) -> Self {
34        let now = Utc::now();
35        Self {
36            value,
37            created_at: now,
38            accessed_at: now,
39            modified_at: now,
40        }
41    }
42
43    /// Check if this entry has expired
44    fn is_expired(&self, ttl: Duration) -> bool {
45        let age = Utc::now().signed_duration_since(self.modified_at);
46        age.num_milliseconds() as u64 > ttl.as_millis() as u64
47    }
48
49    /// Update the accessed timestamp
50    fn touch(&mut self) {
51        self.accessed_at = Utc::now();
52    }
53
54    /// Update the value and modified timestamp
55    fn update(&mut self, new_value: Vec<u8>) {
56        self.value = new_value;
57        self.modified_at = Utc::now();
58        self.accessed_at = Utc::now();
59    }
60}
61
62/// Statistics about the memory state backend
63#[derive(Debug, Clone, Default)]
64pub struct MemoryBackendStats {
65    /// Total number of entries
66    pub total_entries: usize,
67    /// Number of expired entries
68    pub expired_entries: usize,
69    /// Total bytes stored
70    pub total_bytes: usize,
71    /// Number of get operations
72    pub get_count: u64,
73    /// Number of put operations
74    pub put_count: u64,
75    /// Number of delete operations
76    pub delete_count: u64,
77    /// Number of cache hits
78    pub hit_count: u64,
79    /// Number of cache misses
80    pub miss_count: u64,
81}
82
83/// In-memory state backend using DashMap
84///
85/// This backend stores all state in memory using a concurrent hash map (DashMap).
86/// It's ideal for:
87///
88/// - **Testing**: Fast, no external dependencies
89/// - **Stateless processing**: When state doesn't need to survive restarts
90/// - **Small state**: When state fits comfortably in memory
91/// - **High throughput**: Fastest possible state access
92///
93/// ## Features
94///
95/// - **Thread-safe**: Uses DashMap for lock-free concurrent access
96/// - **TTL support**: Automatic expiration of old entries
97/// - **Statistics**: Tracks access patterns and cache hit rates
98/// - **Checkpoint/Restore**: Can snapshot state to disk
99///
100/// ## Example
101///
102/// ```rust,no_run
103/// use processor::state::{MemoryStateBackend, StateBackend};
104/// use std::time::Duration;
105///
106/// #[tokio::main]
107/// async fn main() -> anyhow::Result<()> {
108///     // Create backend with 1-hour TTL
109///     let backend = MemoryStateBackend::new(Some(Duration::from_secs(3600)));
110///
111///     // Store state
112///     backend.put(b"session:123", b"user_data").await?;
113///
114///     // Retrieve state
115///     if let Some(data) = backend.get(b"session:123").await? {
116///         println!("Session data: {:?}", data);
117///     }
118///
119///     // Get statistics
120///     let stats = backend.stats().await;
121///     println!("Hit rate: {:.2}%",
122///         100.0 * stats.hit_count as f64 / (stats.hit_count + stats.miss_count) as f64);
123///
124///     Ok(())
125/// }
126/// ```
127pub struct MemoryStateBackend {
128    /// The actual state storage
129    data: Arc<DashMap<Vec<u8>, StateEntry>>,
130    /// Optional TTL for automatic expiration
131    ttl: Option<Duration>,
132    /// Statistics tracking
133    stats: Arc<RwLock<MemoryBackendStats>>,
134}
135
136impl MemoryStateBackend {
137    /// Create a new in-memory state backend
138    ///
139    /// # Arguments
140    ///
141    /// * `ttl` - Optional time-to-live for entries. If None, entries never expire.
142    ///
143    /// # Returns
144    ///
145    /// A new `MemoryStateBackend` instance
146    ///
147    /// # Example
148    ///
149    /// ```rust
150    /// use processor::state::MemoryStateBackend;
151    /// use std::time::Duration;
152    ///
153    /// // No TTL - entries never expire
154    /// let backend1 = MemoryStateBackend::new(None);
155    ///
156    /// // 1 hour TTL
157    /// let backend2 = MemoryStateBackend::new(Some(Duration::from_secs(3600)));
158    /// ```
159    pub fn new(ttl: Option<Duration>) -> Self {
160        Self {
161            data: Arc::new(DashMap::new()),
162            ttl,
163            stats: Arc::new(RwLock::new(MemoryBackendStats::default())),
164        }
165    }
166
167    /// Get current statistics
168    ///
169    /// # Returns
170    ///
171    /// A snapshot of current backend statistics
172    pub async fn stats(&self) -> MemoryBackendStats {
173        self.stats.read().await.clone()
174    }
175
176    /// Clean up expired entries
177    ///
178    /// This method scans all entries and removes those that have exceeded the TTL.
179    /// Returns the number of entries removed.
180    ///
181    /// # Returns
182    ///
183    /// Number of entries removed
184    ///
185    /// # Example
186    ///
187    /// ```rust,no_run
188    /// # use processor::state::MemoryStateBackend;
189    /// # use std::time::Duration;
190    /// # #[tokio::main]
191    /// # async fn main() -> anyhow::Result<()> {
192    /// let backend = MemoryStateBackend::new(Some(Duration::from_secs(60)));
193    ///
194    /// // Periodically clean up expired entries
195    /// let removed = backend.cleanup_expired().await;
196    /// println!("Removed {} expired entries", removed);
197    /// # Ok(())
198    /// # }
199    /// ```
200    pub async fn cleanup_expired(&self) -> usize {
201        let ttl = match self.ttl {
202            Some(ttl) => ttl,
203            None => return 0, // No TTL, nothing to clean up
204        };
205
206        let mut removed = 0;
207        let keys_to_remove: Vec<Vec<u8>> = self
208            .data
209            .iter()
210            .filter_map(|entry| {
211                if entry.value().is_expired(ttl) {
212                    Some(entry.key().clone())
213                } else {
214                    None
215                }
216            })
217            .collect();
218
219        for key in keys_to_remove {
220            self.data.remove(&key);
221            removed += 1;
222        }
223
224        if removed > 0 {
225            debug!("Cleaned up {} expired entries", removed);
226            let mut stats = self.stats.write().await;
227            stats.expired_entries += removed;
228        }
229
230        removed
231    }
232
233    /// Start automatic cleanup background task
234    ///
235    /// Spawns a background task that periodically cleans up expired entries.
236    ///
237    /// # Arguments
238    ///
239    /// * `interval` - How often to run cleanup
240    ///
241    /// # Returns
242    ///
243    /// A handle that cancels the cleanup task when dropped
244    ///
245    /// # Example
246    ///
247    /// ```rust,no_run
248    /// # use processor::state::MemoryStateBackend;
249    /// # use std::time::Duration;
250    /// # #[tokio::main]
251    /// # async fn main() -> anyhow::Result<()> {
252    /// let backend = MemoryStateBackend::new(Some(Duration::from_secs(3600)));
253    ///
254    /// // Clean up every 5 minutes
255    /// let _cleanup_handle = backend.start_cleanup_task(Duration::from_secs(300));
256    ///
257    /// // Cleanup runs in background...
258    /// # Ok(())
259    /// # }
260    /// ```
261    pub fn start_cleanup_task(&self, interval: Duration) -> tokio::task::JoinHandle<()> {
262        let backend = self.clone();
263        tokio::spawn(async move {
264            let mut interval = tokio::time::interval(interval);
265            loop {
266                interval.tick().await;
267                let removed = backend.cleanup_expired().await;
268                if removed > 0 {
269                    debug!("Background cleanup removed {} entries", removed);
270                }
271            }
272        })
273    }
274
275    /// Create a checkpoint of current state
276    ///
277    /// Returns all key-value pairs as a vector for serialization.
278    ///
279    /// # Returns
280    ///
281    /// Vector of (key, value) pairs
282    pub async fn checkpoint(&self) -> Vec<(Vec<u8>, Vec<u8>)> {
283        self.data
284            .iter()
285            .map(|entry| (entry.key().clone(), entry.value().value.clone()))
286            .collect()
287    }
288
289    /// Restore state from a checkpoint
290    ///
291    /// Replaces all current state with the provided checkpoint data.
292    ///
293    /// # Arguments
294    ///
295    /// * `checkpoint` - Vector of (key, value) pairs to restore
296    pub async fn restore(&self, checkpoint: Vec<(Vec<u8>, Vec<u8>)>) -> StateResult<()> {
297        self.data.clear();
298
299        for (key, value) in checkpoint {
300            self.data.insert(key, StateEntry::new(value));
301        }
302
303        let mut stats = self.stats.write().await;
304        stats.total_entries = self.data.len();
305
306        Ok(())
307    }
308
309    /// Get total memory usage in bytes (approximate)
310    ///
311    /// # Returns
312    ///
313    /// Approximate memory usage in bytes
314    pub async fn memory_usage(&self) -> usize {
315        self.data
316            .iter()
317            .map(|entry| {
318                entry.key().len() + entry.value().value.len() + std::mem::size_of::<StateEntry>()
319            })
320            .sum()
321    }
322}
323
324// Clone implementation for use in background tasks
325impl Clone for MemoryStateBackend {
326    fn clone(&self) -> Self {
327        Self {
328            data: Arc::clone(&self.data),
329            ttl: self.ttl,
330            stats: Arc::clone(&self.stats),
331        }
332    }
333}
334
335#[async_trait]
336impl StateBackend for MemoryStateBackend {
337    async fn get(&self, key: &[u8]) -> StateResult<Option<Vec<u8>>> {
338        trace!("Getting key: {:?}", key);
339
340        let mut stats = self.stats.write().await;
341        stats.get_count += 1;
342
343        // Check if entry exists
344        let result = if let Some(mut entry) = self.data.get_mut(key) {
345            // Check TTL
346            if let Some(ttl) = self.ttl {
347                if entry.is_expired(ttl) {
348                    warn!("Key expired: {:?}", key);
349                    drop(entry); // Release the lock before removing
350                    self.data.remove(key);
351                    stats.miss_count += 1;
352                    stats.expired_entries += 1;
353                    return Ok(None);
354                }
355            }
356
357            // Update access time
358            entry.touch();
359            stats.hit_count += 1;
360            Some(entry.value.clone())
361        } else {
362            stats.miss_count += 1;
363            None
364        };
365
366        Ok(result)
367    }
368
369    async fn put(&self, key: &[u8], value: &[u8]) -> StateResult<()> {
370        trace!("Putting key: {:?}, value size: {} bytes", key, value.len());
371
372        let mut stats = self.stats.write().await;
373        stats.put_count += 1;
374
375        if let Some(mut entry) = self.data.get_mut(key) {
376            entry.update(value.to_vec());
377        } else {
378            self.data.insert(key.to_vec(), StateEntry::new(value.to_vec()));
379            stats.total_entries += 1;
380        }
381
382        Ok(())
383    }
384
385    async fn delete(&self, key: &[u8]) -> StateResult<()> {
386        trace!("Deleting key: {:?}", key);
387
388        let mut stats = self.stats.write().await;
389        stats.delete_count += 1;
390
391        if self.data.remove(key).is_some() {
392            stats.total_entries = stats.total_entries.saturating_sub(1);
393        }
394
395        Ok(())
396    }
397
398    async fn list_keys(&self, prefix: &[u8]) -> StateResult<Vec<Vec<u8>>> {
399        trace!("Listing keys with prefix: {:?}", prefix);
400
401        let keys: Vec<Vec<u8>> = if prefix.is_empty() {
402            self.data.iter().map(|entry| entry.key().clone()).collect()
403        } else {
404            self.data
405                .iter()
406                .filter(|entry| entry.key().starts_with(prefix))
407                .map(|entry| entry.key().clone())
408                .collect()
409        };
410
411        Ok(keys)
412    }
413
414    async fn clear(&self) -> StateResult<()> {
415        debug!("Clearing all state");
416        self.data.clear();
417
418        let mut stats = self.stats.write().await;
419        stats.total_entries = 0;
420
421        Ok(())
422    }
423
424    async fn count(&self) -> StateResult<usize> {
425        Ok(self.data.len())
426    }
427
428    async fn contains(&self, key: &[u8]) -> StateResult<bool> {
429        Ok(self.data.contains_key(key))
430    }
431}
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436    use crate::state::backend::tests::*;
437
438    #[tokio::test]
439    async fn test_memory_backend_basic() {
440        let backend = MemoryStateBackend::new(None);
441        test_backend_basic_ops(backend).await;
442    }
443
444    #[tokio::test]
445    async fn test_memory_backend_list_keys() {
446        let backend = MemoryStateBackend::new(None);
447        test_backend_list_keys(backend).await;
448    }
449
450    #[tokio::test]
451    async fn test_memory_backend_clear() {
452        let backend = MemoryStateBackend::new(None);
453        test_backend_clear(backend).await;
454    }
455
456    #[tokio::test]
457    async fn test_memory_backend_contains() {
458        let backend = MemoryStateBackend::new(None);
459        test_backend_contains(backend).await;
460    }
461
462    #[tokio::test]
463    async fn test_ttl_expiration() {
464        let backend = MemoryStateBackend::new(Some(Duration::from_millis(100)));
465
466        backend.put(b"key1", b"value1").await.unwrap();
467        assert!(backend.get(b"key1").await.unwrap().is_some());
468
469        // Wait for TTL to expire
470        tokio::time::sleep(Duration::from_millis(150)).await;
471
472        // Entry should be expired
473        assert!(backend.get(b"key1").await.unwrap().is_none());
474    }
475
476    #[tokio::test]
477    async fn test_cleanup_expired() {
478        let backend = MemoryStateBackend::new(Some(Duration::from_millis(100)));
479
480        backend.put(b"key1", b"value1").await.unwrap();
481        backend.put(b"key2", b"value2").await.unwrap();
482        backend.put(b"key3", b"value3").await.unwrap();
483
484        assert_eq!(backend.count().await.unwrap(), 3);
485
486        // Wait for entries to expire
487        tokio::time::sleep(Duration::from_millis(150)).await;
488
489        let removed = backend.cleanup_expired().await;
490        assert_eq!(removed, 3);
491        assert_eq!(backend.count().await.unwrap(), 0);
492    }
493
494    #[tokio::test]
495    async fn test_checkpoint_restore() {
496        let backend = MemoryStateBackend::new(None);
497
498        backend.put(b"key1", b"value1").await.unwrap();
499        backend.put(b"key2", b"value2").await.unwrap();
500
501        // Create checkpoint
502        let checkpoint = backend.checkpoint().await;
503        assert_eq!(checkpoint.len(), 2);
504
505        // Clear and restore
506        backend.clear().await.unwrap();
507        assert_eq!(backend.count().await.unwrap(), 0);
508
509        backend.restore(checkpoint).await.unwrap();
510        assert_eq!(backend.count().await.unwrap(), 2);
511
512        assert_eq!(
513            backend.get(b"key1").await.unwrap(),
514            Some(b"value1".to_vec())
515        );
516    }
517
518    #[tokio::test]
519    async fn test_stats() {
520        let backend = MemoryStateBackend::new(None);
521
522        backend.put(b"key1", b"value1").await.unwrap();
523        backend.put(b"key2", b"value2").await.unwrap();
524
525        backend.get(b"key1").await.unwrap(); // Hit
526        backend.get(b"key1").await.unwrap(); // Hit
527        backend.get(b"nonexistent").await.unwrap(); // Miss
528
529        backend.delete(b"key1").await.unwrap();
530
531        let stats = backend.stats().await;
532        assert_eq!(stats.put_count, 2);
533        assert_eq!(stats.get_count, 3);
534        assert_eq!(stats.delete_count, 1);
535        assert_eq!(stats.hit_count, 2);
536        assert_eq!(stats.miss_count, 1);
537    }
538
539    #[tokio::test]
540    async fn test_memory_usage() {
541        let backend = MemoryStateBackend::new(None);
542
543        let initial_usage = backend.memory_usage().await;
544        assert_eq!(initial_usage, 0);
545
546        backend.put(b"key1", b"value1").await.unwrap();
547        let usage_after_put = backend.memory_usage().await;
548        assert!(usage_after_put > initial_usage);
549
550        backend.delete(b"key1").await.unwrap();
551        let usage_after_delete = backend.memory_usage().await;
552        assert_eq!(usage_after_delete, 0);
553    }
554
555    #[tokio::test]
556    async fn test_concurrent_access() {
557        use std::sync::Arc;
558
559        let backend = Arc::new(MemoryStateBackend::new(None));
560        let mut handles = vec![];
561
562        // Spawn multiple tasks writing concurrently
563        for i in 0..10 {
564            let backend = Arc::clone(&backend);
565            let handle = tokio::spawn(async move {
566                for j in 0..10 {
567                    let key = format!("key:{}:{}", i, j).into_bytes();
568                    let value = format!("value:{}:{}", i, j).into_bytes();
569                    backend.put(&key, &value).await.unwrap();
570                }
571            });
572            handles.push(handle);
573        }
574
575        // Wait for all tasks
576        for handle in handles {
577            handle.await.unwrap();
578        }
579
580        // Verify all entries were written
581        assert_eq!(backend.count().await.unwrap(), 100);
582    }
583}