util-genai: record cancellation as a failure, not a success - #520
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes util-genai’s invocation context-manager finalization so cancellations and other BaseException cases are recorded as failures (via fail()), preventing cancelled operations from being exported indistinguishably from successful ones.
Changes:
- Update
GenAIInvocation.__exit__to treat any non-Noneexc_value: BaseExceptionas failure, aligning behavior with existing stream wrapper guards. - Add new unit tests covering
CancelledError,KeyboardInterrupt,SystemExit,GeneratorExit, and ensuring cancellation is not suppressed. - Add a towncrier changelog fragment describing the bug fix.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| util/opentelemetry-util-genai/src/opentelemetry/util/genai/_invocation.py | Treat BaseException in __exit__ as failure to correctly mark cancellation/interruption as errors. |
| util/opentelemetry-util-genai/tests/test_invocation_base_exception.py | New tests verifying __exit__ failure semantics for BaseException and that cancellation still propagates. |
| util/opentelemetry-util-genai/.changelog/PRNUMBER.fixed | Changelog fragment documenting the cancellation-as-failure fix. |
Suppressed comments (2)
util/opentelemetry-util-genai/tests/test_invocation_base_exception.py:162
test_cancellation_of_a_task_still_cancels_itcan hang for up to 60s (or indefinitely onstarted.wait()) if cancellation doesn’t propagate as expected. Adding a shortasyncio.wait_fortimeout makes the test fail fast instead of stalling the suite.
task = asyncio.ensure_future(work())
await started.wait()
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task
util/opentelemetry-util-genai/tests/test_invocation_base_exception.py:119
- This assertion hardcodes the
CancelledErrormodule path (asyncio.exceptions...), which can change between Python versions. Compute the expectederror.typeusingfq_exception_typeto avoid version-specific failures.
self.assertEqual(
span.attributes["error.type"], "asyncio.exceptions.CancelledError"
)
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ordemri
force-pushed
the
fix/util-genai-cancellation
branch
from
September 1, 2026 08:17
02d832e to
009e789
Compare
Pull request dashboard statusMerged · refreshed 2026-09-04 22:19 UTC Status above doesn't look right?
|
lmolkova
approved these changes
Sep 1, 2026
`GenAIInvocation.__exit__` finalized the invocation through `fail()` only when the in-flight exception derived from `Exception`. `asyncio.CancelledError`, `KeyboardInterrupt`, `SystemExit` and `GeneratorExit` derive from `BaseException` but not from `Exception`, so they took the `stop()` branch: a cancelled operation was exported with `StatusCode.UNSET`, no `error.type`, and a duration metric carrying no error dimension. Guard on `BaseException` instead, matching the guard the stream wrappers in `stream.py` already use (`if exc_val is not None:`). `fail()` already accepts a `BaseException` and routes it through `Error.from_exception`, so no other change is needed; `error.type` resolves via `fq_exception_type` and stays consistent with the exception event. `__exit__` still returns `None`, so nothing is suppressed: suppressing `CancelledError` would break asyncio cancellation for every caller. Tests cover that explicitly, including a real cancelled task that must remain cancelled. Assisted-by: Claude Opus 5
ordemri
force-pushed
the
fix/util-genai-cancellation
branch
from
September 2, 2026 11:23
009e789 to
39fbbab
Compare
lzchen
approved these changes
Sep 4, 2026
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
GenAIInvocation.__exit__calledfail()only when the in-flight exception derived fromException, soasyncio.CancelledErrortook thestop()branch: a cancelled operation was exportedwith
StatusCode.UNSET, noerror.type, and a duration metric with no error dimension —indistinguishable from one that completed. This guards on
BaseExceptioninstead, matching the guardthe stream wrappers in
stream.pyalready use.fail()already accepts aBaseException, so nothing else changed.__exit__still returnsNone— suppressing
CancelledErrorwould break asyncio cancellation for every caller — and two testscover that, one of them cancelling a real task mid-
await.Known gaps
KeyboardInterrupt,SystemExitandGeneratorExit.GeneratorExitis not reachable from any shipped instrumentation today — no
with <invocation>:block in thisrepository contains a
yield— but it is covered by a test. Happy to narrow this; it is one line.except Exception:handler are worse off than unfixed, not equal: on cancellation neither branch runs and the span
is never ended, so nothing is exported at all. Happy to follow up separately.
Type of change
How has this been tested?
New tests in
util/opentelemetry-util-genai/tests/test_invocation_base_exception.py, written beforethe fix and confirmed red against unmodified
main: 6 failed, 342 passed — five withAssertionError: <StatusCode.UNSET: 0> != <StatusCode.ERROR: 2>, one withKeyError: 'error.type'.The file's other tests passed before and after, so it discriminates rather than failing wholesale.
tox -e py310-test-util-genai— 348 passedtox -e py314-test-util-genai— 348 passedtox -e py310-test-instrumentation-genai-agno-oldest— 20 passedtox -e py310-test-instrumentation-genai-agno-latest— 20 passedtox -e py314-test-instrumentation-genai-agno-latest— 20 passedgoogle-genaiandsmolagentsoldest / latest — passedweaverbinary is notinstalled, so no assertion ran. Relying on CI for those.
Checklist