Fix awaiting an event handler from a background task: caught exceptions and finished superseding runs
FA
4h ago+305+305+012345678901234567890123456789-3-3-0123456789
All Submissions:
- Have you followed the guidelines stated in CONTRIBUTING.md file?
- Have you checked to ensure there aren't any other open Pull Requests for the desired changed?
Type of change
- Bug fix (non-breaking change which fixes an issue)
Changes To Core Features:
- Have you added an explanation of what your changes do and why you'd like us to include them?
- Have you written new tests for your core changes, as applicable?
- Have you successfully ran tests with your changes locally?
Problem
Awaiting an event handler from a background task with no state lock held enqueues the event and awaits its future. Two things differed from the inline await (#7839), both reproduced on main:
- A caught exception was still reported.
_resolve_futureset the exception on the future (so the awaiting task received it) and then also ran the backend exception handler, so the user got the error toast even though the task caught the exception. If the task did not catch it, it was reported twice: once for the awaited event and once for the background task. - A finished
supersedes=Truerun was reported as cancelled._cancel_older_chainscancelled the previous invocation's future whenever it was not done. A handler task that has returned resolves its future in a done callback, one loop iteration later. A newer invocation enqueued in that window cancelled the future of a run that had completed and committed its writes. The in-memory state manager hits the window every time: the second background task is waiting for the state lock the first search holds, and once it gets the lock it reachesenqueuewithout suspending, ahead of the first task's done callback. Redis suspends on the way, so the future is already resolved.
Fix
EventFuturegets anawaitedflag, set byexecute_event_inlineon the future it is about to await. When_resolve_futuredelivers an exception to an awaited future, it stops there: no telemetry, no backend exception handler. An exception the caller does not catch fails the background task, whose own failure path reports it once._cancel_older_chainsleaves alone a future whose task is already done and only cancels the events that run chained; the done callback then resolves the future with the run's result (or exception). A run that is still in progress is cancelled as before.
Not changed
- The inline (lock held) await path, and other code that awaits an event future directly (e.g. upload streams): those futures are not marked, so their exceptions are reported as before.
- If the awaiting caller is already gone (future cancelled) when the event fails, the exception is still reported to the backend exception handler.
- Other external cancellations (disconnect, processor stop) are untouched; only supersession checked for a finished run.
- No state manager change was needed: the memory manager only exposed the ordering.
Tests
tests/units/reflex_base/event/processor/test_base_state_processor.pytest_exception_of_handler_awaited_from_background_is_the_callers: caught → backend handler not called; uncaught → called exactly once.test_superseding_handler_awaited_from_background_returns_finished_result(memory and Redis managers): both tasks get their results.
tests/units/reflex_base/event/processor/test_event_processor.pytest_superseding_event_keeps_the_result_of_a_finished_run: deterministic version of the window (enqueue between the handler returning and its done callback); the finished run resolves with its result and its chained event is cancelled.
tests/integration/tests_playwright/test_background_task.pytest_await_failing_handler_from_background_taskin the existing app: no toast when the background task catches the exception.
All four fail on main (except the Redis parametrization, which is the reference behaviour) and pass with the fix.
Run locally:
uv run pytest tests/units/reflex_base/event tests/units/test_state.py tests/units/istate -q— passesPLAYWRIGHT_BROWSERS_PATH=... uv run pytest tests/integration/tests_playwright/test_background_task.py -q— 8 passeduv run ruff check .,uv run ruff format .— cleanuv run pyrighton the changed files — 0 errors
closes #7839
https://claude.ai/code/session_01LwJUCU8RtEco15mQvkxdQb
Generated by Claude Code