From 596055e217fa03f22688bfe1e23161b9ab3a2054 Mon Sep 17 00:00:00 2001 From: Robert Karp Date: Tue, 9 Jun 2026 08:17:56 +0200 Subject: [PATCH] fix(#446605, #442484): broaden shutdown TaskCanceledException guard to cover CancellationToken.None MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous guard checked oce.CancellationToken.IsCancellationRequested, but the Azure ServiceBus SDK raises ProcessErrorAsync with a TaskCanceledException whose CancellationToken is CancellationToken.None during processor shutdown — so IsCancellationRequested is always false and the guard never fired. Widening to any OperationCanceledException is safe because genuine transport errors are ServiceBusException, not OperationCanceledException. Co-Authored-By: Claude Sonnet 4.6 --- docs/CHANGELOG.md | 4 +++ .../Management/Wrappers/ReceiverWrapper.cs | 2 +- .../ReceiverWrapperTests.cs | 34 ++++++++++++++++--- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6f7624e..d4df942 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 5.7.3 +- Fixed + - Corrected the shutdown `OperationCanceledException` guard introduced in 5.7.2. The previous guard checked `oce.CancellationToken.IsCancellationRequested`, which was always `false` because the Azure ServiceBus SDK raises `ProcessErrorAsync` with `CancellationToken.None` during processor shutdown. The guard now matches any `OperationCanceledException`, which is safe since genuine transport errors surface as `ServiceBusException`. + ## 5.7.2 - Fixed - Suppressed spurious `OperationCanceledException` / `TaskCanceledException` APM error entries during pod graceful shutdown. When the Service Bus receive loop is cancelled with a requested cancellation token, the exception is now logged at `Warning` level instead of `Error`. diff --git a/src/Ev.ServiceBus/Management/Wrappers/ReceiverWrapper.cs b/src/Ev.ServiceBus/Management/Wrappers/ReceiverWrapper.cs index 58e6fc3..abc180c 100644 --- a/src/Ev.ServiceBus/Management/Wrappers/ReceiverWrapper.cs +++ b/src/Ev.ServiceBus/Management/Wrappers/ReceiverWrapper.cs @@ -132,7 +132,7 @@ private void TrySetReceptionRegistrationOnContext(MessageContext context, IServi /// protected async Task OnExceptionOccured(ProcessErrorEventArgs exceptionEvent) { - if (exceptionEvent.Exception is OperationCanceledException oce && oce.CancellationToken.IsCancellationRequested) + if (exceptionEvent.Exception is OperationCanceledException) { _messageProcessingLogger.LogWarning( "[Ev.ServiceBus] Receive loop cancelled for {ClientType} '{ResourceId}' during shutdown.", diff --git a/tests/Ev.ServiceBus.UnitTests/ReceiverWrapperTests.cs b/tests/Ev.ServiceBus.UnitTests/ReceiverWrapperTests.cs index d6086b2..b735f34 100644 --- a/tests/Ev.ServiceBus.UnitTests/ReceiverWrapperTests.cs +++ b/tests/Ev.ServiceBus.UnitTests/ReceiverWrapperTests.cs @@ -31,20 +31,44 @@ private static TestableReceiverWrapper CreateWrapper(ILogger>(); var wrapper = CreateWrapper(mockLogger.Object); - using var cts = new CancellationTokenSource(); - cts.Cancel(); + // Azure SDK raises ProcessErrorAsync with CancellationToken.None during shutdown — + // the token on the exception is not the shutdown token, so IsCancellationRequested is false. + var args = new ProcessErrorEventArgs( + new OperationCanceledException("shutdown", CancellationToken.None), + ServiceBusErrorSource.Receive, + "test-namespace", + "test-queue", + CancellationToken.None); + + await wrapper.InvokeOnExceptionOccuredAsync(args); + + mockLogger.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => true), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Never()); + } + + [Fact] + public async Task OnExceptionOccured_WithTaskCanceledException_DoesNotLogError() + { + var mockLogger = new Mock>(); + var wrapper = CreateWrapper(mockLogger.Object); var args = new ProcessErrorEventArgs( - new OperationCanceledException("shutdown", cts.Token), + new TaskCanceledException(), ServiceBusErrorSource.Receive, "test-namespace", "test-queue", - cts.Token); + CancellationToken.None); await wrapper.InvokeOnExceptionOccuredAsync(args);