Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/Client/AzureManaged/DurableTaskSchedulerClientExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<GrpcChannel>(source.CreateChannel)).Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<GrpcChannel>(source.CreateChannel)).Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<GrpcDurableTaskClientOptions> optionsFactory = provider.GetRequiredService<IOptionsFactory<GrpcDurableTaskClientOptions>>();
Expand All @@ -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();
Expand All @@ -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<GrpcDurableTaskClientOptions> optionsMonitor = provider.GetRequiredService<IOptionsMonitor<GrpcDurableTaskClientOptions>>();
Expand Down Expand Up @@ -397,4 +397,143 @@ public async Task UseDurableTaskScheduler_ConfigureAfterDispose_ThrowsObjectDisp
Action action = () => optionsMonitor.Get(Options.DefaultName);
action.Should().Throw<ObjectDisposedException>("configuring options after disposal should throw");
}

[Fact]
public async Task UseDurableTaskScheduler_DifferentResourceId_UsesSeparateChannels()
{
// Arrange
ServiceCollection services = new ServiceCollection();
Mock<IDurableTaskClientBuilder> mockBuilder1 = new Mock<IDurableTaskClientBuilder>();
Mock<IDurableTaskClientBuilder> mockBuilder2 = new Mock<IDurableTaskClientBuilder>();
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<GrpcDurableTaskClientOptions> optionsMonitor = provider.GetRequiredService<IOptionsMonitor<GrpcDurableTaskClientOptions>>();
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<IDurableTaskClientBuilder> mockBuilder1 = new Mock<IDurableTaskClientBuilder>();
Mock<IDurableTaskClientBuilder> mockBuilder2 = new Mock<IDurableTaskClientBuilder>();
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<GrpcDurableTaskClientOptions> optionsMonitor = provider.GetRequiredService<IOptionsMonitor<GrpcDurableTaskClientOptions>>();
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<IDurableTaskClientBuilder> mockBuilder1 = new Mock<IDurableTaskClientBuilder>();
Mock<IDurableTaskClientBuilder> mockBuilder2 = new Mock<IDurableTaskClientBuilder>();
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<GrpcDurableTaskClientOptions> optionsMonitor = provider.GetRequiredService<IOptionsMonitor<GrpcDurableTaskClientOptions>>();
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<IDurableTaskClientBuilder> mockBuilder1 = new Mock<IDurableTaskClientBuilder>();
Mock<IDurableTaskClientBuilder> mockBuilder2 = new Mock<IDurableTaskClientBuilder>();
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<GrpcDurableTaskClientOptions> optionsMonitor = provider.GetRequiredService<IOptionsMonitor<GrpcDurableTaskClientOptions>>();
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");
}
}
Loading
Loading