Skip to content

Commit 5b3412d

Browse files
GuangTianLiuntitaker
authored andcommitted
🐛 [getsentry#423] fix reference cycle (getsentry#434)
* 🐛 [getsentry#423] fix reference cycle * 🐛 [getsentry#423] fix reference cycle using descriptor * 🐛 [getsentry#423] patch multiple instance * 🐛 [getsentry#423] compat with python2 and pypy * test: Add tests * doc: Add executive summary * 🎨 [getsentry#423] format code with black * 🐛 [getsentry#423] fix reference cycle with an easier way
1 parent a50b651 commit 5b3412d

File tree

2 files changed

+57
-10
lines changed

2 files changed

+57
-10
lines changed

sentry_sdk/integrations/threading.py

+13-10
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
from __future__ import absolute_import
22

33
import sys
4-
5-
from threading import Thread
4+
from threading import Thread, current_thread
65

76
from sentry_sdk import Hub
87
from sentry_sdk._compat import reraise
9-
from sentry_sdk.utils import event_from_exception
10-
from sentry_sdk.integrations import Integration
11-
128
from sentry_sdk._types import MYPY
9+
from sentry_sdk.integrations import Integration
10+
from sentry_sdk.utils import event_from_exception
1311

1412
if MYPY:
1513
from typing import Any
@@ -34,21 +32,26 @@ def sentry_start(self, *a, **kw):
3432
hub_ = None
3533
else:
3634
hub_ = Hub(hub)
37-
38-
self.run = _wrap_run(hub_, self.run)
35+
# Patching instance methods in `start()` creates a reference cycle if
36+
# done in a naive way. See
37+
# https://github.com/getsentry/sentry-python/pull/434
38+
#
39+
# In threading module, using current_thread API will access current thread instance
40+
# without holding it to avoid a reference cycle in an easier way.
41+
self.run = _wrap_run(hub_, self.run.__func__)
3942

4043
return old_start(self, *a, **kw) # type: ignore
4144

4245
Thread.start = sentry_start # type: ignore
4346

4447

45-
def _wrap_run(parent_hub, old_run):
48+
def _wrap_run(parent_hub, old_run_func):
4649
def run(*a, **kw):
4750
hub = parent_hub or Hub.current
48-
4951
with hub:
5052
try:
51-
return old_run(*a, **kw)
53+
self = current_thread()
54+
return old_run_func(self, *a, **kw)
5255
except Exception:
5356
reraise(*_capture_exception())
5457

tests/integrations/threading/test_threading.py

+44
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import gc
2+
13
from threading import Thread
24

35
import pytest
@@ -62,3 +64,45 @@ def stage2():
6264
assert event["tags"]["stage1"] is True
6365
else:
6466
assert "stage1" not in event.get("tags", {})
67+
68+
69+
def test_circular_references(sentry_init, request):
70+
sentry_init(default_integrations=False, integrations=[ThreadingIntegration()])
71+
72+
gc.collect()
73+
gc.disable()
74+
request.addfinalizer(gc.enable)
75+
76+
class MyThread(Thread):
77+
def run(self):
78+
pass
79+
80+
t = MyThread()
81+
t.start()
82+
t.join()
83+
del t
84+
85+
assert not gc.collect()
86+
87+
88+
def test_double_patching(sentry_init, capture_events):
89+
sentry_init(default_integrations=False, integrations=[ThreadingIntegration()])
90+
events = capture_events()
91+
92+
class MyThread(Thread):
93+
def run(self):
94+
1 / 0
95+
96+
ts = []
97+
for _ in range(10):
98+
t = MyThread()
99+
t.start()
100+
ts.append(t)
101+
102+
for t in ts:
103+
t.join()
104+
105+
assert len(events) == 10
106+
for event in events:
107+
exception, = event["exception"]["values"]
108+
assert exception["type"] == "ZeroDivisionError"

0 commit comments

Comments
 (0)