Skip to content
Draft
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
494fc0d
test: cover Event Hubs authentication recovery
j7nw4r Aug 26, 2026
56d4ef2
fix(eventhubs): add uamqp authentication retry primitives
j7nw4r Aug 26, 2026
522bbe5
fix(eventhubs): recover uamqp producer authentication once
j7nw4r Aug 26, 2026
ade94a5
fix(eventhubs): own replaceable uamqp receiver stack
j7nw4r Aug 26, 2026
e45931d
fix(eventhubs): recover uamqp receiver authentication once
j7nw4r Aug 26, 2026
fe84f7c
docs(eventhubs): document uamqp authentication recovery
j7nw4r Aug 26, 2026
0981936
test: tighten uamqp authentication recovery coverage
j7nw4r Aug 26, 2026
c1bf035
fix(eventhubs): close uamqp authentication recovery races
j7nw4r Aug 26, 2026
29549e9
fix(eventhubs): scope unauthorized recovery to transfers
j7nw4r Aug 26, 2026
fe0e9f7
test: cover producer session invalidation race
j7nw4r Aug 26, 2026
58c7ae8
fix(eventhubs): protect producer session snapshot
j7nw4r Aug 26, 2026
f860d2d
fix(eventhubs): stabilize consumer client layout
j7nw4r Aug 27, 2026
571d35f
fix(eventhubs): stabilize partition client layout
j7nw4r Aug 27, 2026
a4ff8a3
fix(eventhubs): close state on partition client move
j7nw4r Aug 27, 2026
86a391a
fix(eventhubs): release closed partition states
j7nw4r Aug 27, 2026
53d0869
test: synchronize mock AMQP connection count
j7nw4r Aug 27, 2026
b318372
fix(core-amqp): keep AuthenticationException on the management path
j7nw4r Aug 27, 2026
292c09a
fix(eventhubs): release the first connection after a receiver rebuild
j7nw4r Aug 27, 2026
609c1d5
fix(eventhubs): reject a partition client before the stack is built
j7nw4r Aug 27, 2026
6304b8c
refactor(eventhubs): name the shared retry wait for what it does
j7nw4r Aug 27, 2026
13e345c
docs(eventhubs): state the CBS open retry bound that ships
j7nw4r Aug 27, 2026
12b6aca
test(core-amqp): let the mock AMQP server serve a client reconnect
j7nw4r Aug 27, 2026
ce12955
test(eventhubs): make the authentication recovery tests pass on Linux
j7nw4r Aug 27, 2026
200e1e4
test(eventhubs): clear the recovery test hooks with a guard
j7nw4r Aug 27, 2026
d762a6c
test(eventhubs): expect AuthenticationException from a properties call
j7nw4r Aug 27, 2026
f7f1bb8
fix(core-amqp): encode annotations and footer as described sections
j7nw4r Aug 27, 2026
520b86f
Merge branch 'main' into fix/retry-cbs-auth-once
j7nw4r Aug 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@

#include <azure/core/context.hpp>

#include <exception>
#include <stdexcept>
#include <string>
#include <utility>

namespace Azure { namespace Core { namespace Amqp { namespace _detail {
class ClaimsBasedSecurityImpl;
Expand Down Expand Up @@ -53,6 +55,23 @@ namespace Azure { namespace Core { namespace Amqp { namespace _detail {
CbsOpenResult Result;
};

#if ENABLE_UAMQP
/** @brief Identifies a failed uAMQP CBS put-token operation. */
class CbsPutTokenFailedException final : public std::runtime_error {
public:
CbsPutTokenFailedException(std::exception_ptr original, std::string const& what)
: std::runtime_error(what), m_original{std::move(original)}
{
}

std::exception_ptr GetOriginal() const { return m_original; }
[[noreturn]] void RethrowOriginal() const { std::rethrow_exception(m_original); }

private:
std::exception_ptr m_original;
};
#endif

enum class CbsTokenType
{
Invalid,
Expand Down
7 changes: 6 additions & 1 deletion sdk/core/azure-core-amqp/src/amqp/connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,14 @@ namespace Azure { namespace Core { namespace Amqp { namespace _detail {
= claimsBasedSecurity->PutToken(tokenType, audienceUrl, token, expiresOn, context);
if (std::get<0>(result) != CbsOperationResult::Ok)
{
throw Azure::Core::Credentials::AuthenticationException(
auto failure = Azure::Core::Credentials::AuthenticationException(
"Could not authenticate client. Error Status: " + std::to_string(std::get<1>(result))
+ " reason: " + std::get<2>(result));
#if ENABLE_UAMQP
throw CbsPutTokenFailedException(std::make_exception_ptr(failure), failure.what());
#else
throw failure;
#endif
}
Log::Stream(Logger::Level::Verbose) << "Close CBS object";
claimsBasedSecurity->Close(context);
Expand Down
40 changes: 28 additions & 12 deletions sdk/core/azure-core-amqp/test/ut/mock_amqp_server.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include <azure/core/amqp/internal/session.hpp>

#include <memory>
#include <utility>

#include <gtest/gtest.h>

Expand Down Expand Up @@ -62,9 +63,20 @@ namespace Azure { namespace Core { namespace Amqp { namespace Tests {
{
}

virtual ~MockServiceEndpoint() = default;

const std::string& GetName() const { return m_name; }

bool OnLinkAttached(
void DetachLink(
Azure::Core::Amqp::_internal::Session const& session,
Azure::Core::Amqp::_internal::LinkEndpoint const& linkEndpoint,
bool closeLink,
Models::_internal::AmqpError const& error) const
{
session.SendDetach(linkEndpoint, closeLink, error);
}

virtual bool OnLinkAttached(
Azure::Core::Amqp::_internal::Session const& session,
std::string const& linkName,
Azure::Core::Amqp::_internal::LinkEndpoint& linkEndpoint,
Expand Down Expand Up @@ -544,24 +556,27 @@ namespace Azure { namespace Core { namespace Amqp { namespace Tests {
public:
AmqpServerMock(
std::string name = testing::UnitTest::GetInstance()->current_test_info()->name())
: m_connectionId{"Mock Server for " + name}, m_testPort{FindAvailableSocket()}
: AmqpServerMock(FindAvailableSocket(), std::move(name), true)
{
// Every server mock has CBS endpoint support
MockServiceEndpointOptions options;
options.EnableTrace = m_enableTrace;
options.ListenerContext = m_listenerContext;
AddServiceEndpoint(std::make_shared<AmqpClaimBasedSecurity>(options));
}
AmqpServerMock(
uint16_t listeningPort,
std::string name = testing::UnitTest::GetInstance()->current_test_info()->name())
: AmqpServerMock(listeningPort, std::move(name), true)
{
}

AmqpServerMock(uint16_t listeningPort, std::string name, bool addCbsEndpoint)
: m_connectionId{"Mock Server for " + name}, m_testPort{listeningPort}
{
// Every server mock has CBS endpoint support
MockServiceEndpointOptions options;
options.EnableTrace = m_enableTrace;
options.ListenerContext = m_listenerContext;
AddServiceEndpoint(std::make_shared<AmqpClaimBasedSecurity>(options));
if (addCbsEndpoint)
{
// Every server mock has CBS endpoint support
MockServiceEndpointOptions options;
options.EnableTrace = m_enableTrace;
options.ListenerContext = m_listenerContext;
AddServiceEndpoint(std::make_shared<AmqpClaimBasedSecurity>(options));
}
}

virtual ~AmqpServerMock()
Expand All @@ -579,6 +594,7 @@ namespace Azure { namespace Core { namespace Amqp { namespace Tests {
}

uint16_t GetPort() const { return m_testPort; }
std::size_t GetConnectionCount() const { return m_connections.size(); }
Comment thread
j7nw4r marked this conversation as resolved.
Outdated
Azure::Core::Context& GetListenerContext() { return m_listenerContext; }

void StartListening()
Expand Down
1 change: 1 addition & 0 deletions sdk/eventhubs/azure-messaging-eventhubs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

### Bugs Fixed

- The uAMQP backend now retries a CBS PutToken failure or exact unauthorized send or receive error once on a fresh connection, subject to `RetryOptions`, while preserving the final error.
- [[#7254]](https://github.com/Azure/azure-sdk-for-cpp/issues/7254) A teardown of the cached sender no longer runs while another thread sends on that sender. `ProducerClient::Send` gives each attempt a copy of the sender, and a failed attempt on one thread closed the object that a second thread was using. On the Rust AMQP backend that close frees the sender, so the race was a use after free. Each partition now has a guard that lets sends run at the same time and makes a teardown wait for the sends in flight. `ProducerClient::Close` uses the same guard, and it now logs a failed close and continues instead of leaving the other objects open.
- [[#7254]](https://github.com/Azure/azure-sdk-for-cpp/issues/7254) `ProducerClient::CreateBatch` now builds a new sender when it cannot read the maximum message size. The client caches a sender for each partition, and a cached sender holds a link that the service detaches after 30 idle minutes. The size of a batch comes from the attached link, so this call was the first one to touch the dead link, and it threw. The `Send(EventData)` overloads go through this call, so the whole producer failed after an idle period even though `Send` builds a new sender on each attempt.
- [[#7254]](https://github.com/Azure/azure-sdk-for-cpp/issues/7254) Updated producer retries to honor `EventHubsException::IsTransient`, treat empty AMQP error conditions as transient, stop immediately for unknown and known non-transient failures, preserve bounded retries for AMQP runtime failures, and make backoff cancellable through `Azure::Core::Context`. Retry accounting now always performs the initial attempt and treats `MaxRetries` as additional retry attempts.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
#include <azure/core/diagnostics/logger.hpp>
#include <azure/core/http/policies/policy.hpp>
#include <azure/core/internal/diagnostics/log.hpp>

#include <memory>
#include <vector>
namespace Azure { namespace Messaging { namespace EventHubs {
namespace _detail {
class EventHubsPropertiesClient;
Expand Down Expand Up @@ -199,6 +202,12 @@ namespace Azure { namespace Messaging { namespace EventHubs {
Core::Context const& context = {});

private:
#if ENABLE_UAMQP
std::mutex m_partitionClientStatesLock;
std::vector<std::shared_ptr<_detail::PartitionClientState>> m_partitionClientStates;
Comment thread
j7nw4r marked this conversation as resolved.
Outdated
bool m_partitionClientStatesClosing{false};
#endif
Comment thread
j7nw4r marked this conversation as resolved.
Outdated

/// The connection string for the Event Hubs namespace
std::string m_connectionString;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@
#include <azure/core/http/policies/policy.hpp>
#include <azure/core/nullable.hpp>

#include <memory>

namespace Azure { namespace Messaging { namespace EventHubs {
namespace _detail {
class PartitionClientFactory;
}
struct PartitionClientState;
} // namespace _detail

class ConsumerClient;
/**brief PartitionClientOptions provides options for the ConsumerClient::CreatePartitionClient
* function.
*/
Expand Down Expand Up @@ -89,6 +94,15 @@ namespace Azure { namespace Messaging { namespace EventHubs {

private:
friend class _detail::PartitionClientFactory;
friend class ConsumerClient;

#if ENABLE_UAMQP
std::shared_ptr<_detail::PartitionClientState> m_state;
Comment thread
j7nw4r marked this conversation as resolved.
explicit PartitionClient(std::shared_ptr<_detail::PartitionClientState> state);
std::shared_ptr<_detail::PartitionClientState> GetState() const { return m_state; }
#endif
Comment thread
j7nw4r marked this conversation as resolved.
Outdated

#if ENABLE_RUST_AMQP
/// The message receiver used to receive events from the partition.
Azure::Core::Amqp::_internal::MessageReceiver m_receiver;

Expand All @@ -115,7 +129,9 @@ namespace Azure { namespace Messaging { namespace EventHubs {
* response to being throttled or encountering a transient error.
*/
Azure::Core::Http::Policies::RetryOptions m_retryOptions{};
#endif

#if ENABLE_RUST_AMQP
/** Creates a new PartitionClient
*
* @param messageReceiver Message Receiver for the partition client.
Expand All @@ -133,10 +149,15 @@ namespace Azure { namespace Messaging { namespace EventHubs {
std::string receiverName,
PartitionClientOptions options,
Core::Http::Policies::RetryOptions retryOptions);
#endif

#if ENABLE_RUST_AMQP || ENABLE_UAMQP
/// Closes the faulted receiver and attaches a new one starting after the last offset.
void RebuildReceiver(Core::Context const& context);
#endif

#if ENABLE_RUST_AMQP
std::string GetStartExpression(Models::StartPosition const& startPosition);
#endif
};
}}} // namespace Azure::Messaging::EventHubs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ namespace Azure { namespace Messaging { namespace EventHubs {
}

private:
struct ProducerCallState;

/// The connection string for the Event Hubs namespace
std::string m_connectionString;

Expand Down Expand Up @@ -259,6 +261,16 @@ namespace Azure { namespace Messaging { namespace EventHubs {
// Ensure that a message sender for the specified partition has been created.
void EnsureSender(std::string const& partitionId, Azure::Core::Context const& context);

EventDataBatch CreateBatch(
EventDataBatchOptions const& options,
Azure::Core::Context const& context,
ProducerCallState& callState);

void Send(
EventDataBatch const& eventDataBatch,
Core::Context const& context,
ProducerCallState& callState);

// Calls EnsureSender, and discards a failed attach unless the context is cancelled.
void EnsureSenderOrInvalidate(
std::string const& partitionId,
Expand All @@ -268,7 +280,8 @@ namespace Azure { namespace Messaging { namespace EventHubs {
// security open reported CbsOpenResult::Error.
void EstablishSenderWithRetry(
std::string const& partitionId,
Azure::Core::Context const& context);
Azure::Core::Context const& context,
ProducerCallState& callState);

// Discards the sender, session, and connection for the partition. A null generation,
// as Close passes, removes whatever is present regardless of generation.
Expand Down
51 changes: 51 additions & 0 deletions sdk/eventhubs/azure-messaging-eventhubs/src/consumer_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,18 @@ namespace Azure { namespace Messaging { namespace EventHubs {
void ConsumerClient::Close(Azure::Core::Context const& context)
{
Log::Stream(Logger::Level::Verbose) << "Close consumer client.";
#if ENABLE_UAMQP
std::vector<std::shared_ptr<_detail::PartitionClientState>> partitionClientStates;
{
std::lock_guard<std::mutex> lock(m_partitionClientStatesLock);
if (m_partitionClientStatesClosing)
{
return;
}
m_partitionClientStatesClosing = true;
partitionClientStates = std::move(m_partitionClientStates);
}
#endif
{
std::unique_lock<std::mutex> lock(m_propertiesClientLock);
if (m_propertiesClient)
Expand All @@ -83,6 +95,12 @@ namespace Azure { namespace Messaging { namespace EventHubs {
m_propertiesClient.reset();
}
}
#if ENABLE_UAMQP
for (auto const& state : partitionClientStates)
{
_detail::ClosePartitionClientState(state, context);
}
#endif
Log::Stream(Logger::Level::Verbose) << "Closing message receivers.";
// Tear down the sessions and then the connections, in that order.
_detail::ForEachBestEffort(
Expand Down Expand Up @@ -218,6 +236,38 @@ namespace Azure { namespace Messaging { namespace EventHubs {
std::string suffix = !partitionId.empty() ? "/Partitions/" + partitionId : "";
std::string hostUrl = m_hostUrl + suffix;

#if ENABLE_UAMQP
auto partition = _detail::PartitionClientFactory::CreatePartitionClient(
m_fullyQualifiedNamespace,
m_credential,
m_targetPort,
m_consumerClientOptions.ApplicationID,
m_consumerClientOptions.CppStandardVersion,
"Consumer for " + m_consumerClientOptions.ApplicationID + " on " + partitionId,
std::move(hostUrl),
m_consumerClientOptions.Name,
options,
m_consumerClientOptions.RetryOptions,
context);
bool closeLatePartition = false;
{
std::lock_guard<std::mutex> lock(m_partitionClientStatesLock);
if (m_partitionClientStatesClosing)
{
closeLatePartition = true;
}
else
{
m_partitionClientStates.push_back(partition.GetState());
}
}
if (closeLatePartition)
{
_detail::ClosePartitionClientState(partition.GetState(), context);
throw Azure::Core::OperationCancelledException("Consumer client is closed.");
}
return partition;
#elif ENABLE_RUST_AMQP
EnsureSession(partitionId, context);

return _detail::PartitionClientFactory::CreatePartitionClient(
Expand All @@ -227,6 +277,7 @@ namespace Azure { namespace Messaging { namespace EventHubs {
options,
m_consumerClientOptions.RetryOptions,
context);
#endif
}

Models::EventHubProperties ConsumerClient::GetEventHubProperties(Core::Context const& context)
Expand Down
Loading
Loading