diff --git a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs index d2f81fcd5..5cd4fc94d 100644 --- a/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs +++ b/src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs @@ -144,10 +144,15 @@ public void Configure(string? name, GrpcDurableTaskClientOptions options) string optionsName = name ?? Options.DefaultName; DurableTaskSchedulerClientOptions source = this.schedulerOptions.Get(optionsName); - // Create a cache key based on the options name, endpoint, and task hub. + // Create a cache key that includes all properties that affect CreateChannel behavior. // This ensures channels are reused for the same configuration - // but separate channels are created for different configurations. - string cacheKey = $"{optionsName}:{source.EndpointAddress}:{source.TaskHubName}"; + // but separate channels are created when any relevant property changes. + // Use a delimiter character (\u001F) that will not appear in typical endpoint URIs. + string credentialType = source.Credential?.GetType().FullName ?? "null"; + string retryOptionsKey = source.RetryOptions != null + ? $"{source.RetryOptions.MaxRetries}|{source.RetryOptions.InitialBackoffMs}|{source.RetryOptions.MaxBackoffMs}|{source.RetryOptions.BackoffMultiplier}|{(source.RetryOptions.RetryableStatusCodes != null ? string.Join(",", source.RetryOptions.RetryableStatusCodes) : string.Empty)}" + : "null"; + string cacheKey = $"{optionsName}\u001F{source.EndpointAddress}\u001F{source.TaskHubName}\u001F{source.ResourceId}\u001F{credentialType}\u001F{source.AllowInsecureCredentials}\u001F{retryOptionsKey}"; options.Channel = this.channels.GetOrAdd( cacheKey, _ => new Lazy(source.CreateChannel)).Value; diff --git a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs index d409ca274..946bc4ed7 100644 --- a/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs +++ b/src/Worker/AzureManaged/DurableTaskSchedulerWorkerExtensions.cs @@ -146,11 +146,12 @@ public void Configure(string? name, GrpcDurableTaskWorkerOptions options) string optionsName = name ?? Options.DefaultName; DurableTaskSchedulerWorkerOptions source = this.schedulerOptions.Get(optionsName); - // Create a cache key based on the options name, endpoint, and task hub. + // Create a cache key that includes all properties that affect CreateChannel behavior. // This ensures channels are reused for the same configuration - // but separate channels are created for different configurations. + // but separate channels are created when any relevant property changes. // Use a delimiter character (\u001F) that will not appear in typical endpoint URIs. - string cacheKey = $"{optionsName}\u001F{source.EndpointAddress}\u001F{source.TaskHubName}"; + string credentialType = source.Credential?.GetType().FullName ?? "null"; + string cacheKey = $"{optionsName}\u001F{source.EndpointAddress}\u001F{source.TaskHubName}\u001F{source.ResourceId}\u001F{credentialType}\u001F{source.AllowInsecureCredentials}\u001F{source.WorkerId}"; options.Channel = this.channels.GetOrAdd( cacheKey, _ => new Lazy(source.CreateChannel)).Value; diff --git a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs index 09f79427f..1fe5b9f6f 100644 --- a/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs +++ b/test/Client/AzureManaged.Tests/DurableTaskSchedulerClientExtensionsTests.cs @@ -283,7 +283,7 @@ public void UseDurableTaskScheduler_WithConnectionStringAndRetryOptions_ShouldCo } [Fact] - public void UseDurableTaskScheduler_SameConfiguration_ReusesSameChannel() + public async Task UseDurableTaskScheduler_SameConfiguration_ReusesSameChannel() { // Arrange ServiceCollection services = new ServiceCollection(); @@ -293,7 +293,7 @@ public void UseDurableTaskScheduler_SameConfiguration_ReusesSameChannel() // Act mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); - using ServiceProvider provider = services.BuildServiceProvider(); + await using ServiceProvider provider = services.BuildServiceProvider(); // Resolve options multiple times to trigger channel configuration IOptionsFactory optionsFactory = provider.GetRequiredService>(); @@ -307,7 +307,7 @@ public void UseDurableTaskScheduler_SameConfiguration_ReusesSameChannel() } [Fact] - public void UseDurableTaskScheduler_DifferentNamedOptions_UsesSeparateChannels() + public async Task UseDurableTaskScheduler_DifferentNamedOptions_UsesSeparateChannels() { // Arrange ServiceCollection services = new ServiceCollection(); @@ -322,7 +322,7 @@ public void UseDurableTaskScheduler_DifferentNamedOptions_UsesSeparateChannels() // Act - configure two different named clients with different endpoints mockBuilder1.Object.UseDurableTaskScheduler("endpoint1.westus3.durabletask.io", ValidTaskHub, credential); mockBuilder2.Object.UseDurableTaskScheduler("endpoint2.westus3.durabletask.io", ValidTaskHub, credential); - ServiceProvider provider = services.BuildServiceProvider(); + await using ServiceProvider provider = services.BuildServiceProvider(); // Resolve options for both named clients IOptionsMonitor optionsMonitor = provider.GetRequiredService>(); @@ -397,4 +397,143 @@ public async Task UseDurableTaskScheduler_ConfigureAfterDispose_ThrowsObjectDisp Action action = () => optionsMonitor.Get(Options.DefaultName); action.Should().Throw("configuring options after disposal should throw"); } + + [Fact] + public async Task UseDurableTaskScheduler_DifferentResourceId_UsesSeparateChannels() + { + // Arrange + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder1 = new Mock(); + Mock mockBuilder2 = new Mock(); + mockBuilder1.Setup(b => b.Services).Returns(services); + mockBuilder1.Setup(b => b.Name).Returns("client1"); + mockBuilder2.Setup(b => b.Services).Returns(services); + mockBuilder2.Setup(b => b.Name).Returns("client2"); + DefaultAzureCredential credential = new DefaultAzureCredential(); + + // Act - configure two clients with the same endpoint/taskhub but different ResourceId + mockBuilder1.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential, options => + { + options.ResourceId = "https://durabletask.io"; + }); + mockBuilder2.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential, options => + { + options.ResourceId = "https://custom.durabletask.io"; + }); + await using ServiceProvider provider = services.BuildServiceProvider(); + + // Resolve options for both named clients + IOptionsMonitor optionsMonitor = provider.GetRequiredService>(); + GrpcDurableTaskClientOptions options1 = optionsMonitor.Get("client1"); + GrpcDurableTaskClientOptions options2 = optionsMonitor.Get("client2"); + + // Assert + options1.Channel.Should().NotBeNull(); + options2.Channel.Should().NotBeNull(); + options1.Channel.Should().NotBeSameAs(options2.Channel, "different ResourceId should use different channels"); + } + + [Fact] + public async Task UseDurableTaskScheduler_DifferentCredentialType_UsesSeparateChannels() + { + // Arrange + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder1 = new Mock(); + Mock mockBuilder2 = new Mock(); + mockBuilder1.Setup(b => b.Services).Returns(services); + mockBuilder1.Setup(b => b.Name).Returns("client1"); + mockBuilder2.Setup(b => b.Services).Returns(services); + mockBuilder2.Setup(b => b.Name).Returns("client2"); + + // Act - configure two clients with the same endpoint/taskhub but different credential types + mockBuilder1.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, new DefaultAzureCredential()); + mockBuilder2.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, new AzureCliCredential()); + await using ServiceProvider provider = services.BuildServiceProvider(); + + // Resolve options for both named clients + IOptionsMonitor optionsMonitor = provider.GetRequiredService>(); + GrpcDurableTaskClientOptions options1 = optionsMonitor.Get("client1"); + GrpcDurableTaskClientOptions options2 = optionsMonitor.Get("client2"); + + // Assert + options1.Channel.Should().NotBeNull(); + options2.Channel.Should().NotBeNull(); + options1.Channel.Should().NotBeSameAs(options2.Channel, "different credential type should use different channels"); + } + + [Fact] + public async Task UseDurableTaskScheduler_DifferentAllowInsecureCredentials_UsesSeparateChannels() + { + // Arrange + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder1 = new Mock(); + Mock mockBuilder2 = new Mock(); + mockBuilder1.Setup(b => b.Services).Returns(services); + mockBuilder1.Setup(b => b.Name).Returns("client1"); + mockBuilder2.Setup(b => b.Services).Returns(services); + mockBuilder2.Setup(b => b.Name).Returns("client2"); + DefaultAzureCredential credential = new DefaultAzureCredential(); + + // Act - configure two clients with the same endpoint/taskhub but different AllowInsecureCredentials + mockBuilder1.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential, options => + { + options.AllowInsecureCredentials = false; + }); + mockBuilder2.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential, options => + { + options.AllowInsecureCredentials = true; + }); + await using ServiceProvider provider = services.BuildServiceProvider(); + + // Resolve options for both named clients + IOptionsMonitor optionsMonitor = provider.GetRequiredService>(); + GrpcDurableTaskClientOptions options1 = optionsMonitor.Get("client1"); + GrpcDurableTaskClientOptions options2 = optionsMonitor.Get("client2"); + + // Assert + options1.Channel.Should().NotBeNull(); + options2.Channel.Should().NotBeNull(); + options1.Channel.Should().NotBeSameAs(options2.Channel, "different AllowInsecureCredentials should use different channels"); + } + + [Fact] + public async Task UseDurableTaskScheduler_DifferentRetryOptions_UsesSeparateChannels() + { + // Arrange + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder1 = new Mock(); + Mock mockBuilder2 = new Mock(); + mockBuilder1.Setup(b => b.Services).Returns(services); + mockBuilder1.Setup(b => b.Name).Returns("client1"); + mockBuilder2.Setup(b => b.Services).Returns(services); + mockBuilder2.Setup(b => b.Name).Returns("client2"); + DefaultAzureCredential credential = new DefaultAzureCredential(); + + // Act - configure two clients with the same endpoint/taskhub but different RetryOptions + mockBuilder1.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential, options => + { + options.RetryOptions = new DurableTaskSchedulerClientOptions.ClientRetryOptions + { + MaxRetries = 3 + }; + }); + mockBuilder2.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential, options => + { + options.RetryOptions = new DurableTaskSchedulerClientOptions.ClientRetryOptions + { + MaxRetries = 5 + }; + }); + await using ServiceProvider provider = services.BuildServiceProvider(); + + // Resolve options for both named clients + IOptionsMonitor optionsMonitor = provider.GetRequiredService>(); + GrpcDurableTaskClientOptions options1 = optionsMonitor.Get("client1"); + GrpcDurableTaskClientOptions options2 = optionsMonitor.Get("client2"); + + // Assert + options1.Channel.Should().NotBeNull(); + options2.Channel.Should().NotBeNull(); + options1.Channel.Should().NotBeSameAs(options2.Channel, "different RetryOptions should use different channels"); + } } diff --git a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs index 9695391ea..1db552db2 100644 --- a/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs +++ b/test/Worker/AzureManaged.Tests/DurableTaskSchedulerWorkerExtensionsTests.cs @@ -201,7 +201,7 @@ public void UseDurableTaskScheduler_WithNamedOptions_ShouldConfigureCorrectly() } [Fact] - public void UseDurableTaskScheduler_SameConfiguration_ReusesSameChannel() + public async Task UseDurableTaskScheduler_SameConfiguration_ReusesSameChannel() { // Arrange ServiceCollection services = new ServiceCollection(); @@ -211,7 +211,7 @@ public void UseDurableTaskScheduler_SameConfiguration_ReusesSameChannel() // Act mockBuilder.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential); - using ServiceProvider provider = services.BuildServiceProvider(); + await using ServiceProvider provider = services.BuildServiceProvider(); // Resolve options multiple times to trigger channel configuration via new options instances IOptionsFactory optionsFactory = provider.GetRequiredService>(); @@ -225,7 +225,7 @@ public void UseDurableTaskScheduler_SameConfiguration_ReusesSameChannel() } [Fact] - public void UseDurableTaskScheduler_DifferentNamedOptions_UsesSeparateChannels() + public async Task UseDurableTaskScheduler_DifferentNamedOptions_UsesSeparateChannels() { // Arrange ServiceCollection services = new ServiceCollection(); @@ -240,7 +240,7 @@ public void UseDurableTaskScheduler_DifferentNamedOptions_UsesSeparateChannels() // Act - configure two different named workers with the same endpoint and task hub mockBuilder1.Object.UseDurableTaskScheduler("endpoint.westus3.durabletask.io", ValidTaskHub, credential); mockBuilder2.Object.UseDurableTaskScheduler("endpoint.westus3.durabletask.io", ValidTaskHub, credential); - using ServiceProvider provider = services.BuildServiceProvider(); + await using ServiceProvider provider = services.BuildServiceProvider(); // Resolve options for both named workers IOptionsMonitor optionsMonitor = provider.GetRequiredService>(); @@ -315,5 +315,138 @@ public async Task UseDurableTaskScheduler_ConfigureAfterDispose_ThrowsObjectDisp Action action = () => optionsMonitor.Get(Options.DefaultName); action.Should().Throw("configuring options after disposal should throw"); } + + [Fact] + public async Task UseDurableTaskScheduler_DifferentResourceId_UsesSeparateChannels() + { + // Arrange + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder1 = new Mock(); + Mock mockBuilder2 = new Mock(); + mockBuilder1.Setup(b => b.Services).Returns(services); + mockBuilder1.Setup(b => b.Name).Returns("worker1"); + mockBuilder2.Setup(b => b.Services).Returns(services); + mockBuilder2.Setup(b => b.Name).Returns("worker2"); + DefaultAzureCredential credential = new DefaultAzureCredential(); + + // Act - configure two workers with the same endpoint/taskhub but different ResourceId + mockBuilder1.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential, options => + { + options.ResourceId = "https://durabletask.io"; + }); + mockBuilder2.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential, options => + { + options.ResourceId = "https://custom.durabletask.io"; + }); + await using ServiceProvider provider = services.BuildServiceProvider(); + + // Resolve options for both named workers + IOptionsMonitor optionsMonitor = provider.GetRequiredService>(); + GrpcDurableTaskWorkerOptions options1 = optionsMonitor.Get("worker1"); + GrpcDurableTaskWorkerOptions options2 = optionsMonitor.Get("worker2"); + + // Assert + options1.Channel.Should().NotBeNull(); + options2.Channel.Should().NotBeNull(); + options1.Channel.Should().NotBeSameAs(options2.Channel, "different ResourceId should use different channels"); + } + + [Fact] + public async Task UseDurableTaskScheduler_DifferentCredentialType_UsesSeparateChannels() + { + // Arrange + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder1 = new Mock(); + Mock mockBuilder2 = new Mock(); + mockBuilder1.Setup(b => b.Services).Returns(services); + mockBuilder1.Setup(b => b.Name).Returns("worker1"); + mockBuilder2.Setup(b => b.Services).Returns(services); + mockBuilder2.Setup(b => b.Name).Returns("worker2"); + + // Act - configure two workers with the same endpoint/taskhub but different credential types + mockBuilder1.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, new DefaultAzureCredential()); + mockBuilder2.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, new AzureCliCredential()); + await using ServiceProvider provider = services.BuildServiceProvider(); + + // Resolve options for both named workers + IOptionsMonitor optionsMonitor = provider.GetRequiredService>(); + GrpcDurableTaskWorkerOptions options1 = optionsMonitor.Get("worker1"); + GrpcDurableTaskWorkerOptions options2 = optionsMonitor.Get("worker2"); + + // Assert + options1.Channel.Should().NotBeNull(); + options2.Channel.Should().NotBeNull(); + options1.Channel.Should().NotBeSameAs(options2.Channel, "different credential type should use different channels"); + } + + [Fact] + public async Task UseDurableTaskScheduler_DifferentAllowInsecureCredentials_UsesSeparateChannels() + { + // Arrange + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder1 = new Mock(); + Mock mockBuilder2 = new Mock(); + mockBuilder1.Setup(b => b.Services).Returns(services); + mockBuilder1.Setup(b => b.Name).Returns("worker1"); + mockBuilder2.Setup(b => b.Services).Returns(services); + mockBuilder2.Setup(b => b.Name).Returns("worker2"); + DefaultAzureCredential credential = new DefaultAzureCredential(); + + // Act - configure two workers with the same endpoint/taskhub but different AllowInsecureCredentials + mockBuilder1.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential, options => + { + options.AllowInsecureCredentials = false; + }); + mockBuilder2.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential, options => + { + options.AllowInsecureCredentials = true; + }); + await using ServiceProvider provider = services.BuildServiceProvider(); + + // Resolve options for both named workers + IOptionsMonitor optionsMonitor = provider.GetRequiredService>(); + GrpcDurableTaskWorkerOptions options1 = optionsMonitor.Get("worker1"); + GrpcDurableTaskWorkerOptions options2 = optionsMonitor.Get("worker2"); + + // Assert + options1.Channel.Should().NotBeNull(); + options2.Channel.Should().NotBeNull(); + options1.Channel.Should().NotBeSameAs(options2.Channel, "different AllowInsecureCredentials should use different channels"); + } + + [Fact] + public async Task UseDurableTaskScheduler_DifferentWorkerId_UsesSeparateChannels() + { + // Arrange + ServiceCollection services = new ServiceCollection(); + Mock mockBuilder1 = new Mock(); + Mock mockBuilder2 = new Mock(); + mockBuilder1.Setup(b => b.Services).Returns(services); + mockBuilder1.Setup(b => b.Name).Returns("worker1"); + mockBuilder2.Setup(b => b.Services).Returns(services); + mockBuilder2.Setup(b => b.Name).Returns("worker2"); + DefaultAzureCredential credential = new DefaultAzureCredential(); + + // Act - configure two workers with the same endpoint/taskhub but different WorkerId + mockBuilder1.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential, options => + { + options.WorkerId = "worker-id-1"; + }); + mockBuilder2.Object.UseDurableTaskScheduler(ValidEndpoint, ValidTaskHub, credential, options => + { + options.WorkerId = "worker-id-2"; + }); + await using ServiceProvider provider = services.BuildServiceProvider(); + + // Resolve options for both named workers + IOptionsMonitor optionsMonitor = provider.GetRequiredService>(); + GrpcDurableTaskWorkerOptions options1 = optionsMonitor.Get("worker1"); + GrpcDurableTaskWorkerOptions options2 = optionsMonitor.Get("worker2"); + + // Assert + options1.Channel.Should().NotBeNull(); + options2.Channel.Should().NotBeNull(); + options1.Channel.Should().NotBeSameAs(options2.Channel, "different WorkerId should use different channels"); + } }