Skip to content

fix(event_bus): prevent GC of fire-and-forget tasks in fire_event() - #6289

Open
isharak7m wants to merge 1 commit into
odysseus-dev:devfrom
isharak7m:fix/event-bus-task-gc
Open

isharak7m wants to merge 1 commit into
odysseus-dev:devfrom
isharak7m:fix/event-bus-task-gc

Conversation

@isharak7m

@isharak7m isharak7m commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Summary

fire_event() in src/event_bus.py creates asyncio tasks via loop.create_task() without storing the returned Task object. asyncio only keeps weak references to such tasks, so Python's GC can collect them before _handle_event completes — silently dropping the event handler with no error or log. This PR adds a _BG_TASKS set (matching the existing pattern in routes/chat_helpers.py:56-69 and src/builtin_mcp.py:93-105) that holds strong references until tasks finish.

Linked Issue

Fixes #6288

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

What Changed

  • Added _BG_TASKS: set[asyncio.Task] = set() to src/event_bus.py
  • Updated fire_event() to store the task in _BG_TASKS and register a done callback to discard it after completion

Why

fire_event() runs from 46 call sites across 16 files, triggering message_sent, session_created, document_created, email_received, memory_added, skill_added, and research_completed events. The returned Task was never stored — asyncio only keeps weak references to it, so the GC can collect and silently discard it before _handle_event completes. The codebase itself documents this hazard in app.py:1054-1057 and already uses the _BG_TASKS pattern in two other locations.

Diff

@@ -16,6 +16,12 @@ from src.constants import AUTH_FILE
 
 logger = logging.getLogger(__name__)
 
+# Strong references to fire-and-forget tasks. asyncio only keeps weak
+# references to tasks created via create_task(), so without this the GC can
+# collect a task mid-execution and the event handler silently never runs.
+# Mirrors _BG_TASKS in routes/chat_helpers.py and src/builtin_mcp.py.
+_BG_TASKS: set[asyncio.Task] = set()
+
 _task_scheduler = None
 
 
@@ -37,7 +43,9 @@ def fire_event(event_name: str, owner: Optional[str] = None):
     """
     try:
         loop = asyncio.get_running_loop()
-        loop.create_task(_handle_event(event_name, owner))
+        task = loop.create_task(_handle_event(event_name, owner))
+        _BG_TASKS.add(task)
+        task.add_done_callback(_BG_TASKS.discard)
     except RuntimeError:
         # No running loop — run in a new one (shouldn't happen in FastAPI)
         asyncio.run(_handle_event(event_name, owner))

How to Test

  1. Run python -m pytest tests/test_event_bus_task_gc.py -v --noconftest — 4 tests should pass
  2. Verify fire_event() now stores the task in _BG_TASKS and removes it after completion
  3. Verify forced gc.collect() during handler execution does not lose the task
tests/test_event_bus_task_gc.py::TestEventBusTaskGC::test_task_survives_gc           PASSED
tests/test_event_bus_task_gc.py::TestEventBusTaskGC::test_task_removed_after_done     PASSED
tests/test_event_bus_task_gc.py::TestEventBusTaskGC::test_concurrent_events_tracked   PASSED
tests/test_event_bus_task_gc.py::TestEventBusTaskGC::test_no_loop_fallback            PASSED
======================== 4 passed in 0.60s ========================

Checklist

  • I searched open issues and open PRs — this is not a duplicate.
  • This PR targets dev
  • My changes are limited to the scope described above — no unrelated refactors or whitespace changes mixed in.
  • I actually ran the app and verified the change works end-to-end. Type-checks and unit tests are not enough.

fire_event() called loop.create_task() without storing the returned Task.
asyncio only keeps weak references to such tasks, so Python's GC can
collect and silently discard them before _handle_event completes.

Added a module-level _BG_TASKS set (matching the existing pattern in
routes/chat_helpers.py and src/builtin_mcp.py) that holds strong
references to tasks until they finish.

Added regression tests verifying the task is tracked during execution,
removed after completion, and survives forced garbage collection.
@github-actions github-actions Bot added needs work PR description incomplete — please update before review ready for review Description complete — ready for maintainer review and removed needs work PR description incomplete — please update before review labels Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for review Description complete — ready for maintainer review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fire_event() tasks can be GC'd before execution — no strong reference held

1 participant