25
26
class _LRUNode(object):
26
27
"""This maintains the linked-list which is the lru internals."""
28
__slots__ = ('prev', 'next', 'key', 'value', 'cleanup', 'size')
29
__slots__ = ('prev', 'next_key', 'key', 'value', 'cleanup', 'size')
30
31
def __init__(self, key, value, cleanup=None):
33
self.next_key = _null_key
35
36
self.cleanup = cleanup
41
42
def __repr__(self):
46
43
if self.prev is None:
46
prev_key = self.prev.key
50
47
return '%s(%r n:%r p:%r)' % (self.__class__.__name__, self.key,
48
self.next_key, prev_key)
53
50
def run_cleanup(self):
54
if self.cleanup is not None:
55
self.cleanup(self.key, self.value)
57
# Just make sure to break any refcycles, etc
52
if self.cleanup is not None:
53
self.cleanup(self.key, self.value)
55
# cleanup might raise an exception, but we want to make sure
56
# to break refcycles, etc
61
61
class LRUCache(object):
73
73
# The "HEAD" of the lru linked list
74
74
self._most_recently_used = None
75
75
# The "TAIL" of the lru linked list
76
self._last_recently_used = None
76
self._least_recently_used = None
77
77
self._update_max_cache(max_cache, after_cleanup_count)
79
79
def __contains__(self, key):
80
80
return key in self._cache
82
82
def __getitem__(self, key):
83
node = self._cache[key]
84
85
# Inlined from _record_access to decrease the overhead of __getitem__
85
86
# We also have more knowledge about structure if __getitem__ is
86
87
# succeeding, then we know that self._most_recently_used must not be
90
91
# Nothing to do, this node is already at the head of the queue
92
elif node is self._last_recently_used:
93
self._last_recently_used = node.prev
94
93
# Remove this node from the old location
95
94
node_prev = node.prev
97
if node_prev is not None:
98
node_prev.next = node_next
99
if node_next is not None:
95
next_key = node.next_key
96
# benchmarking shows that the lookup of _null_key in globals is faster
97
# than the attribute lookup for (node is self._least_recently_used)
98
if next_key is _null_key:
99
# 'node' is the _least_recently_used, because it doesn't have a
100
# 'next' item. So move the current lru to the previous node.
101
self._least_recently_used = node_prev
103
node_next = cache[next_key]
100
104
node_next.prev = node_prev
101
# Insert this node to the front of the list
105
node_prev.next_key = next_key
106
# Insert this node at the front of the list
107
node.next_key = mru.key
104
109
self._most_recently_used = node
117
122
' supposed to have a previous entry'
119
124
while node is not None:
120
if node.next is None:
121
if node is not self._last_recently_used:
125
if node.next_key is _null_key:
126
if node is not self._least_recently_used:
122
127
raise AssertionError('only the last node should have'
123
128
' no next value: %s' % (node,))
125
if node.next.prev is not node:
131
node_next = self._cache[node.next_key]
132
if node_next.prev is not node:
126
133
raise AssertionError('inconsistency found, node.next.prev'
127
134
' != node: %s' % (node,))
128
135
if node.prev is None:
131
138
' not have a previous node: %s'
134
if node.prev.next is not node:
141
if node.prev.next_key != node.key:
135
142
raise AssertionError('inconsistency found, node.prev.next'
136
143
' != node: %s' % (node,))
140
147
def add(self, key, value, cleanup=None):
141
148
"""Add a new value to the cache.
148
155
:param cleanup: None or a function taking (key, value) to indicate
149
156
'value' should be cleaned up.
159
raise ValueError('cannot use _null_key as a key')
151
160
if key in self._cache:
152
161
node = self._cache[key]
155
node.cleanup = cleanup
165
# Maintain the LRU properties, even if cleanup raises an
168
node.cleanup = cleanup
169
self._record_access(node)
157
171
node = _LRUNode(key, value, cleanup=cleanup)
158
172
self._cache[key] = node
159
self._record_access(node)
173
self._record_access(node)
161
175
if len(self._cache) > self._max_cache:
162
176
# Trigger the cleanup
207
221
# Move 'node' to the front of the queue
208
222
if self._most_recently_used is None:
209
223
self._most_recently_used = node
210
self._last_recently_used = node
224
self._least_recently_used = node
212
226
elif node is self._most_recently_used:
213
227
# Nothing to do, this node is already at the head of the queue
215
elif node is self._last_recently_used:
216
self._last_recently_used = node.prev
217
229
# We've taken care of the tail pointer, remove the node, and insert it
232
if node is self._least_recently_used:
233
self._least_recently_used = node.prev
220
234
if node.prev is not None:
221
node.prev.next = node.next
222
if node.next is not None:
223
node.next.prev = node.prev
235
node.prev.next_key = node.next_key
236
if node.next_key is not _null_key:
237
node_next = self._cache[node.next_key]
238
node_next.prev = node.prev
225
node.next = self._most_recently_used
240
node.next_key = self._most_recently_used.key
241
self._most_recently_used.prev = node
226
242
self._most_recently_used = node
227
node.next.prev = node
230
245
def _remove_node(self, node):
231
if node is self._last_recently_used:
232
self._last_recently_used = node.prev
246
if node is self._least_recently_used:
247
self._least_recently_used = node.prev
233
248
self._cache.pop(node.key)
234
249
# If we have removed all entries, remove the head pointer as well
235
if self._last_recently_used is None:
250
if self._least_recently_used is None:
236
251
self._most_recently_used = None
238
# Now remove this node from the linked list
239
if node.prev is not None:
240
node.prev.next = node.next
241
if node.next is not None:
242
node.next.prev = node.prev
243
# And remove this node's pointers
255
# cleanup might raise an exception, but we want to make sure to
256
# maintain the linked list
257
if node.prev is not None:
258
node.prev.next_key = node.next_key
259
if node.next_key is not _null_key:
260
node_next = self._cache[node.next_key]
261
node_next.prev = node.prev
262
# And remove this node's pointers
264
node.next_key = _null_key
247
266
def _remove_lru(self):
248
267
"""Remove one entry from the lru, and handle consequences.