Open asottile-sentry opened 5 hours ago
fwiw the original trace was generated via this diff:
$ diff -u {venvnew,.venv}/lib/python3.12/site-packages/sentry_sdk/_lru_cache.py
--- venvnew/lib/python3.12/site-packages/sentry_sdk/_lru_cache.py 2024-12-04 17:17:44
+++ .venv/lib/python3.12/site-packages/sentry_sdk/_lru_cache.py 2024-12-04 17:16:18
@@ -62,6 +62,7 @@
"""
+from uuid import uuid4
from copy import copy
SENTINEL = object()
@@ -74,8 +75,16 @@
VALUE = 3
+
class LRUCache:
- def __init__(self, max_size):
+ def _log(self, m):
+ with open('lru_cache.log', 'a+') as f:
+ f.write(f'{self._u}: {m}\n')
+
+ def __init__(self, max_size, _log=True):
+ self._u = uuid4()
+ if _log:
+ self._log('__init__')
assert max_size > 0
self.max_size = max_size
@@ -92,13 +101,15 @@
self.hits = self.misses = 0
def __copy__(self):
- cache = LRUCache(self.max_size)
+ cache = LRUCache(self.max_size, _log=False)
+ self._log(f'__copy__ -> {cache._u}')
cache.full = self.full
cache.cache = copy(self.cache)
cache.root = copy(self.root)
return cache
def set(self, key, value):
+ self._log(f'set {key} {value}')
link = self.cache.get(key, SENTINEL)
if link is not SENTINEL:
@@ -129,7 +140,11 @@
self.root[KEY] = self.root[VALUE] = None
- del self.cache[old_key]
+ try:
+ del self.cache[old_key]
+ except KeyError:
+ self._log('crash!!!')
+ raise
self.cache[key] = old_root
@@ -141,6 +156,7 @@
self.full = len(self.cache) >= self.max_size
def get(self, key, default=None):
+ self._log(f'get {key}')
link = self.cache.get(key, SENTINEL)
if link is SENTINEL:
and then running the first 504 tests of shard 7 in the getsentry/sentry testsuite
assuming python3.6+ only you may be able to utilize builtin dict
as the lru type:
_SENTINEL = object()
class LRUCache:
def __init__(self, max_size: int):
if max_size <= 0:
raise AssertionError(f'invalid max_size: {max_size}')
self.max_size = max_size
self._data = {}
self.hits = self.misses = 0
self.full = False
def __copy__(self):
new = LRUCache(max_size=self.max_size)
new.hits = self.hits
new.misses = self.misses
new.full = self.full
new._data = self._data.copy()
return new
def set(self, key, value):
current = self._data.pop(key, _SENTINEL)
if current is not _SENTINEL:
self._data[key] = value
elif self.full:
self._data.pop(next(iter(self._data)))
self._data[key] = value
else:
self._data[key] = value
self.full = len(self._data) >= self.max_size
def get(self, key, default=None):
try:
ret = self._data.pop(key)
except KeyError:
self.misses += 1
ret = default
else:
self.hits += 1
self._data[key] = ret
return ret
def get_all(self):
return list(self._data.items())
(this passes the current set of tests -- though I'd argue that the AssertionError
should actually be ValueError
)
How do you use Sentry?
Sentry Saas (sentry.io)
Version
latest git revision
Steps to Reproduce
unfortunately I haven't completely narrowed down a "minimal" reproduction but I originally found this from reading the implementation and tracing the output -- attached is a script and data which produces a crash due to a particular ordering of copys and sets.
the root cause of the problem is that
__copy__
is incorrect as written -- it shallow copies the internal datastructures which incorrectly pollutes newly created cachesin the ~best case this produces incorrect values poisoning the parent copy:
at worst case it causes crashes (see attached data)
lru_cache.log
cc @cmanallen -- introduced in dd1117d63fd690d502b32c263e9e970b682fa280
Expected Result
should not crash, should not have internal data integrity problems
Actual Result
the script eventually crashes with: