Skip to content

Commit 9006eec

Browse files
YunchuWangjviaucgillum
authored
Add new packages for Durable Task Scheduler support (#362)
Introduces two new preview packages for use with the Durable Task Scheduler service: * Microsoft.DurableTask.Client.AzureManaged * Microsoft.DurableTask.Worker.AzureManaged These are effectively wrappers around the Grpc packages, adding auth and task hub metadata. Co-authored-by: Jacob Viau <[email protected]> Co-authored-by: Chris Gillum <[email protected]>
1 parent c8cb34c commit 9006eec

20 files changed

+1920
-1
lines changed

.github/workflows/validate-build.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ jobs:
2525
with:
2626
submodules: true
2727

28-
- name: Setup .NET
28+
- name: Setup .NET 6.0
29+
uses: actions/setup-dotnet@v3
30+
with:
31+
dotnet-version: '6.0.x'
32+
33+
- name: Setup .NET from global.json
2934
uses: actions/setup-dotnet@v3
3035
with:
3136
global-json-file: global.json

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Implement work item completion tokens for standalone worker scenarios ([#359](https://github.com/microsoft/durabletask-dotnet/pull/359))
66
- Support for worker concurrency configuration ([#359](https://github.com/microsoft/durabletask-dotnet/pull/359))
77
- Bump System.Text.Json to 6.0.10
8+
- Initial support for the Azure-managed [Durable Task Scheduler](https://techcommunity.microsoft.com/blog/appsonazureblog/announcing-limited-early-access-of-the-durable-task-scheduler-for-azure-durable-/4286526) preview.
89

910
## v1.4.0
1011

Microsoft.DurableTask.sln

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Analyzers.Tests", "test\Ana
7171
EndProject
7272
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureFunctionsApp.Tests", "samples\AzureFunctionsUnitTests\AzureFunctionsApp.Tests.csproj", "{FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}"
7373
EndProject
74+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker.AzureManaged", "src\Worker\AzureManaged\Worker.AzureManaged.csproj", "{6106872F-A730-4A75-9267-1B2E2C2DC18C}"
75+
EndProject
76+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.AzureManaged", "src\Client\AzureManaged\Client.AzureManaged.csproj", "{EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}"
77+
EndProject
78+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared.AzureManaged.Tests", "test\Shared\AzureManaged.Tests\Shared.AzureManaged.Tests.csproj", "{11357B31-9A63-4A5A-9BC5-091952B25BC0}"
79+
EndProject
80+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.AzureManaged.Tests", "test\Client\AzureManaged.Tests\Client.AzureManaged.Tests.csproj", "{A15BA625-DC6B-4C6D-8673-0CB08F1B9737}"
81+
EndProject
82+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Worker.AzureManaged.Tests", "test\Worker\AzureManaged.Tests\Worker.AzureManaged.Tests.csproj", "{B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}"
83+
EndProject
7484
Global
7585
GlobalSection(SolutionConfigurationPlatforms) = preSolution
7686
Debug|Any CPU = Debug|Any CPU
@@ -185,6 +195,34 @@ Global
185195
{FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
186196
{FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
187197
{FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9}.Release|Any CPU.Build.0 = Release|Any CPU
198+
{6106872F-A730-4A75-9267-1B2E2C2DC18C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
199+
{6106872F-A730-4A75-9267-1B2E2C2DC18C}.Debug|Any CPU.Build.0 = Debug|Any CPU
200+
{6106872F-A730-4A75-9267-1B2E2C2DC18C}.Release|Any CPU.ActiveCfg = Release|Any CPU
201+
{6106872F-A730-4A75-9267-1B2E2C2DC18C}.Release|Any CPU.Build.0 = Release|Any CPU
202+
{EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
203+
{EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
204+
{EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
205+
{EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3}.Release|Any CPU.Build.0 = Release|Any CPU
206+
{11357B31-9A63-4A5A-9BC5-091952B25BC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
207+
{11357B31-9A63-4A5A-9BC5-091952B25BC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
208+
{11357B31-9A63-4A5A-9BC5-091952B25BC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
209+
{11357B31-9A63-4A5A-9BC5-091952B25BC0}.Release|Any CPU.Build.0 = Release|Any CPU
210+
{A15BA625-DC6B-4C6D-8673-0CB08F1B9737}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
211+
{A15BA625-DC6B-4C6D-8673-0CB08F1B9737}.Debug|Any CPU.Build.0 = Debug|Any CPU
212+
{A15BA625-DC6B-4C6D-8673-0CB08F1B9737}.Release|Any CPU.ActiveCfg = Release|Any CPU
213+
{A15BA625-DC6B-4C6D-8673-0CB08F1B9737}.Release|Any CPU.Build.0 = Release|Any CPU
214+
{B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
215+
{B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Debug|Any CPU.Build.0 = Debug|Any CPU
216+
{B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU
217+
{B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF}.Release|Any CPU.Build.0 = Release|Any CPU
218+
{869D2D51-9372-4764-B059-C43B6C1180A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
219+
{869D2D51-9372-4764-B059-C43B6C1180A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
220+
{869D2D51-9372-4764-B059-C43B6C1180A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
221+
{869D2D51-9372-4764-B059-C43B6C1180A3}.Release|Any CPU.Build.0 = Release|Any CPU
222+
{D4C87C0F-66CD-459D-B271-340C6D180448}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
223+
{D4C87C0F-66CD-459D-B271-340C6D180448}.Debug|Any CPU.Build.0 = Debug|Any CPU
224+
{D4C87C0F-66CD-459D-B271-340C6D180448}.Release|Any CPU.ActiveCfg = Release|Any CPU
225+
{D4C87C0F-66CD-459D-B271-340C6D180448}.Release|Any CPU.Build.0 = Release|Any CPU
188226
EndGlobalSection
189227
GlobalSection(SolutionProperties) = preSolution
190228
HideSolutionNode = FALSE
@@ -220,6 +258,11 @@ Global
220258
{998E9D97-BD36-4A9D-81FC-5DAC1CE40083} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
221259
{541FCCCE-1059-4691-B027-F761CD80DE92} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
222260
{FC2692E7-79AE-400E-A50F-8E0BCC8C9BD9} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
261+
{6106872F-A730-4A75-9267-1B2E2C2DC18C} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
262+
{EAA6BE9B-E1A5-4E41-9511-EFEA24A51BA3} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
263+
{11357B31-9A63-4A5A-9BC5-091952B25BC0} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
264+
{A15BA625-DC6B-4C6D-8673-0CB08F1B9737} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
265+
{B78F1FFD-47AC-45BE-8FF9-0BF8C9F35DEF} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
223266
EndGlobalSection
224267
GlobalSection(ExtensibilityGlobals) = postSolution
225268
SolutionGuid = {AB41CB55-35EA-4986-A522-387AB3402E71}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<PackageDescription>Azure Managed extensions for the Durable Task Framework client.</PackageDescription>
6+
<EnableStyleCop>true</EnableStyleCop>
7+
<VersionSuffix>preview.1</VersionSuffix>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<ProjectReference Include="../../Client/Grpc/Client.Grpc.csproj" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Azure.Identity" Version="1.13.1" />
16+
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="6.0.1" />
17+
<PackageReference Include="Grpc.Net.Client" Version="2.67.0" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<SharedSection Include="AzureManaged" />
22+
<SharedSection Include="Core" />
23+
</ItemGroup>
24+
25+
</Project>
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Azure.Core;
5+
using Microsoft.DurableTask.Client.Grpc;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.DependencyInjection.Extensions;
8+
using Microsoft.Extensions.Options;
9+
10+
namespace Microsoft.DurableTask.Client.AzureManaged;
11+
12+
/// <summary>
13+
/// Extension methods for configuring Durable Task clients to use the Azure Durable Task Scheduler service.
14+
/// </summary>
15+
public static class DurableTaskSchedulerClientExtensions
16+
{
17+
/// <summary>
18+
/// Configures Durable Task client to use the Azure Durable Task Scheduler service.
19+
/// </summary>
20+
/// <param name="builder">The Durable Task client builder to configure.</param>
21+
/// <param name="endpointAddress">The endpoint address of the Durable Task Scheduler resource. Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io".</param>
22+
/// <param name="taskHubName">The name of the task hub resource associated with the Durable Task Scheduler resource.</param>
23+
/// <param name="credential">The credential used to authenticate with the Durable Task Scheduler task hub resource.</param>
24+
/// <param name="configure">Optional callback to dynamically configure DurableTaskSchedulerClientOptions.</param>
25+
public static void UseDurableTaskScheduler(
26+
this IDurableTaskClientBuilder builder,
27+
string endpointAddress,
28+
string taskHubName,
29+
TokenCredential credential,
30+
Action<DurableTaskSchedulerClientOptions>? configure = null)
31+
{
32+
ConfigureSchedulerOptions(
33+
builder,
34+
options =>
35+
{
36+
options.EndpointAddress = endpointAddress;
37+
options.TaskHubName = taskHubName;
38+
options.Credential = credential;
39+
},
40+
configure);
41+
}
42+
43+
/// <summary>
44+
/// Configures Durable Task client to use the Azure Durable Task Scheduler service using a connection string.
45+
/// </summary>
46+
/// <param name="builder">The Durable Task client builder to configure.</param>
47+
/// <param name="connectionString">The connection string used to connect to the Durable Task Scheduler service.</param>
48+
/// <param name="configure">Optional callback to dynamically configure DurableTaskSchedulerClientOptions.</param>
49+
public static void UseDurableTaskScheduler(
50+
this IDurableTaskClientBuilder builder,
51+
string connectionString,
52+
Action<DurableTaskSchedulerClientOptions>? configure = null)
53+
{
54+
var connectionOptions = DurableTaskSchedulerClientOptions.FromConnectionString(connectionString);
55+
ConfigureSchedulerOptions(
56+
builder,
57+
options =>
58+
{
59+
options.EndpointAddress = connectionOptions.EndpointAddress;
60+
options.TaskHubName = connectionOptions.TaskHubName;
61+
options.Credential = connectionOptions.Credential;
62+
},
63+
configure);
64+
}
65+
66+
/// <summary>
67+
/// Configures Durable Task client to use the Azure Durable Task Scheduler service using configuration options.
68+
/// </summary>
69+
/// <param name="builder">The Durable Task client builder to configure.</param>
70+
/// <param name="configure">Callback to configure DurableTaskSchedulerClientOptions.</param>
71+
public static void UseDurableTaskScheduler(
72+
this IDurableTaskClientBuilder builder,
73+
Action<DurableTaskSchedulerClientOptions>? configure = null)
74+
{
75+
ConfigureSchedulerOptions(builder, _ => { }, configure);
76+
}
77+
78+
static void ConfigureSchedulerOptions(
79+
IDurableTaskClientBuilder builder,
80+
Action<DurableTaskSchedulerClientOptions> initialConfig,
81+
Action<DurableTaskSchedulerClientOptions>? additionalConfig)
82+
{
83+
builder.Services.AddOptions<DurableTaskSchedulerClientOptions>(builder.Name)
84+
.Configure(initialConfig)
85+
.Configure(additionalConfig ?? (_ => { }))
86+
.ValidateDataAnnotations();
87+
88+
builder.Services.TryAddEnumerable(
89+
ServiceDescriptor.Singleton<IConfigureOptions<GrpcDurableTaskClientOptions>, ConfigureGrpcChannel>());
90+
builder.UseGrpc(_ => { });
91+
}
92+
93+
/// <summary>
94+
/// Configuration class that sets up gRPC channels for client options
95+
/// using the provided Durable Task Scheduler options.
96+
/// </summary>
97+
/// <param name="schedulerOptions">Monitor for accessing the current scheduler options configuration.</param>
98+
class ConfigureGrpcChannel(IOptionsMonitor<DurableTaskSchedulerClientOptions> schedulerOptions) :
99+
IConfigureNamedOptions<GrpcDurableTaskClientOptions>
100+
{
101+
/// <summary>
102+
/// Configures the default named options instance.
103+
/// </summary>
104+
/// <param name="options">The options instance to configure.</param>
105+
public void Configure(GrpcDurableTaskClientOptions options) => this.Configure(Options.DefaultName, options);
106+
107+
/// <summary>
108+
/// Configures a named options instance.
109+
/// </summary>
110+
/// <param name="name">The name of the options instance to configure.</param>
111+
/// <param name="options">The options instance to configure.</param>
112+
public void Configure(string? name, GrpcDurableTaskClientOptions options)
113+
{
114+
DurableTaskSchedulerClientOptions source = schedulerOptions.Get(name ?? Options.DefaultName);
115+
options.Channel = source.CreateChannel();
116+
}
117+
}
118+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
using System.ComponentModel.DataAnnotations;
4+
using Azure.Core;
5+
using Azure.Identity;
6+
using Grpc.Core;
7+
using Grpc.Net.Client;
8+
9+
namespace Microsoft.DurableTask;
10+
11+
/// <summary>
12+
/// Options for configuring the Durable Task Scheduler.
13+
/// </summary>
14+
public class DurableTaskSchedulerClientOptions
15+
{
16+
/// <summary>
17+
/// Gets or sets the endpoint address of the Durable Task Scheduler resource.
18+
/// Expected to be in the format "https://{scheduler-name}.{region}.durabletask.io".
19+
/// </summary>
20+
[Required(ErrorMessage = "Endpoint address is required")]
21+
public string EndpointAddress { get; set; } = string.Empty;
22+
23+
/// <summary>
24+
/// Gets or sets the name of the task hub resource associated with the Durable Task Scheduler resource.
25+
/// </summary>
26+
[Required(ErrorMessage = "Task hub name is required")]
27+
public string TaskHubName { get; set; } = string.Empty;
28+
29+
/// <summary>
30+
/// Gets or sets the credential used to authenticate with the Durable Task Scheduler task hub resource.
31+
/// </summary>
32+
public TokenCredential? Credential { get; set; }
33+
34+
/// <summary>
35+
/// Gets or sets the resource ID of the Durable Task Scheduler resource.
36+
/// The default value is https://durabletask.io.
37+
/// </summary>
38+
public string ResourceId { get; set; } = "https://durabletask.io";
39+
40+
/// <summary>
41+
/// Gets or sets a value indicating whether to allow insecure channel credentials.
42+
/// This should only be set to true in local development/testing scenarios.
43+
/// </summary>
44+
public bool AllowInsecureCredentials { get; set; }
45+
46+
/// <summary>
47+
/// Creates a new instance of <see cref="DurableTaskSchedulerClientOptions"/> from a connection string.
48+
/// </summary>
49+
/// <param name="connectionString">The connection string to parse.</param>
50+
/// <returns>A new instance of <see cref="DurableTaskSchedulerClientOptions"/>.</returns>
51+
public static DurableTaskSchedulerClientOptions FromConnectionString(string connectionString)
52+
{
53+
return FromConnectionString(new DurableTaskSchedulerConnectionString(connectionString));
54+
}
55+
56+
/// <summary>
57+
/// Creates a gRPC channel for communicating with the Durable Task Scheduler service.
58+
/// </summary>
59+
/// <returns>A configured <see cref="GrpcChannel"/> instance that can be used to make gRPC calls.</returns>
60+
internal GrpcChannel CreateChannel()
61+
{
62+
Verify.NotNull(this.EndpointAddress, nameof(this.EndpointAddress));
63+
Verify.NotNull(this.TaskHubName, nameof(this.TaskHubName));
64+
string taskHubName = this.TaskHubName;
65+
string endpoint = !this.EndpointAddress.Contains("://")
66+
? $"https://{this.EndpointAddress}"
67+
: this.EndpointAddress;
68+
AccessTokenCache? cache =
69+
this.Credential is not null
70+
? new AccessTokenCache(
71+
this.Credential,
72+
new TokenRequestContext(new[] { $"{this.ResourceId}/.default" }),
73+
TimeSpan.FromMinutes(5))
74+
: null;
75+
CallCredentials managedBackendCreds = CallCredentials.FromInterceptor(
76+
async (context, metadata) =>
77+
{
78+
metadata.Add("taskhub", taskHubName);
79+
if (cache == null)
80+
{
81+
return;
82+
}
83+
84+
AccessToken token = await cache.GetTokenAsync(context.CancellationToken);
85+
metadata.Add("Authorization", $"Bearer {token.Token}");
86+
});
87+
88+
// Production will use HTTPS, but local testing will use HTTP
89+
ChannelCredentials channelCreds = endpoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ?
90+
ChannelCredentials.SecureSsl :
91+
ChannelCredentials.Insecure;
92+
return GrpcChannel.ForAddress(endpoint, new GrpcChannelOptions
93+
{
94+
Credentials = ChannelCredentials.Create(channelCreds, managedBackendCreds),
95+
UnsafeUseInsecureChannelCallCredentials = this.AllowInsecureCredentials,
96+
});
97+
}
98+
99+
/// <summary>
100+
/// Creates a new instance of <see cref="DurableTaskSchedulerClientOptions"/> from a parsed connection string.
101+
/// </summary>
102+
/// <param name="connectionString">The connection string to parse.</param>
103+
/// <returns>A new instance of <see cref="DurableTaskSchedulerClientOptions"/>.</returns>
104+
internal static DurableTaskSchedulerClientOptions FromConnectionString(
105+
DurableTaskSchedulerConnectionString connectionString) => new()
106+
{
107+
EndpointAddress = connectionString.Endpoint,
108+
TaskHubName = connectionString.TaskHubName,
109+
Credential = GetCredentialFromConnectionString(connectionString),
110+
};
111+
112+
static TokenCredential? GetCredentialFromConnectionString(DurableTaskSchedulerConnectionString connectionString)
113+
{
114+
string authType = connectionString.Authentication;
115+
116+
// Parse the supported auth types, in a case-insensitive way and without spaces
117+
switch (authType.ToLowerInvariant())
118+
{
119+
case "defaultazure":
120+
return new DefaultAzureCredential();
121+
case "managedidentity":
122+
return new ManagedIdentityCredential(connectionString.ClientId);
123+
case "workloadidentity":
124+
var opts = new WorkloadIdentityCredentialOptions();
125+
if (!string.IsNullOrEmpty(connectionString.ClientId))
126+
{
127+
opts.ClientId = connectionString.ClientId;
128+
}
129+
130+
if (!string.IsNullOrEmpty(connectionString.TenantId))
131+
{
132+
opts.TenantId = connectionString.TenantId;
133+
}
134+
135+
if (connectionString.AdditionallyAllowedTenants is not null)
136+
{
137+
foreach (string tenant in connectionString.AdditionallyAllowedTenants)
138+
{
139+
opts.AdditionallyAllowedTenants.Add(tenant);
140+
}
141+
}
142+
143+
return new WorkloadIdentityCredential(opts);
144+
case "environment":
145+
return new EnvironmentCredential();
146+
case "azurecli":
147+
return new AzureCliCredential();
148+
case "azurepowershell":
149+
return new AzurePowerShellCredential();
150+
case "none":
151+
return null;
152+
default:
153+
throw new ArgumentException(
154+
$"The connection string contains an unsupported authentication type '{authType}'.",
155+
nameof(connectionString));
156+
}
157+
}
158+
}

0 commit comments

Comments
 (0)