@@ -15,7 +15,6 @@ use chrono::Duration;
1515use lru:: LruCache ;
1616use parking_lot:: RwLock ;
1717use serde:: { Deserialize , Serialize } ;
18- use tokio:: sync:: RwLock as AsyncRwLock ;
1918use tracing:: { debug, info} ;
2019
2120use super :: types:: { MemoEntry , MemoKey , MemoOpType , MemoStats , MemoValue } ;
@@ -41,8 +40,8 @@ struct MemoStoreData {
4140 stats : MemoStats ,
4241}
4342
44- /// Atomic statistics for lock-free access.
45- #[ derive( Debug , Default ) ]
43+ /// Lock-free atomic statistics for concurrent access.
44+ #[ derive( Debug ) ]
4645struct AtomicStats {
4746 hits : AtomicU64 ,
4847 misses : AtomicU64 ,
@@ -77,6 +76,12 @@ impl AtomicStats {
7776 self . tokens_saved . load ( Ordering :: Relaxed ) ,
7877 )
7978 }
79+
80+ fn load_from ( & self , hits : u64 , misses : u64 , tokens_saved : u64 ) {
81+ self . hits . store ( hits, Ordering :: Relaxed ) ;
82+ self . misses . store ( misses, Ordering :: Relaxed ) ;
83+ self . tokens_saved . store ( tokens_saved, Ordering :: Relaxed ) ;
84+ }
8085}
8186
8287/// LLM Memoization store.
@@ -90,7 +95,7 @@ impl AtomicStats {
9095/// # Example
9196///
9297/// ```rust,ignore
93- /// let store = MemoStore::new(1000 );
98+ /// let store = MemoStore::new();
9499///
95100/// let summary = store.get_or_compute(
96101/// MemoKey::summary(&content_fp),
@@ -103,8 +108,8 @@ pub struct MemoStore {
103108 /// LRU cache for entries.
104109 cache : Arc < RwLock < LruCache < String , MemoEntry > > > ,
105110
106- /// Statistics (async for safe updates) .
107- stats : Arc < AsyncRwLock < MemoStats > > ,
111+ /// Lock-free statistics .
112+ stats : Arc < AtomicStats > ,
108113
109114 /// TTL for entries.
110115 ttl : Duration ,
@@ -152,7 +157,7 @@ impl MemoStore {
152157 std:: num:: NonZeroUsize :: new ( capacity)
153158 . unwrap_or ( std:: num:: NonZeroUsize :: new ( 1000 ) . unwrap ( ) ) ,
154159 ) ) ) ,
155- stats : Arc :: new ( AsyncRwLock :: new ( MemoStats :: default ( ) ) ) ,
160+ stats : Arc :: new ( AtomicStats :: new ( ) ) ,
156161 ttl : DEFAULT_TTL ,
157162 model_id : None ,
158163 version : 1 ,
@@ -183,13 +188,10 @@ impl MemoStore {
183188 let mut cache = self . cache . write ( ) ;
184189
185190 if let Some ( entry) = cache. get_mut ( & full_key) {
186- // Check TTL
187191 if entry. is_expired ( self . ttl ) {
188192 cache. pop ( & full_key) ;
189193 return None ;
190194 }
191-
192- // Record hit
193195 entry. record_hit ( ) ;
194196 debug ! ( "Memo cache hit for {:?}" , key. op_type) ;
195197 return Some ( entry. value . clone ( ) ) ;
@@ -226,29 +228,21 @@ impl MemoStore {
226228 {
227229 // Check cache first (synchronous)
228230 if let Some ( value) = self . get ( & key) {
229- // Update stats
230- let mut stats = self . stats . write ( ) . await ;
231- stats. hits += 1 ;
231+ self . stats . record_hit ( ) ;
232232 return Ok ( value) ;
233233 }
234234
235235 // Record miss
236- {
237- let mut stats = self . stats . write ( ) . await ;
238- stats. misses += 1 ;
239- }
236+ self . stats . record_miss ( ) ;
240237
241238 // Compute
242239 let ( value, tokens) = compute ( ) . await ?;
243240
244241 // Cache result
245242 self . put_with_tokens ( key. clone ( ) , value. clone ( ) , tokens) ;
246243
247- // Update stats
248- {
249- let mut stats = self . stats . write ( ) . await ;
250- stats. tokens_saved += tokens;
251- }
244+ // Update tokens saved
245+ self . stats . add_tokens_saved ( tokens) ;
252246
253247 Ok ( value)
254248 }
@@ -285,50 +279,25 @@ impl MemoStore {
285279 self . len ( ) == 0
286280 }
287281
288- /// Get cache statistics.
289- pub async fn stats ( & self ) -> MemoStats {
290- let stats = self . stats . read ( ) . await ;
291- let mut result = stats. clone ( ) ;
292- result. entries = self . len ( ) ;
293- result
294- }
295-
296- /// Get cache statistics synchronously.
297- ///
298- /// This acquires a read lock on the stats, which is generally fast.
299- /// Use this when you need stats without async context.
300- pub fn stats_snapshot ( & self ) -> MemoStats {
301- // Use try_read to avoid blocking; fall back to defaults if locked
302- match self . stats . try_read ( ) {
303- Ok ( stats) => {
304- let mut result = stats. clone ( ) ;
305- result. entries = self . len ( ) ;
306- result
307- }
308- Err ( _) => MemoStats {
309- entries : self . len ( ) ,
310- ..Default :: default ( )
311- } ,
282+ /// Get cache statistics (synchronous, lock-free).
283+ pub fn stats ( & self ) -> MemoStats {
284+ let ( hits, misses, tokens_saved) = self . stats . snapshot ( ) ;
285+ MemoStats {
286+ entries : self . len ( ) ,
287+ hits,
288+ misses,
289+ tokens_saved,
290+ cost_saved : 0.0 ,
312291 }
313292 }
314293
315294 /// Invalidate all entries of a specific operation type.
316295 ///
317- /// This is useful for batch invalidation when the algorithm for
318- /// a specific operation type changes.
319- ///
320- /// # Example
321- ///
322- /// ```rust,ignore
323- /// // Invalidate all pilot decision caches
324- /// let removed = store.invalidate_by_op_type(MemoOpType::PilotDecision);
325- /// println!("Removed {} cached pilot decisions", removed);
326- /// ```
296+ /// Useful when the algorithm for a specific operation changes.
327297 pub fn invalidate_by_op_type ( & self , op_type : MemoOpType ) -> usize {
328298 let mut cache = self . cache . write ( ) ;
329299 let before = cache. len ( ) ;
330300
331- // Collect keys to remove based on entry value type
332301 let keys_to_remove: Vec < String > = cache
333302 . iter ( )
334303 . filter_map ( |( key, entry) | {
@@ -343,7 +312,6 @@ impl MemoStore {
343312 } )
344313 . collect ( ) ;
345314
346- // Remove entries
347315 for key in keys_to_remove {
348316 cache. pop ( & key) ;
349317 }
@@ -357,21 +325,11 @@ impl MemoStore {
357325
358326 /// Invalidate all entries matching a model ID prefix.
359327 ///
360- /// This is useful when switching models or when a model's behavior changes.
361- ///
362- /// # Example
363- ///
364- /// ```rust,ignore
365- /// // Invalidate all GPT-4 caches
366- /// let removed = store.invalidate_by_model_prefix("gpt-4");
367- /// ```
328+ /// Useful when switching models or when a model's behavior changes.
368329 pub fn invalidate_by_model_prefix ( & self , prefix : & str ) -> usize {
369330 let mut cache = self . cache . write ( ) ;
370331 let before = cache. len ( ) ;
371332
372- // Since the key is a fingerprint, we need to check model_id from entries
373- // For now, we'll clear all entries if prefix matches our model_id
374- // A better approach would be to store model_id in entry metadata
375333 let should_clear = self
376334 . model_id
377335 . as_ref ( )
@@ -396,14 +354,12 @@ impl MemoStore {
396354 let mut cache = self . cache . write ( ) ;
397355 let before = cache. len ( ) ;
398356
399- // Collect expired keys
400357 let expired: Vec < String > = cache
401358 . iter ( )
402359 . filter ( |( _, entry) | entry. is_expired ( self . ttl ) )
403360 . map ( |( k, _) | k. clone ( ) )
404361 . collect ( ) ;
405362
406- // Remove expired entries
407363 for key in expired {
408364 cache. pop ( & key) ;
409365 }
@@ -417,16 +373,19 @@ impl MemoStore {
417373
418374 /// Save the cache to disk.
419375 pub async fn save ( & self , path : & Path ) -> Result < ( ) > {
376+ // Prune expired entries before persisting
377+ self . prune_expired ( ) ;
378+
420379 let cache = self . cache . read ( ) ;
421- let stats = self . stats . read ( ) . await ;
380+ let stats = self . stats ( ) ;
422381
423382 let entries: HashMap < String , MemoEntry > =
424383 cache. iter ( ) . map ( |( k, v) | ( k. clone ( ) , v. clone ( ) ) ) . collect ( ) ;
425384
426385 let data = MemoStoreData {
427386 version : 1 ,
428387 entries,
429- stats : stats . clone ( ) ,
388+ stats,
430389 } ;
431390
432391 let parent = path
@@ -459,20 +418,15 @@ impl MemoStore {
459418 . map_err ( |e| crate :: Error :: Parse ( format ! ( "Failed to deserialize memo store: {}" , e) ) ) ?;
460419
461420 let mut cache = self . cache . write ( ) ;
462- let mut stats = self . stats . write ( ) . await ;
463421
464422 for ( key, entry) in data. entries {
465- // Skip expired entries
466423 if !entry. is_expired ( self . ttl ) {
467424 cache. put ( key, entry) ;
468425 }
469426 }
470427
471- stats. entries = cache. len ( ) ;
472- stats. hits = data. stats . hits ;
473- stats. misses = data. stats . misses ;
474- stats. tokens_saved = data. stats . tokens_saved ;
475- stats. cost_saved = data. stats . cost_saved ;
428+ // Restore stats
429+ self . stats . load_from ( data. stats . hits , data. stats . misses , data. stats . tokens_saved ) ;
476430
477431 info ! (
478432 "Loaded memo store with {} entries from {:?}" ,
@@ -484,7 +438,6 @@ impl MemoStore {
484438
485439 /// Make a full cache key from a MemoKey.
486440 fn make_key ( & self , key : & MemoKey ) -> String {
487- // Include model_id and version in the key
488441 let mut key_with_context = key. clone ( ) ;
489442 if key_with_context. model_id . is_none ( ) {
490443 key_with_context. model_id = self . model_id . clone ( ) ;
@@ -616,7 +569,6 @@ mod tests {
616569 store. put ( key, MemoValue :: Summary ( format ! ( "Summary {}" , i) ) ) ;
617570 }
618571
619- // Only 3 entries should remain
620572 assert_eq ! ( store. len( ) , 3 ) ;
621573 }
622574
@@ -656,7 +608,7 @@ mod tests {
656608 . unwrap ( ) ;
657609
658610 assert_eq ! ( result2. as_summary( ) , Some ( "Computed" ) ) ;
659- assert_eq ! ( call_count. load( std:: sync:: atomic:: Ordering :: SeqCst ) , 1 ) ; // Still 1
611+ assert_eq ! ( call_count. load( std:: sync:: atomic:: Ordering :: SeqCst ) , 1 ) ;
660612 }
661613
662614 #[ tokio:: test]
@@ -699,15 +651,15 @@ mod tests {
699651 . await
700652 . unwrap ( ) ;
701653
702- // Hit via get_or_compute (this updates global stats)
654+ // Hit
703655 store
704656 . get_or_compute ( key. clone ( ) , || async {
705657 Ok ( ( MemoValue :: Summary ( "Should not be called" . to_string ( ) ) , 0 ) )
706658 } )
707659 . await
708660 . unwrap ( ) ;
709661
710- let stats = store. stats ( ) . await ;
662+ let stats = store. stats ( ) ;
711663 assert_eq ! ( stats. misses, 1 ) ;
712664 assert_eq ! ( stats. hits, 1 ) ;
713665 assert_eq ! ( stats. tokens_saved, 100 ) ;
0 commit comments