fix(core-amqp): leave an object closed when its close fails - #7326
fix(core-amqp): leave an object closed when its close fails#7326Johnathan W (j7nw4r) wants to merge 1 commit into
Conversation
|
Azure Pipelines: Successfully started running 2 pipeline(s). 8 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Prevents failed AMQP closes from leaving objects open and aborting during destruction.
Changes:
- Ensures uAMQP teardown runs even when close throws.
- Marks Rust AMQP objects closed before detach.
- Adds regression tests and changelog documentation.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
sdk/core/azure-core-amqp/test/ut/message_sender_receiver.cpp |
Adds cancelled-context close tests. |
sdk/core/azure-core-amqp/test/ut/management_tests.cpp |
Adds management close regression test. |
sdk/core/azure-core-amqp/src/impl/uamqp/amqp/private/message_sender_impl.hpp |
Declares sender teardown helper. |
sdk/core/azure-core-amqp/src/impl/uamqp/amqp/private/message_receiver_impl.hpp |
Declares receiver teardown helper. |
sdk/core/azure-core-amqp/src/impl/uamqp/amqp/message_sender.cpp |
Guarantees sender teardown after failures. |
sdk/core/azure-core-amqp/src/impl/uamqp/amqp/message_receiver.cpp |
Guarantees receiver teardown after failures. |
sdk/core/azure-core-amqp/src/impl/uamqp/amqp/management.cpp |
Closes both links and rethrows the first failure. |
sdk/core/azure-core-amqp/src/impl/rust_amqp/amqp/message_sender.cpp |
Clears sender state before detach. |
sdk/core/azure-core-amqp/src/impl/rust_amqp/amqp/management.cpp |
Clears management state before detach. |
sdk/core/azure-core-amqp/CHANGELOG.md |
Documents the bug fix. |
Suppressed comments (1)
sdk/core/azure-core-amqp/test/ut/message_sender_receiver.cpp:536
- This test treats both throwing and returning normally as success, so it does not verify that the original close exception still reaches the caller. Assert the transport-specific outcome to cover the new rethrow behavior rather than relying only on destructor survival.
sender.Close(cancelledContext);
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
ManagementClientImpl::Close, MessageSenderImpl::Close, and MessageReceiverImpl::Close set their open flag to false only at the end of the function. A close that throws skipped that assignment, and the destructor of the object then called AzureNoReturnPath, which stops the process. A caller could not catch the failure. The message sender and the message receiver also skipped the teardown after the throw. That teardown releases the link and calls EnableAsyncOperation(false). Without it the poll count on the connection stays raised, and ~ConnectionImpl stops the process with "Connection is being destroyed while polling". The uAMQP message sender and message receiver now run the teardown in a new CompleteClose method. Close calls it on the normal path and in a catch block that rethrows, so the teardown runs on every path. The uAMQP management client now closes its message sender and its message receiver even when the first close throws. It keeps the first exception, clears its own flag, and rethrows after both closes. A skipped receiver close left the receiver open, and that stopped the process in the receiver destructor. The Rust management client and the Rust message sender clear their flag before the detach call, which matches the Rust message receiver. No close gets a deadline. A ten second deadline made the Event Hubs concurrency test stop the process on each run, because a close under load needs more time. Three tests open an object, close it with a context that is already cancelled, and then destroy that object. Each one asserts that the close throws on the uAMQP stack, and each one stops the process against the production code of main, with exit code 134 and the assert that names the object. The Rust close does not wait on the context, so each test asserts no throw on that stack. The receiver test needs a mock service endpoint, a message target, and a wait for the Open state, because the receiver open returns before the link attaches and the close then never reads the context. Fixes #7323
bf39a3d to
b34bf99
Compare
|
/azp run cpp - storage - ci |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
Closing this one. The change already reached Everything here is in |
Summary
ManagementClientImpl::Close,MessageSenderImpl::Close, andMessageReceiverImpl::Closeclear the open flag only on the last line. A close that throws skips that line and leaves the object open. The destructor then callsAzureNoReturnPath, which stops the process in a Debug build and in a Release build. The caller cannot catch this.Fixes #7323.
Motivation
MessageSenderImpl::Closewaits on its close queue with the context of the caller. A cancelled context, or a peer that sends no detach, makesWaitForResultreturn nothing. The close then throwsOperationCancelledException. An Event Hubs caller that puts a deadline on a properties call therefore stops the process.The uAMQP sender and receiver also run their teardown after that wait, so a throw skips it. The teardown calls
EnableAsyncOperation(false). Without that call the poll count stays raised, and~ConnectionImplaborts with "Connection is being destroyed while polling". A fix that only clears the flag moves the abort.ManagementClientImpl::Closeskips its receiver close when the sender close throws, and the open receiver aborts in its destructor.Pull request #7308 rebases on this change. Issue #7327 records the one open item in this area.
Changes
MessageSenderImplandMessageReceiverImpl(uAMQP) move the teardown into a privateCompleteClosemethod.Closecalls it on the normal path and in a catch block that rethrows.CompleteClosereleases the link, callsEnableAsyncOperation(false), and clears the open flag.ManagementClientImpl::Close(uAMQP) runs both closes, keeps the first exception, clears its own flag, and rethrows.ManagementClientImpl::CloseandMessageSenderImpl::Close(Rust) clear the flag before the detach call. This matches the RustMessageReceiverImpl::Close, which already did this.### Bugs Fixedfor 1.0.0-beta.13.Test plan
Three tests open an object, close it with a context that is already cancelled, and then destroy it.
TestMessageSendReceive.SenderCloseWithCancelledContextandTestMessageSendReceive.ReceiverCloseWithCancelledContextinsdk/core/azure-core-amqp/test/ut/message_sender_receiver.cpp. Both register a mock service endpoint. Both assertEXPECT_THROW(..., Azure::Core::OperationCancelledException)on uAMQP andEXPECT_NO_THROWon Rust.MessageReceiverEventshandler.MessageReceiverImpl::Openreturns before the link attaches, so an immediate close leavesshouldWaitForClosefalse and never reads the context.TestManagement.ManagementCloseWithCancelledContextinsdk/core/azure-core-amqp/test/ut/management_tests.cppasserts the same exception. Its body sits behind#if !defined(USE_NATIVE_BROKER), so it is empty on a Rust build.All three sit inside the
#if !defined(AZ_PLATFORM_MAC)block that holds the tests around them.The pipeline builds the Rust stack alone, because
sdk/core/azure-core-amqp/CMakeLists.txtforcesUSE_RUST_AMQPon unlessDISABLE_RUST_IN_BUILDis set. A cancelled context cannot make a Rust detach fail.amqpmessagesender_detach_and_releaseinrust_wrapper/src/amqp/message_sender.rsuses the call context only for the runtime handle and the error slot, then callsblock_on(sender.inner.detach()).Validation
A Linux container ran the tests on the branch, then ran them again with the production code of
origin/mainrestored.mainTestMessageSendReceive.SenderCloseWithCancelledContext[ OK ] ... (510 ms)AzureNoReturnPath (msg="MessageSenderImpl is being destroyed while open.")atmessage_sender.cpp:125TestMessageSendReceive.ReceiverCloseWithCancelledContext[ OK ] ... (516 ms)AzureNoReturnPath (msg="MessageReceiverImpl is being destroyed while open.")atmessage_receiver.cpp:257TestManagement.ManagementCloseWithCancelledContext[ OK ] ... (720 ms)management.cpp:37: ~ManagementClientImpl(): Assertion ((void)("Management being destroyed while open."), (!m_isOpen)) failedgdbbacktrace of the aborting run, so the frame and the message text are exact.[ PASSED ] 167 tests, exit 0. With the production code ofmainit reaches 120 tests, then aborts atTestManagement.ManagementCloseWithCancelledContextwith exit 134.ubuntu:22.04, GCC 11.4.0, CMake 4.3.4 from the Kitware release (vcpkg needsstring(JSON ... STRING_ENCODE)and Ubuntu 22.04 ships CMake 3.22), and vcpkg at thebuiltin-baselineofvcpkg.json. Thencmake -S . -B build-uamqp -G Ninja -DCMAKE_TOOLCHAIN_FILE=<vcpkg>/scripts/buildsystems/vcpkg.cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON -DDISABLE_RUST_IN_BUILD=ONandcmake --build build-uamqp --target azure-core-amqp-tests. The build needs noWARNINGS_AS_ERRORS=OFF.linux/arm64and the pipeline usesx86_64. The defect sits in C++ control flow and not in code generation, but this measurement does not coverx86_64.