Skip to content

[EventHubs] Retry a CBS authentication failure once on a fresh connection - #7380

Draft
Johnathan W (j7nw4r) wants to merge 27 commits into
Azure:mainfrom
j7nw4r:fix/retry-cbs-auth-once
Draft

[EventHubs] Retry a CBS authentication failure once on a fresh connection#7380
Johnathan W (j7nw4r) wants to merge 27 commits into
Azure:mainfrom
j7nw4r:fix/retry-cbs-auth-once

Conversation

@j7nw4r

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

Copy link
Copy Markdown
Member

Summary

Adds one bounded uAMQP authentication recovery for Event Hubs producer and receiver calls. A public call can replace its complete AMQP stack and authenticate on a fresh connection after a CBS PutToken non-OK result or an amqp:unauthorized-access transfer result. Rust AMQP and Rust FFI production behavior remain unchanged.

Motivation

A stale uAMQP connection can reject CBS token authentication after credential acquisition succeeds. Without bounded recovery, the public call fails although a replacement connection can succeed. The recovery must not make authorization failures globally transient, retry credential rejection, or classify a producer attach failure as recoverable.

Changes

  • Adds a uAMQP CBS marker exception around a non-OK PutToken result. MessageSender::Open and MessageReceiver::Open throw it; the management client unwraps it, so GetEventHubProperties and GetPartitionProperties still throw AuthenticationException.
  • Adds one authentication recovery budget per public producer or receiver call and shares it across nested phases. MaxRetries == 0 disables this recovery. Each ordinary retry phase gets its own MaxRetries budget after a recovery.
  • Limits producer recovery to an amqp:unauthorized-access transfer result; a producer attach failure stays permanent. A receiver attach rejection reaches the client through the receive result, so it gets the same single recovery.
  • Routes a CBS open Error through the ordinary retry budget in both CreateBatch and Send. Before this change CreateBatch made one extra attempt. CBS open Invalid and Cancelled stay permanent.
  • Uses the configured first RetryDelay, the existing 0.8 to 1.3 jitter, the MaxRetryDelay cap, and Context-aware waits. The default 800 ms delay gives a 640 to 1,040 ms range.
  • Replaces complete producer stacks for batch creation, direct batch send, and convenience send. Sender installation uses generation checks and holds no client map lock during network work.
  • Gives each uAMQP PartitionClient a consumer-owned replaceable receiver stack. Recovery preserves the client handle, resumes exclusively after the last delivered offset, releases the replaced connection, and preserves final pending errors.
  • Cancels and joins active receives during close, rejects a late CreatePartitionClient before and after the stack build, and protects concurrent stack teardown and producer session snapshots.
  • Keeps installed ConsumerClient and PartitionClient layouts identical with no backend macro, ENABLE_UAMQP, and ENABLE_RUST_AMQP. Closes the destination uAMQP receiver state during PartitionClient move assignment. Stores consumer partition states as weak references and promotes only live states during close.
  • Encodes delivery annotations, message annotations, and the footer as described sections in the vendored uAMQP sender. The sender wrote bare maps, and a uAMQP receiver failed to decode any message that carried them.
  • Makes the mock AMQP server serve a second client connection after the first one ends, and synchronizes its connection count.
  • Adds uAMQP recovery, cancellation, retry-budget, error-preservation, ownership, move, concurrency, resume-offset, properties-client, and annotation round-trip coverage.

Test plan

No CI leg builds the uAMQP backend, so the uAMQP results come from a local Linux container (Ubuntu 22.04, gcc 11, arm64, -DDISABLE_RUST_IN_BUILD=ON).

  • Linux uAMQP core: 225 of 227 tests passed. TestCbs.CbsOpenNoListener and TestMessageSendReceive.SenderCloseWhileUnsettledSendIgnoresLateDisposition fail the same way on this branch before these commits. TestManagement.ManagementOpenCloseAuthenticatedFail passes; it failed before the management unwrap.
  • Linux uAMQP Event Hubs: 14 of 14 AuthRecoveryTest cases passed, each under 6 s, with the three concurrency cases and the two receiver cases also passing 3 of 3 on repeat. The remaining non-live tests pass except CheckpointStoreTest.TestCheckpoints, which needs EVENTHUB_CONSUMER_GROUP.
  • TestMessageSendReceive.ReceiverDecodesAnnotationsAndFooter failed before the sender patch with "Message receiver has transitioned to the error state" and passes after it.
  • macOS uAMQP: the socket tests skip.
  • Rust: the CI matrix covers it; the Rust code paths are unchanged.
  • C++14 installed-header probes with no backend macro, ENABLE_UAMQP, and ENABLE_RUST_AMQP produced identical layouts: ConsumerClient size 624 and alignment 8; PartitionClient size 368 and alignment 8.
  • git diff --check passed. clang-format 11 is not installed on the build hosts; a clang-format 21 dry run flags only lines this PR did not add.

Fixes #7376

@azure-pipelines

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds bounded uAMQP authentication recovery for Event Hubs producer and receiver operations.

Changes:

  • Rebuilds producer/receiver stacks once after eligible authentication failures.
  • Preserves retry budgets, receiver offsets, cancellation, and teardown safety.
  • Adds focused uAMQP recovery tests and supporting AMQP test hooks.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
CHANGELOG.md Documents authentication recovery.
claims_based_security.hpp Adds the CBS token-failure exception.
connection.cpp Wraps uAMQP PutToken failures.
mock_amqp_server.hpp Extends AMQP test-server behavior.
consumer_client.hpp Tracks partition-client states.
partition_client.hpp Adds backend-specific receiver state.
producer_client.hpp Adds shared producer-call retry state.
consumer_client.cpp Creates and closes recoverable receivers.
partition_client.cpp Implements receiver rebuilding and resumption.
producer_client.cpp Implements producer authentication recovery.
eventhubs_utilities.hpp Adds recovery helpers and factory APIs.
retry_operation.hpp Defines authentication recovery state.
retry_operation.cpp Implements bounded recovery delay handling.
auth_recovery_test.cpp Tests producer and receiver recovery scenarios.
test/ut/CMakeLists.txt Registers uAMQP recovery tests and hooks.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread sdk/core/azure-core-amqp/test/ut/mock_amqp_server.hpp Outdated
The uAMQP put-token failure now throws CbsPutTokenFailedException, a
std::runtime_error that carries the original AuthenticationException.
The Event Hubs producer and receiver unwrap it. The management client
did not, so GetEventHubProperties and GetPartitionProperties threw the
internal type, and a caller that catches AuthenticationException no
longer saw the failure. TestManagement.ManagementOpenCloseAuthenticatedFail
failed on uAMQP for the same reason.

ManagementClientImpl::Open and ExecuteOperation now catch the marker and
throw the original exception again. The core-amqp CHANGELOG names the new
type on MessageSender::Open and MessageReceiver::Open.
The uAMQP PartitionClient constructor copies the receiver and the session
of the first stack into m_receiver and m_session, and RebuildReceiver
never replaced them. Those copies own the first ConnectionImpl, and the
uAMQP connection closes only when its last owner goes away. A rebuilt
client therefore kept the first socket open until the client was
destroyed.

RebuildReceiver now assigns the receiver and the session of the
installed stack to those members, so the old connection is released.
Receives run under the receive lock, so nothing reads the members while
they change.
ConsumerClient::CreatePartitionClient on uAMQP built the complete
receiver stack, with the network work and the authentication, and only
then tested the closing flag and closed the stack again. The flag is
now tested under the lock before the build. The check after the build
stays, because Close can start while the build runs.
RetryOperation::WaitForAuthenticationRecovery was the generic
cancellable wait, and the receiver rebuild used it for ordinary retries
too. It is now WaitForRetryDelay. The receiver recover lambda called it
from both branches of a condition; only the attempt counter differed.
EstablishSenderWithRetry tested the Execute result and returned on both
paths. Behavior is unchanged.
The one extra attempt for a CBS open Error in CreateBatch became a loop
on the ordinary retry budget when the establish step moved under
RetryOperation::Execute. The catch now carries the reason that bound is
the only one available: uAMQP returns no value that separates a
transient open failure from a permanent one. The CHANGELOG entry links
issue 7376, states the new bound for CreateBatch and Send, and states
that each retry phase gets its own budget after an authentication
recovery.
The mock cancelled its shared listener context when any connection
reached End or Error. That stopped the accept loop and every endpoint
message loop after the first client connection closed, so a client that
reconnected got no CBS reply and waited forever. The endpoint loop also
exited when its link maps became empty, and a finished thread stays
joinable, so a later attach never restarted it.

Only StopListening cancels the listener context now. The endpoint loop
runs until StopProcessing. A state change to Idle counts as a link
disconnect, because a client that drops its connection raises no detach
event, and the loop tolerates a name it already removed. A new attach
waits up to two seconds for the loop to remove a stale link with the
same name. The server-side links skip the authentication step, which
has no credential and would read a source or target address that an
Event Hubs client does not send.

The core-amqp suite on Linux uAMQP reports the same 223 passed and 3
failed before and after this change.
The tests had never run: they skip on macOS and no CI leg builds uAMQP.
On Linux they hung or crashed. The endpoint names used localhost while
the connection string used 127.0.0.1, and the consumer endpoint carried
a port that the consumer client does not put in its partition URL. Two
message checks compared const char pointers with EXPECT_EQ. A delivery
worker captured the LinkEndpoint parameter by reference after the base
class had released it to the new link, so the detach used a dangling
handle.

A receive returns as soon as it holds an event and the queue is empty,
so the resume test collects events across calls, and both receiver tests
wait for the mock to send the first event and the unauthorized detach
before the receive starts. The resume test now captures the selector
filter of each receiver attach and checks that the second attach resumes
after offset 10.

Twelve of the thirteen tests pass on Linux uAMQP with a local fix for
the message annotation encoding in the uAMQP send path; the two receiver
tests need that fix.
The tests cleared the producer snapshot hook and the partition close
hook by hand after each use. A failed ASSERT returns before that line
and leaves the hook armed for the next test. A HookGuard now sets the
hook and clears it when the scope ends.
A rejected CBS put-token on a uAMQP properties call must reach the
caller as Azure::Core::Credentials::AuthenticationException. The
management client path has no unwrap for the internal put-token marker,
so this test fails on this branch with "it throws
Azure::Core::Amqp::_detail::CbsPutTokenFailedException" for both the
producer and the consumer. It stays red until the management path
unwraps the marker.
The vendored uAMQP sender wrote delivery annotations, message
annotations, and the footer as bare maps. The receiving link expects a
described section there, so it logged "Error decoding message" and went
to the error state. The uAMQP receiver stores these sections as bare
maps after it strips the descriptor, so the fix wraps them in the sender
size pass and encode pass, the same way the sender already wraps the
application properties. Upstream azure-uamqp-c encodes message
annotations bare too and does not send the footer or the delivery
annotations at all.

Adds a mock server round trip that fails on the old sender and passes
on the new one.
@j7nw4r

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@j7nw4r

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines will not run the associated pipelines, because the pull request was updated after the run command was issued. Review the pull request again and issue a new run command.

@j7nw4r

Copy link
Copy Markdown
Member Author

/azp run cpp - storage - ci

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

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.

[EventHubs] Retry a CBS authentication failure once on a fresh connection

2 participants