From f4d80f22b09e09fc347ced9feff3b95ead09751a Mon Sep 17 00:00:00 2001 From: halspang Date: Tue, 11 Mar 2025 12:48:47 -0700 Subject: [PATCH 1/2] Introduce versioning to the DurableTaskClient This change adds a DefaultVersion to the DurableTaskClient builder options. When no other version is set, which is currently always the case, this value is used as the version of any new orchestration started. The value is then passed down to workers via the TaskOrchestrationContext. This allows users to specify a version during their app setup and then key off of the version during orchestration. Using conditional logic, changes can then be made to the orchestration without harming in-progress orchestrations. Signed-off-by: halspang --- src/Abstractions/TaskOrchestrationContext.cs | 5 +++ .../DurableTaskClientBuilderExtensions.cs | 12 +++++++ src/Client/Core/DurableTaskClientOptions.cs | 13 +++++++ src/Client/Grpc/GrpcDurableTaskClient.cs | 12 ++++++- .../Shims/TaskOrchestrationContextWrapper.cs | 3 ++ ...DurableTaskClientBuilderExtensionsTests.cs | 14 ++++++++ .../IntegrationTestBase.cs | 16 +++++---- .../OrchestrationPatterns.cs | 35 +++++++++++++++++-- 8 files changed, 99 insertions(+), 11 deletions(-) diff --git a/src/Abstractions/TaskOrchestrationContext.cs b/src/Abstractions/TaskOrchestrationContext.cs index b37b46d36..4eb087bae 100644 --- a/src/Abstractions/TaskOrchestrationContext.cs +++ b/src/Abstractions/TaskOrchestrationContext.cs @@ -59,6 +59,11 @@ public abstract class TaskOrchestrationContext /// public abstract bool IsReplaying { get; } + /// + /// Gets the version of the current orchestration instance, which was set when the instance was created. + /// + public abstract string Version { get; } + /// /// Gets the entity feature, for interacting with entities. /// diff --git a/src/Client/Core/DependencyInjection/DurableTaskClientBuilderExtensions.cs b/src/Client/Core/DependencyInjection/DurableTaskClientBuilderExtensions.cs index bb274ad08..5c8547592 100644 --- a/src/Client/Core/DependencyInjection/DurableTaskClientBuilderExtensions.cs +++ b/src/Client/Core/DependencyInjection/DurableTaskClientBuilderExtensions.cs @@ -89,4 +89,16 @@ public static IDurableTaskClientBuilder UseBuildTarget(this I }); return builder; } + + /// + /// Sets the default version for this builder. This version will be applied by default to all orchestrations if set. + /// + /// The builder to set the version for. + /// The version that will be used as the default version. + /// The original builder, for call chaining. + public static IDurableTaskClientBuilder UseDefaultVersion(this IDurableTaskClientBuilder builder, string version) + { + builder.Configure(options => options.DefaultVersion = version); + return builder; + } } diff --git a/src/Client/Core/DurableTaskClientOptions.cs b/src/Client/Core/DurableTaskClientOptions.cs index 296b750db..05f19e7bd 100644 --- a/src/Client/Core/DurableTaskClientOptions.cs +++ b/src/Client/Core/DurableTaskClientOptions.cs @@ -13,6 +13,14 @@ public class DurableTaskClientOptions DataConverter dataConverter = JsonDataConverter.Default; bool enableEntitySupport; + /// + /// Gets or sets the version of orchestrations that will be created. + /// + /// + /// Currently, this is sourced from the AzureManaged client options. + /// + public string DefaultVersion { get; set; } = string.Empty; + /// /// Gets or sets the data converter. Default value is . /// @@ -95,6 +103,11 @@ internal void ApplyTo(DurableTaskClientOptions other) { other.EnableEntitySupport = this.EnableEntitySupport; } + + if (!string.IsNullOrWhiteSpace(this.DefaultVersion)) + { + other.DefaultVersion = this.DefaultVersion; + } } } } diff --git a/src/Client/Grpc/GrpcDurableTaskClient.cs b/src/Client/Grpc/GrpcDurableTaskClient.cs index a408151d7..e7fd1da92 100644 --- a/src/Client/Grpc/GrpcDurableTaskClient.cs +++ b/src/Client/Grpc/GrpcDurableTaskClient.cs @@ -78,10 +78,20 @@ public override async Task ScheduleNewOrchestrationInstanceAsync( { Check.NotEntity(this.options.EnableEntitySupport, options?.InstanceId); + string version = string.Empty; + if (!string.IsNullOrEmpty(orchestratorName.Version)) + { + version = orchestratorName.Version; + } + else if (!string.IsNullOrEmpty(this.options.DefaultVersion)) + { + version = this.options.DefaultVersion; + } + var request = new P.CreateInstanceRequest { Name = orchestratorName.Name, - Version = orchestratorName.Version, + Version = version, InstanceId = options?.InstanceId ?? Guid.NewGuid().ToString("N"), Input = this.DataConverter.Serialize(input), }; diff --git a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs index 7151a4e0f..8ce7d5125 100644 --- a/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs +++ b/src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs @@ -81,6 +81,9 @@ public override TaskOrchestrationEntityFeature Entities } } + /// + public override string Version => this.innerContext.Version; + /// /// Gets the DataConverter to use for inputs, outputs, and entity states. /// diff --git a/test/Client/Grpc.Tests/DependencyInjection/DurableTaskClientBuilderExtensionsTests.cs b/test/Client/Grpc.Tests/DependencyInjection/DurableTaskClientBuilderExtensionsTests.cs index 8c0498eb7..59c03d98c 100644 --- a/test/Client/Grpc.Tests/DependencyInjection/DurableTaskClientBuilderExtensionsTests.cs +++ b/test/Client/Grpc.Tests/DependencyInjection/DurableTaskClientBuilderExtensionsTests.cs @@ -67,6 +67,20 @@ public void UseGrpc_Callback_Sets() options.Address.Should().BeNull(); } + [Fact] + public void UseDefaultVersion_DefaultVersion_Sets() + { + ServiceCollection services = new(); + DefaultDurableTaskClientBuilder builder = new(null, services); + builder.UseDefaultVersion("0.1") + .UseGrpc(); + + IServiceProvider provider = services.BuildServiceProvider(); + GrpcDurableTaskClientOptions options = provider.GetOptions(); + + options.DefaultVersion.Should().Be("0.1"); + } + #if NET6_0_OR_GREATER static GrpcChannel GetChannel() => GrpcChannel.ForAddress("http://localhost:9001"); #endif diff --git a/test/Grpc.IntegrationTests/IntegrationTestBase.cs b/test/Grpc.IntegrationTests/IntegrationTestBase.cs index fd63613f4..d6a915c92 100644 --- a/test/Grpc.IntegrationTests/IntegrationTestBase.cs +++ b/test/Grpc.IntegrationTests/IntegrationTestBase.cs @@ -2,13 +2,13 @@ // Licensed under the MIT License. using System.Diagnostics; -using Microsoft.DurableTask.Tests.Logging; using Microsoft.DurableTask.Client; +using Microsoft.DurableTask.Tests.Logging; using Microsoft.DurableTask.Worker; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Xunit.Abstractions; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.DurableTask.Grpc.Tests; @@ -43,9 +43,9 @@ void IDisposable.Dispose() GC.SuppressFinalize(this); } - protected async Task StartWorkerAsync(Action configure) + protected async Task StartWorkerAsync(Action workerConfigure, Action? clientConfigure = null) { - IHost host = this.CreateHostBuilder(configure).Build(); + IHost host = this.CreateHostBuilder(workerConfigure, clientConfigure).Build(); await host.StartAsync(this.TimeoutToken); return new HostTestLifetime(host, this.TimeoutToken); } @@ -53,8 +53,9 @@ protected async Task StartWorkerAsync(Action /// Creates a configured to output logs to xunit logging infrastructure. /// - /// Configures the durable task builder. - protected IHostBuilder CreateHostBuilder(Action configure) + /// Configures the durable task worker builder. + /// Configures the durable task client builder. + protected IHostBuilder CreateHostBuilder(Action workerConfigure, Action? clientConfigure) { return Host.CreateDefaultBuilder() .ConfigureLogging(b => @@ -68,13 +69,14 @@ protected IHostBuilder CreateHostBuilder(Action confi services.AddDurableTaskWorker(b => { b.UseGrpc(this.sidecarFixture.Channel); - configure(b); + workerConfigure(b); }); services.AddDurableTaskClient(b => { b.UseGrpc(this.sidecarFixture.Channel); b.RegisterDirectly(); + clientConfigure?.Invoke(b); }); }); } diff --git a/test/Grpc.IntegrationTests/OrchestrationPatterns.cs b/test/Grpc.IntegrationTests/OrchestrationPatterns.cs index 071dbad5c..3cbc0d793 100644 --- a/test/Grpc.IntegrationTests/OrchestrationPatterns.cs +++ b/test/Grpc.IntegrationTests/OrchestrationPatterns.cs @@ -3,11 +3,11 @@ using System.Text.Json; using System.Text.Json.Nodes; -using Microsoft.DurableTask.Worker; +using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Tests.Logging; +using Microsoft.DurableTask.Worker; using Microsoft.Extensions.DependencyInjection; using Xunit.Abstractions; -using Microsoft.DurableTask.Client; namespace Microsoft.DurableTask.Grpc.Tests; @@ -561,8 +561,37 @@ public async Task SpecialSerialization() Assert.Equal("new value", output?["newProperty"]?.ToString()); } + // TODO: Additional versioning tests + [Fact] + public async Task OrchestrationVersionPassedThroughContext() + { + var version = "0.1"; + await using HostTestLifetime server = await this.StartWorkerAsync(b => + { + b.AddTasks(tasks => tasks + .AddOrchestratorFunc("Versioned_Orchestration", (ctx, input) => + { + return ctx.CallActivityAsync("Versioned_Activity", ctx.Version); + }) + .AddActivityFunc("Versioned_Activity", (ctx, input) => + { + return $"Orchestration version: {input}"; + })); + }, c => + { + c.UseDefaultVersion(version); + }); + + var instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync("Versioned_Orchestration", input: string.Empty); + var result = await server.Client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true, this.TimeoutToken); + var output = result.ReadOutputAs(); + + Assert.NotNull(output); + Assert.Equal(output, $"Orchestration version: {version}"); + + } + // TODO: Test for multiple external events with the same name // TODO: Test for ContinueAsNew with external events that carry over // TODO: Test for catching activity exceptions of specific types - // TODO: Versioning tests } From f795c87eeb3a3526686844eb8d1f7437e1bfbb09 Mon Sep 17 00:00:00 2001 From: Chris Gillum Date: Wed, 12 Mar 2025 16:04:36 -0700 Subject: [PATCH 2/2] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 418fe8729..55f9d0172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v1.9.0 (unreleased) + +- Introduce default version setting to DurableTaskClient and expose to orchestrator ([#393](https://github.com/microsoft/durabletask-dotnet/pull/393)) + ## v1.8.1 - Add timeout to gRPC workitem streaming ([#390](https://github.com/microsoft/durabletask-dotnet/pull/390))