Skip to content

Don't require the parent span to have a tracer in traced_from_parent_span() - #788

Merged
wbarnha merged 4 commits into
masterfrom
claude/faust-issue-786-patch-pqf7d8
Aug 20, 2026
Merged

Don't require the parent span to have a tracer in traced_from_parent_span()#788
wbarnha merged 4 commits into
masterfrom
claude/faust-issue-786-patch-pqf7d8

Conversation

@wbarnha

@wbarnha wbarnha commented Aug 19, 2026

Copy link
Copy Markdown
Member

Note: Before submitting this pull request, please review our contributing
guidelines
.

Description

Rebased onto master now that #787 has landed. The scope of this PR has changed substantially — it opened as an alternative fix for #786, and is now just the one piece that #787 did not cover.

#787 landed the same root-cause fix while this was open, so this defers to it: faust/utils/_opentracing.py is taken from master verbatim, and its parity tests (test_opentracing_shim.py) stand as the guard for the stand-in itself. Nothing here re-litigates that.

What's left

traced_from_parent_span() still assumes the parent span it is handed has a tracer:

child = parent.tracer.start_span(...)

Master's fix means the stand-in always supplies one, so the crash reported in #786 is gone either way. But a span is whatever the configured tracer hands back, and parent.tracer is not guaranteed — opentracing.Span documents tracer as a property backed by whatever was passed to the constructor, and nothing stops it being None. Tracing is instrumentation; it shouldn't be what breaks a rebalance. So this runs the wrapped function untraced instead:

tracer = getattr(parent, "tracer", None)
if tracer is None:
    return fun(*args, **kwargs)

getattr covers both branches the old code had — "no parent at all" and "parent with no tracer" — so the if parent is not None test folds into it and the body de-indents. That accounts for most of the diff; the behavioural change is the two added lines.

Tests

Cut down to what test_opentracing_shim.py doesn't already cover, and moved to tests/unit/utils/test_tracing.py to match what they now exercise:

  • A tracerless parent, passed as an argument and reached through the context variable.
  • Error propagation through the traced wrapper, parametrized over traced and untraced parents — the untraced path is new code, and it still has to let the wrapped function's exception through.
  • Both rebalance callbacks with the stand-in substituted for faust.utils.tracing.opentracing. test_opentracing_shim.py notes that it exercises the stand-in's objects directly and so "do[es] not prove the except ImportError fallback wiring itself" — these cover that wiring, and would have caught Kafka rebalancing fails when opentracing is not installed #786 end to end.

Three of the eight fail without the guard; the rebalance tests pass on master already and are here as regression cover.

Verification

  • tests/unit + tests/functional: 2503 passed, 12 skipped.
  • isort / black clean; flake8 output byte-identical to master (4 pre-existing hits, none in touched files).

Happy to close this instead if you'd rather leave traced_from_parent_span() strict — #786 itself is fixed on master either way.

Both rebalance callbacks trace their work from the span
`_start_span_from_rebalancing()` returns, which is a no-op span whenever
no tracer is configured. The no-op stand-in Faust falls back to when the
`faust[opentracing]` extra is missing gave its spans no tracer, while
`traced_from_parent_span()` starts its child span from `parent.tracer`,
so every rebalance raised:

    AttributeError: 'NoneType' object has no attribute 'start_span'

`on_rebalance_start()` has already run by then, and `on_rebalance_end()`
is only reached through recovery -- downstream of the callback that
raised -- so the app was left with `rebalancing` stuck true.

The stand-in's spans now carry the tracer that made them, as the real
library's do, and `Tracer.start_span()` returns that tracer's own no-op
span rather than a fresh tracerless one.

The same drift hid a second divergence: the stand-in defined
`Span.operation_name`, which `AIOKafkaConsumerThread` reads to tell a
real span from a no-op one, catching the `AttributeError` the real
library raises. Defining it sent no-op spans down the real-span path,
where they failed on `_real_finish`. Dropping it restores the sentinel.

Two smaller parity fixes go with those: `Span.__exit__` now calls
`finish()`, which is what the driver's lazy spans rebind and what the
real library calls on exit, and `start_child_span()` goes through the
parent's tracer instead of returning an unrelated span.

Finally, `traced_from_parent_span()` no longer assumes a parent span has
a tracer. A span is whatever the configured tracer hands back, and
tracing is instrumentation: it now runs the wrapped function untraced
rather than breaking its caller.

`Tracer.start_active_span()` is dropped. Faust never calls it, and the
real one returns a `Scope` wrapping the span rather than the span
itself, so the stand-in's version would have mis-served any caller.

Tests run the stand-in side by side with the real library, so a
divergence fails as a parametrization rather than resting on a claim
about what the real library does, and cover both rebalance callbacks
with the stand-in substituted in. Eleven of them fail before this change.

Fixes #786

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sejq9tYzzeqWeZAgA3EB6s
@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.20%. Comparing base (39ded3c) to head (d006fd2).

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #788   +/-   ##
=======================================
  Coverage   96.20%   96.20%           
=======================================
  Files         110      110           
  Lines       11789    11789           
  Branches     1281     1281           
=======================================
  Hits        11342    11342           
  Misses        350      350           
  Partials       97       97           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

#787 landed the same root-cause fix for #786 while this branch was open,
so defer to it: `faust/utils/_opentracing.py` is taken from master
verbatim, and its parity tests (`test_opentracing_shim.py`) stand as the
guard for the stand-in itself.

What master does not have, and this branch keeps:

`traced_from_parent_span()` still assumes the parent span it is given has
a tracer. Master's fix means the stand-in always supplies one, so the
reported crash is gone either way -- but a span is whatever the
configured tracer hands back, and tracing is instrumentation. It now runs
the wrapped function untraced rather than failing its caller.

The tests are cut down to what `test_opentracing_shim.py` does not
already cover, and moved to `test_tracing.py` to match: the tracerless
parent (by argument and through the context variable), error propagation,
and both rebalance callbacks with the stand-in substituted for the real
library -- the wiring the parity tests note they do not reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sejq9tYzzeqWeZAgA3EB6s
@wbarnha wbarnha changed the title Fix Kafka rebalancing when opentracing is not installed Don't require the parent span to have a tracer in traced_from_parent_span() Aug 19, 2026
@wbarnha
wbarnha merged commit dc690de into master Aug 20, 2026
35 of 36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants