1616"""Least Frequently Used (LFU) cache implementation."""
1717
1818import asyncio
19- import json
2019import logging
21- import os
2220import threading
2321import time
2422from typing import Any , Callable , Optional
@@ -88,8 +86,6 @@ def __init__(
8886 self ,
8987 capacity : int ,
9088 track_stats : bool = False ,
91- persistence_interval : Optional [float ] = None ,
92- persistence_path : Optional [str ] = None ,
9389 stats_logging_interval : Optional [float ] = None ,
9490 ) -> None :
9591 """
@@ -98,8 +94,6 @@ def __init__(
9894 Args:
9995 capacity: Maximum number of items the cache can hold
10096 track_stats: Enable tracking of cache statistics
101- persistence_interval: Seconds between periodic dumps to disk (None disables persistence)
102- persistence_path: Path to persistence file (defaults to 'lfu_cache.json' if persistence enabled)
10397 stats_logging_interval: Seconds between periodic stats logging (None disables logging)
10498 """
10599 if capacity < 0 :
@@ -114,12 +108,6 @@ def __init__(
114108 self .freq_map : dict [int , DoublyLinkedList ] = {} # frequency -> list of nodes
115109 self .min_freq = 0 # Track minimum frequency for eviction
116110
117- # Persistence configuration
118- self .persistence_interval = persistence_interval
119- self .persistence_path = persistence_path or "lfu_cache.json"
120- # Initialize to None to ensure first check doesn't trigger immediately
121- self .last_persist_time = None
122-
123111 # Stats logging configuration
124112 self .stats_logging_interval = stats_logging_interval
125113 # Initialize to None to ensure first check doesn't trigger immediately
@@ -135,10 +123,6 @@ def __init__(
135123 "updates" : 0 ,
136124 }
137125
138- # Load from disk if persistence is enabled and file exists
139- if self .persistence_interval is not None :
140- self ._load_from_disk ()
141-
142126 def _update_node_freq (self , node : LFUNode ) -> None :
143127 """Update the frequency of a node and move it to the appropriate frequency list."""
144128 old_freq = node .freq
@@ -175,9 +159,6 @@ def get(self, key: Any, default: Any = None) -> Any:
175159 The value associated with the key, or default if not found
176160 """
177161 with self ._lock :
178- # Check if we should persist
179- self ._check_and_persist ()
180-
181162 # Check if we should log stats
182163 self ._check_and_log_stats ()
183164
@@ -203,9 +184,6 @@ def put(self, key: Any, value: Any) -> None:
203184 value: The value to associate with the key
204185 """
205186 with self ._lock :
206- # Check if we should persist
207- self ._check_and_persist ()
208-
209187 # Check if we should log stats
210188 self ._check_and_log_stats ()
211189
@@ -312,109 +290,6 @@ def reset_stats(self) -> None:
312290 "updates" : 0 ,
313291 }
314292
315- def _check_and_persist (self ) -> None :
316- """Check if enough time has passed and persist to disk if needed."""
317- if self .persistence_interval is None :
318- return
319-
320- current_time = time .time ()
321-
322- # Initialize timestamp on first check
323- if self .last_persist_time is None :
324- self .last_persist_time = current_time
325- return
326-
327- if current_time - self .last_persist_time >= self .persistence_interval :
328- self ._persist_to_disk ()
329- self .last_persist_time = current_time
330-
331- def _persist_to_disk (self ) -> None :
332- """
333- Serialize cache to disk.
334-
335- Stores cache data as JSON with node information including keys, values,
336- frequencies, and timestamps for reconstruction.
337- """
338- if not self .key_map :
339- # If cache is empty, remove the persistence file
340- if os .path .exists (self .persistence_path ):
341- os .remove (self .persistence_path )
342- return
343-
344- cache_data = {
345- "capacity" : self ._capacity ,
346- "min_freq" : self .min_freq ,
347- "nodes" : [],
348- }
349-
350- # Serialize all nodes
351- for key , node in self .key_map .items ():
352- cache_data ["nodes" ].append (
353- {
354- "key" : key ,
355- "value" : node .value ,
356- "freq" : node .freq ,
357- "created_at" : node .created_at ,
358- "accessed_at" : node .accessed_at ,
359- }
360- )
361-
362- # Write to disk
363- try :
364- with open (self .persistence_path , "w" ) as f :
365- json .dump (cache_data , f , indent = 2 )
366- except Exception as e :
367- # Silently fail on persistence errors to not disrupt cache operations
368- pass
369-
370- def _load_from_disk (self ) -> None :
371- """
372- Load cache from disk if persistence file exists.
373-
374- Reconstructs the cache state including frequency lists and node relationships.
375- """
376- if not os .path .exists (self .persistence_path ):
377- return
378-
379- try :
380- with open (self .persistence_path , "r" ) as f :
381- cache_data = json .load (f )
382-
383- # Reconstruct cache
384- self .min_freq = cache_data .get ("min_freq" , 0 )
385-
386- for node_data in cache_data .get ("nodes" , []):
387- # Create node
388- node = LFUNode (node_data ["key" ], node_data ["value" ])
389- node .freq = node_data ["freq" ]
390- node .created_at = node_data ["created_at" ]
391- node .accessed_at = node_data ["accessed_at" ]
392-
393- # Add to key map
394- self .key_map [node .key ] = node
395-
396- # Add to appropriate frequency list
397- if node .freq not in self .freq_map :
398- self .freq_map [node .freq ] = DoublyLinkedList ()
399- self .freq_map [node .freq ].append (node )
400-
401- except Exception as e :
402- # If loading fails, start with empty cache
403- self .key_map .clear ()
404- self .freq_map .clear ()
405- self .min_freq = 0
406-
407- def persist_now (self ) -> None :
408- """Force immediate persistence to disk (useful for shutdown)."""
409- with self ._lock :
410- if self .persistence_interval is not None :
411- self ._persist_to_disk ()
412- self .last_persist_time = time .time ()
413-
414- def supports_persistence (self ) -> bool :
415- """Check if this cache instance supports persistence."""
416- return self .persistence_interval is not None
417-
418293 def _check_and_log_stats (self ) -> None :
419294 """Check if enough time has passed and log stats if needed."""
420295 if not self .track_stats or self .stats_logging_interval is None :
@@ -644,34 +519,3 @@ def capacity(self) -> int:
644519 # Reset statistics
645520 stats_cache .reset_stats ()
646521 print (f"\n After reset: { stats_cache .get_stats ()} " )
647-
648- print ("\n === Cache with Persistence ===" )
649-
650- # Create cache with persistence (5 second interval)
651- persist_cache = LFUCache (
652- capacity = 3 , persistence_interval = 5.0 , persistence_path = "test_cache.json"
653- )
654-
655- # Add some items
656- persist_cache .put ("item1" , "value1" )
657- persist_cache .put ("item2" , "value2" )
658- persist_cache .put ("item3" , "value3" )
659-
660- # Force immediate persistence
661- persist_cache .persist_now ()
662- print ("Cache persisted to disk" )
663-
664- # Create new cache instance that will load from disk
665- new_cache = LFUCache (
666- capacity = 3 , persistence_interval = 5.0 , persistence_path = "test_cache.json"
667- )
668-
669- # Verify data was loaded
670- print (f"Loaded item1: { new_cache .get ('item1' )} " ) # Should return 'value1'
671- print (f"Loaded item2: { new_cache .get ('item2' )} " ) # Should return 'value2'
672- print (f"Cache size after loading: { new_cache .size ()} " ) # Should return 3
673-
674- # Clean up
675- if os .path .exists ("test_cache.json" ):
676- os .remove ("test_cache.json" )
677- print ("Cleaned up test persistence file" )
0 commit comments