Skip to content

Commit 50b1919

Browse files
authored
Improve asyncio integration error handling. (#4129)
Instrumenting asyncio projects can be confusing. Here are two improvements: - If users try to init the Sentry SDK outside of an async loop, a warning message will now printed instructing them how to correctly call init() in async envrionments. Including a link to the docs. - During shutdown of Python unfinished async tasks emit an error `Task was destroyed but it is pending!`. This happens if you use Sentry or not. The error message is confusing and led people to believe the Sentry instrumentation caused this problem. This is now remediated by - The tasks is wrapped by Sentry, but we now **set the name of the wrapped task to include the original** and (and a hint that is has been wrapped by Sentry) to show that the original task is failing, not just some Sentry task unknown to the user. - When shutting down a **info message** is printed, informing that there could be `Task was destroyed but it is pending!` but that those are OK and not a problem with the users code or Sentry. Before this PR the users saw this during shutdown: ``` Exception ignored in: <coroutine object patch_asyncio.<locals>._sentry_task_factory.<locals>._coro_creating_hub_and_span at 0x103ae84f0> Traceback (most recent call last): File "/Users/antonpirker/code/sentry-python/sentry_sdk/integrations/asyncio.py", line 46, in _coro_creating_hub_and_span with sentry_sdk.isolation_scope(): File "/Users/antonpirker/.pyenv/versions/3.12.3/lib/python3.12/contextlib.py", line 158, in __exit__ self.gen.throw(value) File "/Users/antonpirker/code/sentry-python/sentry_sdk/scope.py", line 1732, in isolation_scope _current_scope.reset(current_token) ValueError: <Token var=<ContextVar name='current_scope' default=None at 0x102a87f60> at 0x103b1cfc0> was created in a different Context Task was destroyed but it is pending! task: <Task cancelling name='Task-2' coro=<patch_asyncio.<locals>._sentry_task_factory.<locals>._coro_creating_hub_and_span() done, defined at /Users/antonpirker/code/sentry-python/sentry_sdk/integrations/asyncio.py:42> wait_for=<Future pending cb=[Task.task_wakeup()]> cb=[gather.<locals>._done_callback() at /Users/antonpirker/.pyenv/versions/3.12.3/lib/python3.12/asyncio/tasks.py:767]> ``` With this PR the users will see this during shutdown: Note the INFO message on top and also the task name on the bottom. ``` [sentry] INFO: AsyncIO is shutting down. If you see 'Task was destroyed but it is pending!' errors with '_task_with_sentry_span_creation', these are normal during shutdown and not a problem with your code or Sentry. Exception ignored in: <coroutine object patch_asyncio.<locals>._sentry_task_factory.<locals>._task_with_sentry_span_creation at 0x1028fc4f0> Traceback (most recent call last): File "/Users/antonpirker/code/sentry-python/sentry_sdk/integrations/asyncio.py", line 62, in _task_with_sentry_span_creation with sentry_sdk.isolation_scope(): File "/Users/antonpirker/.pyenv/versions/3.12.3/lib/python3.12/contextlib.py", line 158, in __exit__ self.gen.throw(value) File "/Users/antonpirker/code/sentry-python/sentry_sdk/scope.py", line 1732, in isolation_scope _current_scope.reset(current_token) ValueError: <Token var=<ContextVar name='current_scope' default=None at 0x10193ff10> at 0x1029710c0> was created in a different Context Task was destroyed but it is pending! task: <Task cancelling name='long_running_task (Sentry-wrapped)' coro=<patch_asyncio.<locals>._sentry_task_factory.<locals>._task_with_sentry_span_creation() done, defined at /Users/antonpirker/code/sentry-python/sentry_sdk/integrations/asyncio.py:58> wait_for=<Future pending cb=[Task.task_wakeup()]> cb=[gather.<locals>._done_callback() at /Users/antonpirker/.pyenv/versions/3.12.3/lib/python3.12/asyncio/tasks.py:767]> ``` Fixes #2908 Improves #2333
1 parent d4f4130 commit 50b1919

File tree

1 file changed

+53
-16
lines changed

1 file changed

+53
-16
lines changed

Diff for: sentry_sdk/integrations/asyncio.py

+53-16
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import sys
2+
import signal
23

34
import sentry_sdk
45
from sentry_sdk.consts import OP
56
from sentry_sdk.integrations import Integration, DidNotEnable
6-
from sentry_sdk.utils import event_from_exception, reraise
7+
from sentry_sdk.utils import event_from_exception, logger, reraise
78

89
try:
910
import asyncio
1011
from asyncio.tasks import Task
1112
except ImportError:
1213
raise DidNotEnable("asyncio not available")
1314

14-
from typing import TYPE_CHECKING
15+
from typing import cast, TYPE_CHECKING
1516

1617
if TYPE_CHECKING:
1718
from typing import Any
@@ -36,10 +37,26 @@ def patch_asyncio():
3637
loop = asyncio.get_running_loop()
3738
orig_task_factory = loop.get_task_factory()
3839

40+
# Add a shutdown handler to log a helpful message
41+
def shutdown_handler():
42+
# type: () -> None
43+
logger.info(
44+
"AsyncIO is shutting down. If you see 'Task was destroyed but it is pending!' "
45+
"errors with '_task_with_sentry_span_creation', these are normal during shutdown "
46+
"and not a problem with your code or Sentry."
47+
)
48+
49+
try:
50+
loop.add_signal_handler(signal.SIGINT, shutdown_handler)
51+
loop.add_signal_handler(signal.SIGTERM, shutdown_handler)
52+
except (NotImplementedError, AttributeError):
53+
# Signal handlers might not be supported on all platforms
54+
pass
55+
3956
def _sentry_task_factory(loop, coro, **kwargs):
4057
# type: (asyncio.AbstractEventLoop, Coroutine[Any, Any, Any], Any) -> asyncio.Future[Any]
4158

42-
async def _coro_creating_hub_and_span():
59+
async def _task_with_sentry_span_creation():
4360
# type: () -> Any
4461
result = None
4562

@@ -56,27 +73,47 @@ async def _coro_creating_hub_and_span():
5673

5774
return result
5875

76+
task = None
77+
5978
# Trying to use user set task factory (if there is one)
6079
if orig_task_factory:
61-
return orig_task_factory(loop, _coro_creating_hub_and_span(), **kwargs)
62-
63-
# The default task factory in `asyncio` does not have its own function
64-
# but is just a couple of lines in `asyncio.base_events.create_task()`
65-
# Those lines are copied here.
66-
67-
# WARNING:
68-
# If the default behavior of the task creation in asyncio changes,
69-
# this will break!
70-
task = Task(_coro_creating_hub_and_span(), loop=loop, **kwargs)
71-
if task._source_traceback: # type: ignore
72-
del task._source_traceback[-1] # type: ignore
80+
task = orig_task_factory(
81+
loop, _task_with_sentry_span_creation(), **kwargs
82+
)
83+
84+
if task is None:
85+
# The default task factory in `asyncio` does not have its own function
86+
# but is just a couple of lines in `asyncio.base_events.create_task()`
87+
# Those lines are copied here.
88+
89+
# WARNING:
90+
# If the default behavior of the task creation in asyncio changes,
91+
# this will break!
92+
task = Task(_task_with_sentry_span_creation(), loop=loop, **kwargs)
93+
if task._source_traceback: # type: ignore
94+
del task._source_traceback[-1] # type: ignore
95+
96+
# Set the task name to include the original coroutine's name
97+
try:
98+
cast("asyncio.Task[Any]", task).set_name(
99+
f"{get_name(coro)} (Sentry-wrapped)"
100+
)
101+
except AttributeError:
102+
# set_name might not be available in all Python versions
103+
pass
73104

74105
return task
75106

76107
loop.set_task_factory(_sentry_task_factory) # type: ignore
108+
77109
except RuntimeError:
78110
# When there is no running loop, we have nothing to patch.
79-
pass
111+
logger.warning(
112+
"There is no running asyncio loop so there is nothing Sentry can patch. "
113+
"Please make sure you call sentry_sdk.init() within a running "
114+
"asyncio loop for the AsyncioIntegration to work. "
115+
"See https://docs.sentry.io/platforms/python/integrations/asyncio/"
116+
)
80117

81118

82119
def _capture_exception():

0 commit comments

Comments
 (0)