diff --git a/src/DurableTask.ServiceBus/ServiceBusOrchestrationService.cs b/src/DurableTask.ServiceBus/ServiceBusOrchestrationService.cs index b8cd2791..14d5f0d8 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/HubQueueMaxDeliveryCountsTests.cs b/test/DurableTask.ServiceBus.Tests/HubQueueMaxDeliveryCountsTests.cs new file mode 100644 index 00000000..ef1d7279 --- /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 00000000..3ef5dbdf --- /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/test/DurableTask.ServiceBus.Tests/WaitForOrchestrationTests.cs b/test/DurableTask.ServiceBus.Tests/WaitForOrchestrationTests.cs index faee4592..ec85ad39 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,