Skip to content

Commit 331e40b

Browse files
committed
feat(worker): Enable concurrent callbacks on async task worker
Enable concurrent callbacks on async task worker by firing them as a task rather than awaiting them. A done callback handles the necessary queue and exception logic. GH-4581
1 parent 96fcd85 commit 331e40b

File tree

1 file changed

+33
-11
lines changed

1 file changed

+33
-11
lines changed

sentry_sdk/worker.py

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ def __init__(self, queue_size: int = DEFAULT_QUEUE_SIZE) -> None:
181181
# Event loop needs to remain in the same process
182182
self._task_for_pid: Optional[int] = None
183183
self._loop: Optional[asyncio.AbstractEventLoop] = None
184+
# Track active callback tasks so they have a strong reference and can be cancelled on kill
185+
self._active_tasks: set[asyncio.Task] = set()
184186

185187
@property
186188
def is_alive(self) -> bool:
@@ -195,6 +197,12 @@ def kill(self) -> None:
195197
self._task.cancel()
196198
self._task = None
197199
self._task_for_pid = None
200+
# Also cancel any active callback tasks
201+
# Avoid modifying the set while cancelling tasks
202+
tasks_to_cancel = set(self._active_tasks)
203+
for task in tasks_to_cancel:
204+
task.cancel()
205+
self._active_tasks.clear()
198206
self._loop = None
199207

200208
def start(self) -> None:
@@ -256,16 +264,30 @@ def submit(self, callback: Callable[[], Any]) -> bool:
256264
async def _target(self) -> None:
257265
while True:
258266
callback = await self._queue.get()
259-
try:
260-
if inspect.iscoroutinefunction(callback):
261-
# Callback is an async coroutine, need to await it
262-
await callback()
263-
else:
264-
# Callback is a sync function, need to call it
265-
callback()
266-
except Exception:
267-
logger.error("Failed processing job", exc_info=True)
268-
finally:
269-
self._queue.task_done()
267+
# Firing tasks instead of awaiting them allows for concurrent requests
268+
task = asyncio.create_task(self._process_callback(callback))
269+
# Create a strong reference to the task so it can be cancelled on kill
270+
# and does not get garbage collected while running
271+
self._active_tasks.add(task)
272+
task.add_done_callback(self._on_task_complete)
270273
# Yield to let the event loop run other tasks
271274
await asyncio.sleep(0)
275+
276+
async def _process_callback(self, callback: Callable[[], Any]) -> None:
277+
if inspect.iscoroutinefunction(callback):
278+
# Callback is an async coroutine, need to await it
279+
await callback()
280+
else:
281+
# Callback is a sync function, need to call it
282+
callback()
283+
284+
def _on_task_complete(self, task: asyncio.Task[None]) -> None:
285+
try:
286+
task.result()
287+
except Exception:
288+
logger.error("Failed processing job", exc_info=True)
289+
finally:
290+
# Mark the task as done and remove it from the active tasks set
291+
# This happens only after the task has completed
292+
self._queue.task_done()
293+
self._active_tasks.discard(task)

0 commit comments

Comments
 (0)