fix(events): track the sync-handler future when async handlers also exist - #6746
fix(events): track the sync-handler future when async handlers also exist#6746LHMQ878 wants to merge 2 commits into
Conversation
…xist emit() registered the sync-handler future with _track_future only inside the sync-only return, so when an event type had both sync and async handlers the sync future was created, submitted, and never tracked. flush() waits on _pending_futures, so it returned True with the sync handlers still running. replay() already tracks unconditionally and only returns the sync future in the sync-only case; emit() now matches. The documented return value is unchanged. The triggering combination is one crewAI sets up itself: the trace listener registers a sync handler on LLMCallCompletedEvent, and the checkpointing docs show users registering an async handler on the same event.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe event bus now tracks synchronous handler futures for mixed sync/async events, allowing ChangesEvent flush tracking
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
lib/crewai/tests/utilities/events/test_flush.py (1)
24-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPrefer the observable
flush()contract over private future-set inspection.
_pending_futuresand_futures_lockare implementation details. Fold this coverage into the blocked-gateflush()regression instead of asserting the internal set delta.As per coding guidelines, unit tests for new functionality must focus on behavior rather than implementation details.
Also applies to: 85-112
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/crewai/tests/utilities/events/test_flush.py` around lines 24 - 26, Replace the private `_tracked_futures()` inspection and related `_pending_futures`/`_futures_lock` assertions with coverage through the public `crewai_event_bus.flush()` contract. Fold this setup and verification into the blocked-gate flush regression, asserting observable completion or blocking behavior rather than internal future-set changes.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/crewai/tests/utilities/events/test_flush.py`:
- Around line 115-141: Strengthen
test_emit_returns_the_sync_future_only_when_there_are_no_async_handlers by
gating the sync_handler for the mixed event, asserting mixed_future resolves
before releasing the gate, then releasing the sync handler and calling flush()
for cleanup. Keep the existing sync-only assertions and ensure the test proves
emit returns the async future rather than the current mixed sync future.
- Around line 47-57: Update lib/crewai/tests/utilities/events/test_flush.py
lines 47-57 to add an async-completion event, wait for the async handler in the
mixed case, assert flush() returns False while gate remains closed, then release
gate and verify a subsequent flush succeeds; update lines 74-82 to assert
flush() returns False before releasing gate. Keep the tests focused on
observable flush behavior.
---
Nitpick comments:
In `@lib/crewai/tests/utilities/events/test_flush.py`:
- Around line 24-26: Replace the private `_tracked_futures()` inspection and
related `_pending_futures`/`_futures_lock` assertions with coverage through the
public `crewai_event_bus.flush()` contract. Fold this setup and verification
into the blocked-gate flush regression, asserting observable completion or
blocking behavior rather than internal future-set changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 90f5c2bc-91c9-4509-a402-4c629435106a
📒 Files selected for processing (2)
lib/crewai/src/crewai/events/event_bus.pylib/crewai/tests/utilities/events/test_flush.py
@coderabbitai was right on both counts, and the first is a real hole in the test rather than a style point. The mixed-case test opened the gate in `finally` and then asserted the completion marker. On the unfixed code `flush` returns early, `finally` releases the handler, and the handler can append its marker before the assertion runs -- so the test could pass against the bug it exists to catch. It happens not to on this machine (15/15 runs caught it), but the ordering is a scheduling accident, not something the test enforces. `flush` already gives a directly observable answer: `False` on timeout, `True` when every handler finished. So each test now asserts `flush(timeout=1.0) is False` with the handler *still gated*, then releases it and asserts `flush(...) is True`. A `flush` that ignored an untracked handler fails the first assertion, and there is no ordering in which it can pass. The mixed case waits for the async handler to finish first, so the timeout cannot be won by the async half still scheduling. That also removes the reason to read `_pending_futures` and `_futures_lock`: the delta-on-the-internal-set test is gone, since the blocked-gate assertion covers what it was there for through the public contract, per guideline 3 in AGENTS.md. On the second point, the mixed-case return value no longer leans on `is not sync_only_future` -- which would not reject an implementation returning the current mixed *sync* future. The sync handler is gated, so a future that resolves is necessarily the async one. Added `test_flush_waits_for_a_gated_async_handler_too`, so the fix cannot trade the sync gap for an async one. Verified: 4 passed against the fix (10/10 repeated runs), and reverting only `event_bus.py` to its parent fails `test_flush_blocks_on_a_sync_handler_alongside_async_handlers` at the `flush(timeout=1.0) is False` assertion, 15/15 runs. 21 passed across test_crewai_event_bus.py, test_shutdown.py and test_rw_lock.py. Signed-off-by: LHMQ878 <LHMQ878@users.noreply.github.com>
Description
emitregistered the sync-handler future with_track_futureonly when the event type had no async handlers.flushwaits on the tracked set, so for a mixed event type it returnedTruewith the sync handlers still running.Fixes #6745
Root cause
event_bus.py:628-645onmain:Tracking was folded into the
return, so it happened only on the path that returns the sync future. When async handlers also exist, control falls through to the async branch and the sync future is dropped — created, running, and unreachable from_pending_futures.replay(:714-721) already does the right thing: it calls_track_future(sync_future)unconditionally and only returns it in the sync-only case. This PR makesemitmatch.Measured on
main:flushreturned in 0.00s and the sync handler finished 1.5s afterwards. (tracked=1is the async future, whose done-callback had not yet fired; the sync one was never added.)Impact
flushexists so callers know handlers finished.crew.py:1950-1953says so:Other call sites:
conversational_mixin.py:1223,flow/runtime/__init__.py:1351and:2498, andshutdown(wait=True)(:905), which isatexit-registered — so at interpreter exit the executor could be shut down with sync handlers still in flight.The triggering combination is the documented one:
listeners/tracing/trace_listener.py:400registers a sync handler onLLMCallCompletedEvent, anddocs/edge/en/concepts/checkpointing.mdx:274shows users registering an async handler on that same event.The change
The documented return-value contract is unchanged —
emit's docstring (:585-591) promises theThreadPoolExecutorfuture for sync-only handlers and the asyncio future for async or mixed. Only tracking moves; the sync future is still returned exactly when it was before.Tests
lib/crewai/tests/utilities/events/test_flush.py, new. No existing test emitted an event that had both a sync and an async handler and then asserted onflush, which is why the gap survived.test_flush_waits_for_sync_handlers_alongside_async_handlersflushtest_flush_waits_for_sync_handlers_when_there_are_no_async_handlerstest_emit_tracks_the_sync_future_when_async_handlers_exist_pending_futures, not just the async onetest_emit_returns_the_sync_future_only_when_there_are_no_async_handlersTwo notes for review:
threading.Eventgate that is released only afterflushis already blocking. A pass therefore cannot come from the handler having finished early...._tracks_the_sync_future...compares the tracked set as a delta and keeps both handlers blocked while it reads it._pending_futuresbelongs to the process-wide singleton bus and_track_future's done-callback discards completed futures, so neither an absolute count nor a read after completion would be meaningful.Control experiment
Reverting only
event_bus.pyand keeping the new tests:..._sync_handlers_alongside_async_handlers..._tracks_the_sync_future_when_async_handlers_exist..._when_there_are_no_async_handlers..._returns_the_sync_future_only_when...Verification
tests/utilities/events/test_flush.pytests/utilities/events/+tests/events/main(9 pre-existing VCR-cassette errors intest_event_ordering.py, unchanged)ruff check/ruff format --checkOut of scope
aemitruns only async handlers, and its docstring says so explicitly, so a mixed event type emitted throughaemitskips its sync handlers by design. It has no call sites insrc/. Left alone here.