Skip to content

Commit 9da7be8

Browse files
committed
ref(asyncio): Refactor loop close patch in asyncio integration
GH-4601
1 parent 6e2c4f6 commit 9da7be8

File tree

2 files changed

+91
-43
lines changed

2 files changed

+91
-43
lines changed

sentry_sdk/integrations/asyncio.py

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,47 @@ def get_name(coro: Any) -> str:
3030
)
3131

3232

33+
def patch_loop_close() -> None:
34+
"""Patch loop.close to flush pending events before shutdown."""
35+
# Atexit shutdown hook happens after the event loop is closed.
36+
# Therefore, it is necessary to patch the loop.close method to ensure
37+
# that pending events are flushed before the interpreter shuts down.
38+
try:
39+
loop = asyncio.get_running_loop()
40+
except RuntimeError:
41+
# No running loop → cannot patch now
42+
return
43+
44+
if getattr(loop, "_sentry_flush_patched", False):
45+
return
46+
47+
async def _flush() -> None:
48+
client = sentry_sdk.get_client()
49+
if not client:
50+
return
51+
52+
try:
53+
if not isinstance(client.transport, AsyncHttpTransport):
54+
return
55+
56+
task = client.close() # type: ignore
57+
if task is not None:
58+
await task
59+
except Exception:
60+
logger.warning("Sentry flush failed during loop shutdown", exc_info=True)
61+
62+
orig_close = loop.close
63+
64+
def _patched_close() -> None:
65+
try:
66+
loop.run_until_complete(_flush())
67+
finally:
68+
orig_close()
69+
70+
loop.close = _patched_close # type: ignore
71+
loop._sentry_flush_patched = True # type: ignore
72+
73+
3374
def patch_asyncio() -> None:
3475
orig_task_factory = None
3576
try:
@@ -125,46 +166,4 @@ class AsyncioIntegration(Integration):
125166
@staticmethod
126167
def setup_once() -> None:
127168
patch_asyncio()
128-
129-
def _patch_loop_close() -> None:
130-
# Atexit shutdown hook happens after the event loop is closed.
131-
# Therefore, it is necessary to patch the loop.close method to ensure
132-
# that pending events are flushed before the interpreter shuts down.
133-
try:
134-
loop = asyncio.get_running_loop()
135-
except RuntimeError:
136-
# No running loop → cannot patch now
137-
return
138-
139-
if getattr(loop, "_sentry_flush_patched", False):
140-
return
141-
142-
async def _flush() -> None:
143-
client = sentry_sdk.get_client()
144-
if not client:
145-
return
146-
try:
147-
148-
if not isinstance(client.transport, AsyncHttpTransport):
149-
return
150-
151-
t = client.close() # type: ignore
152-
if t is not None:
153-
await t
154-
except Exception:
155-
logger.warning(
156-
"Sentry flush failed during loop shutdown", exc_info=True
157-
)
158-
159-
orig_close = loop.close
160-
161-
def _patched_close() -> None:
162-
try:
163-
loop.run_until_complete(_flush())
164-
finally:
165-
orig_close()
166-
167-
loop.close = _patched_close # type: ignore
168-
loop._sentry_flush_patched = True # type: ignore
169-
170-
_patch_loop_close()
169+
patch_loop_close()

tests/integrations/asyncio/test_asyncio.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,3 +377,52 @@ async def test_span_origin(
377377

378378
assert event["contexts"]["trace"]["origin"] == "manual"
379379
assert event["spans"][0]["origin"] == "auto.function.asyncio"
380+
381+
382+
@minimum_python_38
383+
def test_loop_close_patching(sentry_init):
384+
sentry_init(integrations=[AsyncioIntegration()])
385+
386+
loop = asyncio.new_event_loop()
387+
asyncio.set_event_loop(loop)
388+
389+
try:
390+
with patch("asyncio.get_running_loop", return_value=loop):
391+
assert not hasattr(loop, "_sentry_flush_patched")
392+
AsyncioIntegration.setup_once()
393+
assert hasattr(loop, "_sentry_flush_patched")
394+
assert loop._sentry_flush_patched is True
395+
396+
finally:
397+
if not loop.is_closed():
398+
loop.close()
399+
400+
401+
@minimum_python_38
402+
def test_loop_close_flushes_async_transport(sentry_init):
403+
from sentry_sdk.transport import AsyncHttpTransport
404+
from unittest.mock import Mock, AsyncMock
405+
406+
sentry_init(integrations=[AsyncioIntegration()])
407+
408+
loop = asyncio.new_event_loop()
409+
asyncio.set_event_loop(loop)
410+
411+
try:
412+
with patch("asyncio.get_running_loop", return_value=loop):
413+
AsyncioIntegration.setup_once()
414+
415+
mock_client = Mock()
416+
mock_transport = Mock(spec=AsyncHttpTransport)
417+
mock_client.transport = mock_transport
418+
mock_client.close = AsyncMock(return_value=None)
419+
420+
with patch("sentry_sdk.get_client", return_value=mock_client):
421+
loop.close()
422+
423+
mock_client.close.assert_called_once()
424+
mock_client.close.assert_awaited_once()
425+
426+
except Exception:
427+
if not loop.is_closed():
428+
loop.close()

0 commit comments

Comments
 (0)