Skip to content

[Event Hubs] Correct the premature receiver-attach log and document the deferred attach - #5106

Draft
Johnathan W (j7nw4r) wants to merge 2 commits into
mainfrom
j7nw4r/fix-eventhubs-receiver-attach
Draft

[Event Hubs] Correct the premature receiver-attach log and document the deferred attach#5106
Johnathan W (j7nw4r) wants to merge 2 commits into
mainfrom
j7nw4r/fix-eventhubs-receiver-attach

Conversation

@j7nw4r

@j7nw4r Johnathan W (j7nw4r) commented Aug 20, 2026

Copy link
Copy Markdown
Member

Summary

ConsumerClient::open_receiver_on_partition logged Receiver attached on partition. before it opened any AMQP link, which misled anyone reading a trace of a failed connection. This change corrects that log line and documents the deferred attach on the three public surfaces that depend on it. The eager-attach part of the issue is deliberately not implemented and needs a maintainer decision. See "Open decision" below.

Motivation

open_receiver_on_partition performs no network I/O. It builds an EventReceiver and returns it, and its only fallible step is a URL parse. The AMQP link attaches on the first poll of stream_events(), which is where the broker reports an unknown consumer group or an unknown partition id. The old log line claimed the attach had already happened, and the doc comment claimed the call "establishes a connection", so a caller who checked the result believed the receiver was connected when it was not. A truthful attach record already exists at the real attach site in RecoverableConnection::ensure_receiver, so the premature line was a duplicate rather than the only record.

Changes

  • Reworded the premature info! in open_receiver_on_partition to state that the receiver was created and that the AMQP link attaches on the first stream_events() poll.
  • Left the truthful info!("Attached receiver on partition.") in RecoverableConnection::ensure_receiver untouched, so a real attach is still recorded once per attach and not once per delivery.
  • Rewrote the open_receiver_on_partition doc comment, which said the call "establishes a connection" and twice named MessageReceiver, a type that does not exist.
  • Documented the deferred attach on EventProcessor::run and on PartitionClient::stream_events.
  • Replaced a comment in add_partition_client that stopped in mid sentence.
  • Added a CHANGELOG entry under 0.15.0.

No public API changes. The diff touches doc text, one log message, one comment, the CHANGELOG, and tests.

Open decision

Issue items "attach the receiver inside open_receiver_on_partition" and "report an invalid consumer group from EventProcessor::run()" are not implemented here. The second cannot be met without an eager attach somewhere, and each placement changes public behavior, so the choice belongs to a maintainer.

  • Attach eagerly inside open_receiver_on_partition. This reports the error where the caller asks for the receiver, but it adds a network round trip to every call and can break a caller that opens many receivers up front.
  • Attach eagerly in the processor only, and leave the public method lazy. The Go SDK does this: ConsumerClient.NewPartitionClient stays lazy, and Processor.openPartitionClientImpl forces the link with the comment "if we're stealing we want to stake a claim now, rather than later when the user actually calls ReceiveEvents()". It also stakes the epoch claim at dispatch time, which changes load-balancing timing between processor instances.
  • Leave the attach lazy and document it, which is what this change does.

Two facts to weigh, both unverified here because live tests were out of scope for this change. The broker condition for an unknown consumer group is inferred to be amqp:not-found with the description "The messaging entity ... could not be found", and it is not established whether the CBS put-token call in authorize_path rejects an unknown consumer group before the link attach does. If it does, the second item is reachable with no link attach at all, which would be the better fix. AmqpErrorCondition::NotFound already exists in azure_core_amqp, so neither option needs new plumbing there.

For reference, the .NET processor validates by opening a consumer on a random partition and doing a 5 ms probe read inside StartProcessingAsync, not by a cheap metadata call, and the .NET EventHubConsumerClient is lazy in the same way this client is.

Test plan

Three offline tests in src/consumer/mod.rs. None needs a namespace, a credential, or a recording.

  • open_receiver_on_partition_logs_a_deferred_attach captures tracing output and asserts that the new message is present and that Receiver attached on partition. is absent. Proven red before the fix, with the old string visible in the captured text.
  • open_receiver_on_partition_defers_the_attach_to_the_first_poll arms an attach error and shows that the open returns Ok while the first poll surfaces the error. It is green by design and pins the lazy contract. Mutation proof: inserting an eager attach before the return makes it fail.
  • open_receiver_on_partition_never_logs_an_attach_that_failed asserts that nothing records an attach when no attach happened, with a positive anchor assertion so it cannot pass vacuously. Mutation proof: moving the attach log onto the per-poll path makes it fail.

Gates run locally, each with CARGO_BUILD_JOBS=1: cargo test --no-run with RUSTFLAGS=-Dwarnings (exit 0), the lib suite with --test-threads=1 (147 passed, 0 failed, 14 ignored), cargo fmt --check, cargo clippy --all-targets with -Dwarnings, cargo doc --no-deps --all-features with RUSTDOCFLAGS=-Dwarnings, 45 doctests, and cspell on the four changed files. No live test ran.

Refs #5094. This pull request does not close that issue. Two of its four proposed items stay open: reporting an invalid consumer group from EventProcessor::run(), and attaching the receiver inside open_receiver_on_partition. Both need a maintainer decision, and the options are set out below.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
3 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

`open_receiver_on_partition` builds an `EventReceiver` and returns. The
AMQP link attaches on the first `stream_events()` poll, but the method
logs "Receiver attached on partition." before it returns. The log claims
an attach that did not happen, and it claims it again when the attach
later fails.

Add three tests in the `consumer::tests` module:

- `open_receiver_on_partition_logs_a_deferred_attach` asserts the log
  says the link attaches on the first poll, and that the old attach
  claim is absent. This test is red until the source changes.
- `open_receiver_on_partition_defers_the_attach_to_the_first_poll`
  arms an attach error and shows that the open succeeds and the first
  poll fails. It pins the lazy contract.
- `open_receiver_on_partition_never_logs_an_attach_that_failed` shows
  that a failed attach records no attach message.

Add a `LogBuffer` writer and a `capture_logs` helper that install a
thread-local `tracing` subscriber, plus two helpers that build an
unconnected client. The tests use `MockCredential` and reach no network.
`ConsumerClient::open_receiver_on_partition` does no network I/O, but it
wrote `info!("Receiver attached on partition.")` before it returned the
`EventReceiver`. The AMQP link attaches on the first poll of
`stream_events()`, and the attach closure in the recoverable connection
already writes the truthful record. A reader of the log saw an attach
that had not happened, and the log kept that claim even when the attach
failed later.

The message now says the call created the receiver, and that the link
attaches on the first poll. The level, the position, and the four
fields stay the same. No eager attach is added.

The documentation on `open_receiver_on_partition`, `EventProcessor::run`
and `PartitionClient::stream_events` now states where the attach
happens, and that the service reports an unknown consumer group or an
unknown partition id from that first poll. The old text also named
`MessageReceiver`, a type that does not exist.

Refs #5094
@j7nw4r
Johnathan W (j7nw4r) force-pushed the j7nw4r/fix-eventhubs-receiver-attach branch from 90c20e8 to 6339dc1 Compare August 25, 2026 18:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant