Skip to content

Commit fe3f23e

Browse files
committed
Simpler LRU cache implementation.
1 parent c559c8a commit fe3f23e

File tree

1 file changed

+28
-171
lines changed

1 file changed

+28
-171
lines changed

sentry_sdk/_lru_cache.py

Lines changed: 28 additions & 171 deletions
Original file line numberDiff line numberDiff line change
@@ -1,188 +1,45 @@
1-
"""
2-
A fork of Python 3.6's stdlib lru_cache (found in Python's 'cpython/Lib/functools.py')
3-
adapted into a data structure for single threaded uses.
4-
5-
https://github.com/python/cpython/blob/v3.6.12/Lib/functools.py
6-
7-
8-
Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
9-
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation;
10-
11-
All Rights Reserved
12-
13-
14-
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
15-
--------------------------------------------
16-
17-
1. This LICENSE AGREEMENT is between the Python Software Foundation
18-
("PSF"), and the Individual or Organization ("Licensee") accessing and
19-
otherwise using this software ("Python") in source or binary form and
20-
its associated documentation.
21-
22-
2. Subject to the terms and conditions of this License Agreement, PSF hereby
23-
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
24-
analyze, test, perform and/or display publicly, prepare derivative works,
25-
distribute, and otherwise use Python alone or in any derivative version,
26-
provided, however, that PSF's License Agreement and PSF's notice of copyright,
27-
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
28-
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Python Software Foundation;
29-
All Rights Reserved" are retained in Python alone or in any derivative version
30-
prepared by Licensee.
31-
32-
3. In the event Licensee prepares a derivative work that is based on
33-
or incorporates Python or any part thereof, and wants to make
34-
the derivative work available to others as provided herein, then
35-
Licensee hereby agrees to include in any such work a brief summary of
36-
the changes made to Python.
37-
38-
4. PSF is making Python available to Licensee on an "AS IS"
39-
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
40-
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
41-
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
42-
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
43-
INFRINGE ANY THIRD PARTY RIGHTS.
44-
45-
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
46-
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
47-
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
48-
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
49-
50-
6. This License Agreement will automatically terminate upon a material
51-
breach of its terms and conditions.
52-
53-
7. Nothing in this License Agreement shall be deemed to create any
54-
relationship of agency, partnership, or joint venture between PSF and
55-
Licensee. This License Agreement does not grant permission to use PSF
56-
trademarks or trade name in a trademark sense to endorse or promote
57-
products or services of Licensee, or any third party.
58-
59-
8. By copying, installing or otherwise using Python, Licensee
60-
agrees to be bound by the terms and conditions of this License
61-
Agreement.
62-
63-
"""
64-
65-
from typing import cast, Any
66-
67-
SENTINEL = object()
68-
69-
70-
# aliases to the entries in a node
71-
PREV = 0
72-
NEXT = 1
73-
KEY = 2
74-
VALUE = 3
1+
_SENTINEL = object()
752

763

774
class LRUCache:
78-
def __init__(self, max_size):
79-
assert max_size > 0
80-
5+
def __init__(self, max_size: int):
6+
if max_size <= 0:
7+
raise AssertionError(f"invalid max_size: {max_size}")
818
self.max_size = max_size
82-
self.full = False
83-
84-
self.cache = {}
85-
86-
# root of the circularly linked list to keep track of
87-
# the least recently used key
88-
self.root = [] # type: ignore
89-
# the node looks like [PREV, NEXT, KEY, VALUE]
90-
self.root[:] = [self.root, self.root, None, None]
91-
9+
self._data = {}
9210
self.hits = self.misses = 0
11+
self.full = False
9312

9413
def __copy__(self):
95-
# walk around the circle and fill the new root / cache
96-
cache = {}
97-
node_old = root_old = self.root
98-
node_new = root_new = [cast(Any, None)] * 4
99-
while (node_old := node_old[NEXT]) is not root_old:
100-
_, _, key, val = node_old
101-
cache[node_old[KEY]] = node_new[NEXT] = [node_new, None, key, val]
102-
node_new = node_new[NEXT]
103-
104-
# close the circle
105-
node_new[NEXT] = root_new
106-
root_new[PREV] = node_new
107-
108-
lru_cache = LRUCache(self.max_size)
109-
lru_cache.full = self.full
110-
lru_cache.cache = cache
111-
lru_cache.root = root_new
112-
return lru_cache
14+
new = LRUCache(max_size=self.max_size)
15+
new.hits = self.hits
16+
new.misses = self.misses
17+
new.full = self.full
18+
new._data = self._data.copy()
19+
return new
11320

11421
def set(self, key, value):
115-
link = self.cache.get(key, SENTINEL)
116-
117-
if link is not SENTINEL:
118-
# have to move the node to the front of the linked list
119-
link_prev, link_next, _key, _value = link
120-
121-
# first remove the node from the lsnked list
122-
link_prev[NEXT] = link_next
123-
link_next[PREV] = link_prev
124-
125-
# insert the node between the root and the last
126-
last = self.root[PREV]
127-
last[NEXT] = self.root[PREV] = link
128-
link[PREV] = last
129-
link[NEXT] = self.root
130-
131-
# update the value
132-
link[VALUE] = value
133-
22+
current = self._data.pop(key, _SENTINEL)
23+
if current is not _SENTINEL:
24+
self._data[key] = value
13425
elif self.full:
135-
# reuse the root node, so update its key/value
136-
old_root = self.root
137-
old_root[KEY] = key
138-
old_root[VALUE] = value
139-
140-
self.root = old_root[NEXT]
141-
old_key = self.root[KEY]
142-
143-
self.root[KEY] = self.root[VALUE] = None
144-
145-
del self.cache[old_key]
146-
147-
self.cache[key] = old_root
148-
26+
self._data.pop(next(iter(self._data)))
27+
self._data[key] = value
14928
else:
150-
# insert new node after last
151-
last = self.root[PREV]
152-
link = [last, self.root, key, value]
153-
last[NEXT] = self.root[PREV] = self.cache[key] = link
154-
self.full = len(self.cache) >= self.max_size
29+
self._data[key] = value
30+
self.full = len(self._data) >= self.max_size
15531

15632
def get(self, key, default=None):
157-
link = self.cache.get(key, SENTINEL)
158-
159-
if link is SENTINEL:
33+
try:
34+
ret = self._data.pop(key)
35+
except KeyError:
16036
self.misses += 1
161-
return default
162-
163-
# have to move the node to the front of the linked list
164-
link_prev, link_next, _key, _value = link
165-
166-
# first remove the node from the lsnked list
167-
link_prev[NEXT] = link_next
168-
link_next[PREV] = link_prev
169-
170-
# insert the node between the root and the last
171-
last = self.root[PREV]
172-
last[NEXT] = self.root[PREV] = link
173-
link[PREV] = last
174-
link[NEXT] = self.root
175-
176-
self.hits += 1
37+
ret = default
38+
else:
39+
self.hits += 1
40+
self._data[key] = ret
17741

178-
return link[VALUE]
42+
return ret
17943

18044
def get_all(self):
181-
nodes = []
182-
node = self.root[NEXT]
183-
184-
while node is not self.root:
185-
nodes.append((node[KEY], node[VALUE]))
186-
node = node[NEXT]
187-
188-
return nodes
45+
return list(self._data.items())

0 commit comments

Comments
 (0)