11from cachetools import Cache
22from collections import OrderedDict
3+ from readerwriterlock import rwlock
34
4- class ARC (Cache ):
5- """
6- Adaptive Replacement Cache (ARC) implementation with on_evict callback.
7- Balances recency and frequency via two active lists (T1, T2) and two ghost lists (B1, B2).
8- Calls on_evict([key]) whenever an item is evicted from the active cache.
9- """
5+ _sentinel = object ()
106
11- def __init__ (self , maxsize , getsizeof = None , on_evict = None ):
12- """
13- Args:
14- maxsize (int): Maximum cache size.
15- getsizeof (callable, optional): Sizing function for items.
16- on_evict (callable, optional): Callback called as on_evict([key]) when a key is evicted.
17- """
7+ class ARC (Cache ):
8+ def __init__ (self , maxsize , getsizeof = None ):
189 super ().__init__ (maxsize , getsizeof )
1910 self .t1 = OrderedDict ()
2011 self .t2 = OrderedDict ()
2112 self .b1 = OrderedDict ()
2213 self .b2 = OrderedDict ()
23- self .p = 0 # Adaptive target for T1 size.
24- self .on_evict = on_evict
14+ self .p = 0
15+ self ._rw_lock = rwlock . RWLockWrite ()
2516
2617 def __len__ (self ):
2718 return len (self .t1 ) + len (self .t2 )
@@ -30,96 +21,80 @@ def __contains__(self, key):
3021 return key in self .t1 or key in self .t2
3122
3223 def _evict_internal (self ):
33- """
34- Evicts items from T1 or T2 if cache is over capacity, and prunes ghost lists.
35- Calls on_evict for each evicted key.
36- """
37- # Evict from T1 or T2 if active cache > maxsize
3824 while len (self .t1 ) + len (self .t2 ) > self .maxsize :
3925 if len (self .t1 ) > self .p or (len (self .t1 ) == 0 and len (self .t2 ) > 0 ):
4026 key , value = self .t1 .popitem (last = False )
4127 self .b1 [key ] = value
42- if self .on_evict :
43- self .on_evict ([key ])
4428 else :
4529 key , value = self .t2 .popitem (last = False )
4630 self .b2 [key ] = value
47- if self .on_evict :
48- self .on_evict ([key ])
49- # Prune ghost lists to their max lengths
5031 while len (self .b1 ) > (self .maxsize - self .p ):
5132 self .b1 .popitem (last = False )
5233 while len (self .b2 ) > self .p :
5334 self .b2 .popitem (last = False )
5435
5536 def __setitem__ (self , key , value ):
56- # Remove from all lists before re-inserting
57- for l in (self .t1 , self .t2 , self .b1 , self .b2 ):
58- l .pop (key , None )
59- self .t1 [key ] = value
60- self .t1 .move_to_end (key )
61- self ._evict_internal ()
37+ with self . _rw_lock . gen_wlock ():
38+ for l in (self .t1 , self .t2 , self .b1 , self .b2 ):
39+ l .pop (key , None )
40+ self .t1 [key ] = value
41+ self .t1 .move_to_end (key )
42+ self ._evict_internal ()
6243
6344 def __getitem__ (self , key ):
64- # Case 1: Hit in T1 → promote to T2
65- if key in self .t1 :
66- value = self .t1 .pop (key )
67- self .t2 [key ] = value
68- self .t2 .move_to_end (key )
69- self .p = max (0 , self .p - 1 )
70- self ._evict_internal ()
71- return value
72- # Case 2: Hit in T2 → refresh in T2
73- if key in self .t2 :
74- value = self .t2 .pop (key )
75- self .t2 [key ] = value
76- self .t2 .move_to_end (key )
77- self .p = min (self .maxsize , self .p + 1 )
78- self ._evict_internal ()
79- return value
80- # Case 3: Hit in B1 (ghost) → fetch and promote to T2
81- if key in self .b1 :
82- self .b1 .pop (key )
83- self .p = min (self .maxsize , self .p + 1 )
84- self ._evict_internal ()
85- value = super ().__missing__ (key )
86- self .t2 [key ] = value
87- self .t2 .move_to_end (key )
88- return value
89- # Case 4: Hit in B2 (ghost) → fetch and promote to T2
90- if key in self .b2 :
91- self .b2 .pop (key )
92- self .p = max (0 , self .p - 1 )
93- self ._evict_internal ()
94- value = super ().__missing__ (key )
95- self .t2 [key ] = value
96- self .t2 .move_to_end (key )
97- return value
98- # Case 5: Cold miss → handled by Cache base class (calls __setitem__ after __missing__)
99- return super ().__getitem__ (key )
45+ with self ._rw_lock .gen_wlock ():
46+ if key in self .t1 :
47+ value = self .t1 .pop (key )
48+ self .t2 [key ] = value
49+ self .t2 .move_to_end (key )
50+ self .p = max (0 , self .p - 1 )
51+ self ._evict_internal ()
52+ return value
53+ if key in self .t2 :
54+ value = self .t2 .pop (key )
55+ self .t2 [key ] = value
56+ self .t2 .move_to_end (key )
57+ self .p = min (self .maxsize , self .p + 1 )
58+ self ._evict_internal ()
59+ return value
60+ if key in self .b1 :
61+ self .b1 .pop (key )
62+ self .p = min (self .maxsize , self .p + 1 )
63+ self ._evict_internal ()
64+ value = super ().__missing__ (key )
65+ self .t2 [key ] = value
66+ self .t2 .move_to_end (key )
67+ return value
68+ if key in self .b2 :
69+ self .b2 .pop (key )
70+ self .p = max (0 , self .p - 1 )
71+ self ._evict_internal ()
72+ value = super ().__missing__ (key )
73+ self .t2 [key ] = value
74+ self .t2 .move_to_end (key )
75+ return value
76+ return super ().__getitem__ (key )
10077
10178 def __missing__ (self , key ):
102- """
103- Override this in a subclass, or rely on direct assignment (cache[key] = value).
104- """
10579 raise KeyError (key )
10680
107- def pop (self , key , default = None ):
108- """
109- Remove key from all lists.
110- """
111- for l in ( self . t1 , self . t2 , self . b1 , self . b2 ):
112- if key in l :
113- return l . pop (key )
114- return default
81+ def pop (self , key , default = _sentinel ):
82+ with self . _rw_lock . gen_wlock ():
83+ for l in ( self . t1 , self . t2 , self . b1 , self . b2 ):
84+ if key in l :
85+ return l . pop ( key )
86+ if default is _sentinel :
87+ raise KeyError (key )
88+ return default
11589
11690 def clear (self ):
117- self .t1 .clear ()
118- self .t2 .clear ()
119- self .b1 .clear ()
120- self .b2 .clear ()
121- self .p = 0
122- super ().clear ()
91+ with self ._rw_lock .gen_wlock ():
92+ self .t1 .clear ()
93+ self .t2 .clear ()
94+ self .b1 .clear ()
95+ self .b2 .clear ()
96+ self .p = 0
97+ super ().clear ()
12398
12499 def __iter__ (self ):
125100 yield from self .t1
0 commit comments