fix(streaming): replace sleep gate with _started event for cancel tests#1146
Conversation
test_external_task_cancellation_releases_consumers and test_external_cancellation_acomplete_raise_once both slept 10 ms before calling cancel() on the orchestration task. On a fast runner the mock backend (sleep(0) between tokens) can drain the entire stream within that window, making cancel() a no-op on an already-done task — no CancelledError is ever raised and the test fails. Add StreamChunkingResult._started (asyncio.Event), set at the very top of _orchestrate_streaming before its first suspension point. Tests now await _started before cancelling: after _started fires the orchestration is live but suspended, and cancel() is called without yielding back to the event loop, so the task cannot complete between the two calls. Fixes generative-computing#1132 Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
fa5713d to
767d0b9
Compare
…ne() guard Three improvements from code review: - Rename _started to _orchestration_started for consistency with the existing _orchestration_task and _orchestration_exception fields. - Expand the attribute comment to make explicit that the event is set synchronously before any await, and that the object is single-use. - Add `assert not result._orchestration_task.done()` after the _orchestration_started.wait() in both cancellation tests as a regression guard: if a future change makes the orchestration synchronous to its first yield, the tests would silently regress to vacuous-pass without this assertion. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
planetf1
left a comment
There was a problem hiding this comment.
Clean fix — deterministic event coordination is strictly better than a wall-clock sleep for this class of test. Two minor notes below; neither blocks merge.
The third cancellation test (test_cancelled_task_sets_completed_false, line ~1699) still uses await asyncio.sleep(0) with BlockingBackend. That's correct — a never-draining backend makes the race impossible — but the pattern now diverges silently from the two tests you updated, which could confuse a future reader or get copy-pasted as a template. Consider either migrating it to await asyncio.wait_for(result._orchestration_started.wait(), timeout=2.0) (strictly stronger, also satisfies the coro.send(None) requirement the docstring mentions), or adding one line: # Deliberately uses sleep(0), not _orchestration_started.wait(): BlockingBackend blocks indefinitely so there is no race. Happy to leave this as a follow-up issue rather than hold up this PR.
Add failure messages to the two `assert not done()` regression guards so a future failure is self-explanatory rather than a bare AssertionError. Replace the BlockingBackend test's original comment with one that explicitly notes the deliberate divergence from the _orchestration_started pattern: the backend blocks indefinitely so there is no stream-drain race, making sleep(0) both correct and sufficient. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
aa03481 to
06c65b0
Compare
Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
3a6a068
…ts (generative-computing#1146) * fix(streaming): replace sleep gate with _started event for cancel tests test_external_task_cancellation_releases_consumers and test_external_cancellation_acomplete_raise_once both slept 10 ms before calling cancel() on the orchestration task. On a fast runner the mock backend (sleep(0) between tokens) can drain the entire stream within that window, making cancel() a no-op on an already-done task — no CancelledError is ever raised and the test fails. Add StreamChunkingResult._started (asyncio.Event), set at the very top of _orchestrate_streaming before its first suspension point. Tests now await _started before cancelling: after _started fires the orchestration is live but suspended, and cancel() is called without yielding back to the event loop, so the task cannot complete between the two calls. Fixes generative-computing#1132 Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * refactor(streaming): rename _started → _orchestration_started; add done() guard Three improvements from code review: - Rename _started to _orchestration_started for consistency with the existing _orchestration_task and _orchestration_exception fields. - Expand the attribute comment to make explicit that the event is set synchronously before any await, and that the object is single-use. - Add `assert not result._orchestration_task.done()` after the _orchestration_started.wait() in both cancellation tests as a regression guard: if a future change makes the orchestration synchronous to its first yield, the tests would silently regress to vacuous-pass without this assertion. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> * test(streaming): improve assert messages and clarify sleep(0) test Add failure messages to the two `assert not done()` regression guards so a future failure is self-explanatory rather than a bare AssertionError. Replace the BlockingBackend test's original comment with one that explicitly notes the deliberate divergence from the _orchestration_started pattern: the backend blocks indefinitely so there is no stream-drain race, making sleep(0) both correct and sufficient. Assisted-by: Claude Code Signed-off-by: Nigel Jones <jonesn@uk.ibm.com> --------- Signed-off-by: Nigel Jones <jonesn@uk.ibm.com>
Misc PR
Type of PR
Description
Two streaming cancellation tests gated on
await asyncio.sleep(0.01)before callingcancel(). On a fast runner,StreamingMockBackend(onlysleep(0)between tokens) can drain the whole 200-word stream inside that window —cancel()then no-ops on an already-done task and noCancelledErroris raised.Adds
_orchestration_started(asyncio.Event) toStreamChunkingResult, set synchronously at the top of_orchestrate_streamingbefore anyawait. The two tests now wait on this event instead of sleeping: once it fires the task is suspended at its first real I/O point, socancel()is guaranteed to land on a live task. Anassert not task.done()guard follows the wait to catch any future regression to vacuous-pass behaviour.Testing
Attribution