When an Async Stream Finishes but the Run Failed: Preserving Exceptions at the Boundary
When an Async Stream Finishes but the Run Failed: Preserving Exceptions at the Boundary
The failure symptom: stream_events completes without exposing the run-loop exception
Completion of an asynchronous event stream is not, by itself, evidence that the underlying work succeeded. In the failure case covered here, the model's response method raises RuntimeError before yielding stream events. At the verified revision, consuming stream_events raises RuntimeError with the message run loop boom.
That failure must remain visible to the caller. Otherwise, code waiting for the stream boundary can mistake completion for successful work and begin a dependent action without first inspecting the run-loop outcome.
The exception boundary: defining run_loop_exception and its None states
The patch adds a run_loop_exception property. When the background run-loop task has finished and was not cancelled, the property returns the exception raised by that task.
The property returns None in three states: when the run loop completed without error, when it has not finished, or when it was cancelled. These states distinguish an observed run-loop failure from an incomplete or cancelled task and from a clean completion.
The important boundary is therefore explicit: callers can inspect the settled run-loop exception rather than treating stream completion as the complete result of the operation.
The race condition: re-checking after the run loop settles
At the verified revision, the patch also re-checks for errors after the run loop has fully settled. This handles the observed case where an exception races past the stream sentinel: the stream boundary may be reached before the background task's final exception state is available, but the stored-exception path can surface the failure once settlement is complete.
This preserves the causal relationship between the run-loop task and the stream consumer. The consumer does not need to infer success from the sentinel; it can observe the exception recorded by the settled task.
Failure timeline: a regression model raises RuntimeError before yielding events
The fixed regression case uses a regression model whose response methods raise RuntimeError before yielding stream events. The failure timeline is:
The run loop invokes the relevant model response method.
The response method raises
RuntimeErrorwith the messagerun loop boombefore producing stream events.The run-loop task settles with that exception.
The stream consumer observes the failure when it consumes
stream_events.After the failed stream is consumed,
result.run_loop_exceptionis non-null and contains the sameRuntimeError.
The stored exception is not a replacement for the stream failure. It is the explicit inspection boundary that remains available after the background task has settled.
Regression shape: asserting stream failure and the stored run_loop_exception
The regression must cover both observable sides of the boundary. First, consuming stream_events in the failure case must raise RuntimeError with the message run loop boom. Second, after the failed stream has been consumed, result.run_loop_exception must be non-null and contain the same RuntimeError.
Keeping these assertions together protects the causal chain: the stream reports the failure, and the result retains the settled exception for inspection. A test that checks only stream consumption would not verify the stored-exception boundary; a test that checks only the property would not verify the consumer-facing failure.
Success timeline: confirming a clean stream leaves run_loop_exception as None
The success regression case asserts that run_loop_exception is None after stream_events completes without error. This is the complementary invariant to the failure case: a cleanly completed stream does not populate the stored exception.
Together, the failure and success cases distinguish an exception-bearing completion from a clean completion. They also preserve the documented None behavior for a run loop that completed without error.
Verification: the 11-test cancellation-streaming suite on CPython 3.13.13
At the verified upstream revision, the focused tests/test_cancel_streaming.py suite collected 11 tests, and all 11 passed under CPython 3.13.13.
This is bounded verification of the cancellation-streaming test suite at that revision. It does not establish behavior beyond the verified revision or make a universal claim about asynchronous streaming protocols.
Operational takeaway: inspect the settled exception before starting dependent work
For this API, treat completion of an asynchronous event stream as insufficient evidence of successful work. The run-loop exception must remain observable after stream completion, and callers should retain an explicit exception-inspection boundary before initiating dependent work.
The focused regression shape is equally important: use a response method that raises RuntimeError before yielding events, assert the failure observed while consuming stream_events, assert that run_loop_exception retains the same exception, and separately assert that a clean stream leaves the property as None.
Comments (0)
No comments yet.
Add a comment
Comments are published after moderation. Your e-mail address stays private.