Skip to content

Commit

Permalink
Cancel timer when transaction already finished
Browse files Browse the repository at this point in the history
  • Loading branch information
antonpirker committed Nov 22, 2024
1 parent 2e69500 commit 30d18fa
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 6 deletions.
14 changes: 10 additions & 4 deletions sentry_sdk/integrations/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def __call__(self, environ, start_response):
)

method = environ.get("REQUEST_METHOD", "").upper()

transaction = None
if method in self.http_methods_to_capture:
transaction = continue_trace(
Expand All @@ -124,6 +125,7 @@ def __call__(self, environ, start_response):
origin=self.span_origin,
)

timer = None
if transaction is not None:
sentry_sdk.start_transaction(
transaction,
Expand All @@ -135,6 +137,7 @@ def __call__(self, environ, start_response):
args=(current_scope, scope),
)
timer.start()

try:
response = self.app(
environ,
Expand All @@ -147,7 +150,7 @@ def __call__(self, environ, start_response):
except BaseException:
exc_info = sys.exc_info()
_capture_exception(exc_info)
finish_running_transaction(current_scope, exc_info)
finish_running_transaction(current_scope, exc_info, timer)
reraise(*exc_info)

finally:
Expand All @@ -157,6 +160,7 @@ def __call__(self, environ, start_response):
response=response,
current_scope=current_scope,
isolation_scope=scope,
timer=timer,
)


Expand Down Expand Up @@ -271,18 +275,20 @@ class _ScopedResponse:
- WSGI servers streaming responses interleaved from the same thread
"""

__slots__ = ("_response", "_current_scope", "_isolation_scope")
__slots__ = ("_response", "_current_scope", "_isolation_scope", "_timer")

def __init__(
self,
response, # type: Iterator[bytes]
current_scope, # type: sentry_sdk.scope.Scope
isolation_scope, # type: sentry_sdk.scope.Scope
timer=None, # type: Optional[Timer]
):
# type: (...) -> None
self._response = response
self._current_scope = current_scope
self._isolation_scope = isolation_scope
self._timer = timer

def __iter__(self):
# type: () -> Iterator[bytes]
Expand All @@ -304,14 +310,14 @@ def __iter__(self):
finally:
with use_isolation_scope(self._isolation_scope):
with use_scope(self._current_scope):
finish_running_transaction()
finish_running_transaction(timer=self._timer)

def close(self):
# type: () -> None
with use_isolation_scope(self._isolation_scope):
with use_scope(self._current_scope):
try:
finish_running_transaction()
finish_running_transaction(timer=self._timer)
self._response.close() # type: ignore
except AttributeError:
pass
Expand Down
8 changes: 6 additions & 2 deletions sentry_sdk/tracing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from types import FrameType

from sentry_sdk._types import ExcInfo
from threading import Timer


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


def finish_running_transaction(scope=None, exc_info=None):
# type: (Optional[sentry_sdk.Scope], Optional[ExcInfo]) -> None
def finish_running_transaction(scope=None, exc_info=None, timer=None):
# type: (Optional[sentry_sdk.Scope], Optional[ExcInfo], Timer) -> None
current_scope = scope or sentry_sdk.get_current_scope()
if current_scope.transaction is not None and hasattr(
current_scope.transaction, "_context_manager_state"
):
if timer is not None:
timer.cancel()

if exc_info is not None:
current_scope.transaction.__exit__(*exc_info)
else:
Expand Down
37 changes: 37 additions & 0 deletions tests/integrations/wsgi/test_wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,3 +537,40 @@ def long_running_app(environ, start_response):
assert (
transaction_duration <= new_max_duration * 1.2
) # we allow 2% margin for processing the request


def test_long_running_transaction_timer_canceled(sentry_init, capture_events):
# we allow transactions to be 0.5 seconds as a maximum
new_max_duration = 0.5

with mock.patch.object(
sentry_sdk.integrations.wsgi,
"MAX_TRANSACTION_DURATION_SECONDS",
new_max_duration,
):
with mock.patch(
"sentry_sdk.integrations.wsgi.finish_long_running_transaction"
) as mock_finish:

def generate_content():
# This response will take 0.3 seconds to generate
for _ in range(3):
time.sleep(0.1)
yield "ok"

def long_running_app(environ, start_response):
start_response("200 OK", [])
return generate_content()

sentry_init(send_default_pii=True, traces_sample_rate=1.0)
app = SentryWsgiMiddleware(long_running_app)

events = capture_events()

client = Client(app)
response = client.get("/")
_ = response.get_data()

(transaction,) = events

mock_finish.assert_not_called()

0 comments on commit 30d18fa

Please sign in to comment.