From 5c79cf4341509747a1ddae56826170909bf1abfe Mon Sep 17 00:00:00 2001 From: Davide Montanari Date: Wed, 21 Oct 2026 22:35:49 -0700 Subject: [PATCH 1/2] Fix GetHubQueueMaxDeliveryCountsAsync throwing when a hub queue is missing --- .../HubQueueMaxDeliveryCountsTests.cs | 117 ++++++++++++++++++ .../NoOpTokenCredential.cs | 35 ++++++ .../ServiceBusOrchestrationService.cs | 17 ++- .../WaitForOrchestrationTests.cs | 5 +- 4 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 Test/DurableTask.ServiceBus.Tests/HubQueueMaxDeliveryCountsTests.cs create mode 100644 Test/DurableTask.ServiceBus.Tests/NoOpTokenCredential.cs diff --git a/Test/DurableTask.ServiceBus.Tests/HubQueueMaxDeliveryCountsTests.cs b/Test/DurableTask.ServiceBus.Tests/HubQueueMaxDeliveryCountsTests.cs new file mode 100644 index 000000000..ef1d72795 --- /dev/null +++ b/Test/DurableTask.ServiceBus.Tests/HubQueueMaxDeliveryCountsTests.cs @@ -0,0 +1,117 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace DurableTask.ServiceBus.Tests +{ + using System; + using System.Collections.Generic; + using Azure.Messaging.ServiceBus; + using Azure.Messaging.ServiceBus.Administration; + using DurableTask.ServiceBus.Common.Abstraction; + using DurableTask.ServiceBus.Settings; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + /// + /// Unit tests for . + /// These use in-memory queue descriptions so they do not require a live Service Bus namespace. + /// + [TestClass] + public class HubQueueMaxDeliveryCountsTests + { + const string HubName = "testhub"; + static ServiceBusOrchestrationService CreateService() + { + return new ServiceBusOrchestrationService( + NoOpTokenCredential.NamespaceHostName, + new NoOpTokenCredential(), + HubName, + null, + null, + new ServiceBusOrchestrationServiceSettings()); + } + + static QueueDescription CreateQueue(string name, int maxDeliveryCount) + { + QueueProperties properties = ServiceBusModelFactory.QueueProperties( + name: name, + lockDuration: TimeSpan.FromMinutes(1), + maxSizeInMegabytes: 1024, + requiresDuplicateDetection: false, + requiresSession: false, + defaultMessageTimeToLive: TimeSpan.MaxValue, + autoDeleteOnIdle: TimeSpan.MaxValue, + deadLetteringOnMessageExpiration: false, + duplicateDetectionHistoryTimeWindow: TimeSpan.FromMinutes(10), + maxDeliveryCount: maxDeliveryCount, + enableBatchedOperations: true, + status: EntityStatus.Active, + forwardTo: string.Empty, + forwardDeadLetteredMessagesTo: string.Empty, + userMetadata: string.Empty, + enablePartitioning: false); + + return new QueueDescription(properties); + } + + [TestMethod] + public void ReturnsMaxDeliveryCountForEachHubQueue() + { + ServiceBusOrchestrationService service = CreateService(); + + Dictionary counts = service.GetHubQueueMaxDeliveryCounts(new[] + { + CreateQueue($"{HubName}/orchestrator", 10), + CreateQueue($"{HubName}/worker", 20), + CreateQueue($"{HubName}/tracking", 30), + }); + + Assert.AreEqual(10, counts["TaskOrchestration"]); + Assert.AreEqual(20, counts["TaskActivity"]); + Assert.AreEqual(30, counts["Tracking"]); + } + + [TestMethod] + public void ReturnsMinusOneForMissingTrackingQueue() + { + // Without an instance store the tracking queue is never created. + ServiceBusOrchestrationService service = CreateService(); + + Dictionary counts = service.GetHubQueueMaxDeliveryCounts(new[] + { + CreateQueue($"{HubName}/orchestrator", 10), + CreateQueue($"{HubName}/worker", 20), + }); + + Assert.AreEqual(10, counts["TaskOrchestration"]); + Assert.AreEqual(20, counts["TaskActivity"]); + Assert.AreEqual(-1, counts["Tracking"]); + } + + [TestMethod] + public void ReturnsMinusOneForAllQueuesWhenHubDoesNotExist() + { + ServiceBusOrchestrationService service = CreateService(); + + Dictionary counts = service.GetHubQueueMaxDeliveryCounts(new[] + { + CreateQueue("otherhub/orchestrator", 10), + CreateQueue("otherhub/worker", 20), + CreateQueue("otherhub/tracking", 30), + }); + + Assert.AreEqual(-1, counts["TaskOrchestration"]); + Assert.AreEqual(-1, counts["TaskActivity"]); + Assert.AreEqual(-1, counts["Tracking"]); + } + } +} diff --git a/Test/DurableTask.ServiceBus.Tests/NoOpTokenCredential.cs b/Test/DurableTask.ServiceBus.Tests/NoOpTokenCredential.cs new file mode 100644 index 000000000..3ef5dbdf2 --- /dev/null +++ b/Test/DurableTask.ServiceBus.Tests/NoOpTokenCredential.cs @@ -0,0 +1,35 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ---------------------------------------------------------------------------------- + +namespace DurableTask.ServiceBus.Tests +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Azure.Core; + + /// + /// Credential stub for offline tests that construct a + /// but never contact Service Bus, so no token is ever requested. + /// + sealed class NoOpTokenCredential : TokenCredential + { + public const string NamespaceHostName = "test.servicebus.windows.net"; + + public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) + => throw new NotSupportedException(); + + public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) + => throw new NotSupportedException(); + } +} diff --git a/src/DurableTask.ServiceBus/ServiceBusOrchestrationService.cs b/src/DurableTask.ServiceBus/ServiceBusOrchestrationService.cs index b8cd27913..14d5f0d8b 100644 --- a/src/DurableTask.ServiceBus/ServiceBusOrchestrationService.cs +++ b/src/DurableTask.ServiceBus/ServiceBusOrchestrationService.cs @@ -426,14 +426,21 @@ internal async Task> GetHubQueueMaxDeliveryCountsAsync() { ManagementClient managementClient = this.CreateManagementClient(); + return GetHubQueueMaxDeliveryCounts(await managementClient.GetQueuesAsync()); + } + + /// + /// Gets the max delivery count of each hub queue from the given queues, or -1 for a queue that doesn't exist + /// + internal Dictionary GetHubQueueMaxDeliveryCounts(IEnumerable allQueues) + { var result = new Dictionary(3); - var queues = - (await managementClient.GetQueuesAsync()).Where(x => x.Path.StartsWith(this.hubName)).ToList(); + var queues = allQueues.Where(x => x.Path.StartsWith(this.hubName)).ToList(); - result.Add("TaskOrchestration", queues.Single(q => string.Equals(q.Path, this.orchestratorEntityName))?.MaxDeliveryCount ?? -1); - result.Add("TaskActivity", queues.Single(q => string.Equals(q.Path, this.workerEntityName))?.MaxDeliveryCount ?? -1); - result.Add("Tracking", queues.Single(q => string.Equals(q.Path, this.trackingEntityName))?.MaxDeliveryCount ?? -1); + result.Add("TaskOrchestration", queues.SingleOrDefault(q => string.Equals(q.Path, this.orchestratorEntityName))?.MaxDeliveryCount ?? -1); + result.Add("TaskActivity", queues.SingleOrDefault(q => string.Equals(q.Path, this.workerEntityName))?.MaxDeliveryCount ?? -1); + result.Add("Tracking", queues.SingleOrDefault(q => string.Equals(q.Path, this.trackingEntityName))?.MaxDeliveryCount ?? -1); return result; } diff --git a/test/DurableTask.ServiceBus.Tests/WaitForOrchestrationTests.cs b/test/DurableTask.ServiceBus.Tests/WaitForOrchestrationTests.cs index faee45921..ec85ad394 100644 --- a/test/DurableTask.ServiceBus.Tests/WaitForOrchestrationTests.cs +++ b/test/DurableTask.ServiceBus.Tests/WaitForOrchestrationTests.cs @@ -31,15 +31,14 @@ namespace DurableTask.ServiceBus.Tests public class WaitForOrchestrationTests { const string InstanceId = "instance-1"; - const string FakeConnectionString = - "Endpoint=sb://test.servicebus.windows.net/;SharedAccessKeyName=key;SharedAccessKey=dGVzdGtleQ=="; static readonly DateTime BaseTime = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc); static ServiceBusOrchestrationService CreateService(FakeInstanceStore instanceStore) { return new ServiceBusOrchestrationService( - FakeConnectionString, + NoOpTokenCredential.NamespaceHostName, + new NoOpTokenCredential(), "testhub", instanceStore, null, From ec32a06cf9ace22e4b9dfd0bc2871876691e541a Mon Sep 17 00:00:00 2001 From: Davide Montanari Date: Wed, 21 Oct 2026 22:50:57 -0700 Subject: [PATCH 2/2] Renamed test files --- .../HubQueueMaxDeliveryCountsTests.cs | 0 .../DurableTask.ServiceBus.Tests/NoOpTokenCredential.cs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {Test => test}/DurableTask.ServiceBus.Tests/HubQueueMaxDeliveryCountsTests.cs (100%) rename {Test => test}/DurableTask.ServiceBus.Tests/NoOpTokenCredential.cs (100%) diff --git a/Test/DurableTask.ServiceBus.Tests/HubQueueMaxDeliveryCountsTests.cs b/test/DurableTask.ServiceBus.Tests/HubQueueMaxDeliveryCountsTests.cs similarity index 100% rename from Test/DurableTask.ServiceBus.Tests/HubQueueMaxDeliveryCountsTests.cs rename to test/DurableTask.ServiceBus.Tests/HubQueueMaxDeliveryCountsTests.cs diff --git a/Test/DurableTask.ServiceBus.Tests/NoOpTokenCredential.cs b/test/DurableTask.ServiceBus.Tests/NoOpTokenCredential.cs similarity index 100% rename from Test/DurableTask.ServiceBus.Tests/NoOpTokenCredential.cs rename to test/DurableTask.ServiceBus.Tests/NoOpTokenCredential.cs