Skip to content

Commit 30d18fa

Browse files
committed
Cancel timer when transaction already finished
1 parent 2e69500 commit 30d18fa

File tree

3 files changed

+53
-6
lines changed

3 files changed

+53
-6
lines changed

sentry_sdk/integrations/wsgi.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ def __call__(self, environ, start_response):
114114
)
115115

116116
method = environ.get("REQUEST_METHOD", "").upper()
117+
117118
transaction = None
118119
if method in self.http_methods_to_capture:
119120
transaction = continue_trace(
@@ -124,6 +125,7 @@ def __call__(self, environ, start_response):
124125
origin=self.span_origin,
125126
)
126127

128+
timer = None
127129
if transaction is not None:
128130
sentry_sdk.start_transaction(
129131
transaction,
@@ -135,6 +137,7 @@ def __call__(self, environ, start_response):
135137
args=(current_scope, scope),
136138
)
137139
timer.start()
140+
138141
try:
139142
response = self.app(
140143
environ,
@@ -147,7 +150,7 @@ def __call__(self, environ, start_response):
147150
except BaseException:
148151
exc_info = sys.exc_info()
149152
_capture_exception(exc_info)
150-
finish_running_transaction(current_scope, exc_info)
153+
finish_running_transaction(current_scope, exc_info, timer)
151154
reraise(*exc_info)
152155

153156
finally:
@@ -157,6 +160,7 @@ def __call__(self, environ, start_response):
157160
response=response,
158161
current_scope=current_scope,
159162
isolation_scope=scope,
163+
timer=timer,
160164
)
161165

162166

@@ -271,18 +275,20 @@ class _ScopedResponse:
271275
- WSGI servers streaming responses interleaved from the same thread
272276
"""
273277

274-
__slots__ = ("_response", "_current_scope", "_isolation_scope")
278+
__slots__ = ("_response", "_current_scope", "_isolation_scope", "_timer")
275279

276280
def __init__(
277281
self,
278282
response, # type: Iterator[bytes]
279283
current_scope, # type: sentry_sdk.scope.Scope
280284
isolation_scope, # type: sentry_sdk.scope.Scope
285+
timer=None, # type: Optional[Timer]
281286
):
282287
# type: (...) -> None
283288
self._response = response
284289
self._current_scope = current_scope
285290
self._isolation_scope = isolation_scope
291+
self._timer = timer
286292

287293
def __iter__(self):
288294
# type: () -> Iterator[bytes]
@@ -304,14 +310,14 @@ def __iter__(self):
304310
finally:
305311
with use_isolation_scope(self._isolation_scope):
306312
with use_scope(self._current_scope):
307-
finish_running_transaction()
313+
finish_running_transaction(timer=self._timer)
308314

309315
def close(self):
310316
# type: () -> None
311317
with use_isolation_scope(self._isolation_scope):
312318
with use_scope(self._current_scope):
313319
try:
314-
finish_running_transaction()
320+
finish_running_transaction(timer=self._timer)
315321
self._response.close() # type: ignore
316322
except AttributeError:
317323
pass

sentry_sdk/tracing_utils.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from types import FrameType
3838

3939
from sentry_sdk._types import ExcInfo
40+
from threading import Timer
4041

4142

4243
SENTRY_TRACE_REGEX = re.compile(
@@ -743,12 +744,15 @@ def get_current_span(scope=None):
743744
from sentry_sdk.tracing import Span
744745

745746

746-
def finish_running_transaction(scope=None, exc_info=None):
747-
# type: (Optional[sentry_sdk.Scope], Optional[ExcInfo]) -> None
747+
def finish_running_transaction(scope=None, exc_info=None, timer=None):
748+
# type: (Optional[sentry_sdk.Scope], Optional[ExcInfo], Timer) -> None
748749
current_scope = scope or sentry_sdk.get_current_scope()
749750
if current_scope.transaction is not None and hasattr(
750751
current_scope.transaction, "_context_manager_state"
751752
):
753+
if timer is not None:
754+
timer.cancel()
755+
752756
if exc_info is not None:
753757
current_scope.transaction.__exit__(*exc_info)
754758
else:

tests/integrations/wsgi/test_wsgi.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,3 +537,40 @@ def long_running_app(environ, start_response):
537537
assert (
538538
transaction_duration <= new_max_duration * 1.2
539539
) # we allow 2% margin for processing the request
540+
541+
542+
def test_long_running_transaction_timer_canceled(sentry_init, capture_events):
543+
# we allow transactions to be 0.5 seconds as a maximum
544+
new_max_duration = 0.5
545+
546+
with mock.patch.object(
547+
sentry_sdk.integrations.wsgi,
548+
"MAX_TRANSACTION_DURATION_SECONDS",
549+
new_max_duration,
550+
):
551+
with mock.patch(
552+
"sentry_sdk.integrations.wsgi.finish_long_running_transaction"
553+
) as mock_finish:
554+
555+
def generate_content():
556+
# This response will take 0.3 seconds to generate
557+
for _ in range(3):
558+
time.sleep(0.1)
559+
yield "ok"
560+
561+
def long_running_app(environ, start_response):
562+
start_response("200 OK", [])
563+
return generate_content()
564+
565+
sentry_init(send_default_pii=True, traces_sample_rate=1.0)
566+
app = SentryWsgiMiddleware(long_running_app)
567+
568+
events = capture_events()
569+
570+
client = Client(app)
571+
response = client.get("/")
572+
_ = response.get_data()
573+
574+
(transaction,) = events
575+
576+
mock_finish.assert_not_called()

0 commit comments

Comments
 (0)