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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- Add user agent header to gRPC called in ([#417](https://github.com/microsoft/durabletask-dotnet/pull/417))
- Enrich User-Agent Header in gRPC Metadata to indicate Client or Worker as caller ([#421](https://github.com/microsoft/durabletask-dotnet/pull/421))
- Add extension methods for registering entities by type ([#427](https://github.com/microsoft/durabletask-dotnet/pull/427))
- Add TaskVersion and utilize it for version overrides when starting orchestrations ([#416](https://github.com/microsoft/durabletask-dotnet/pull/416))

## v1.10.0

Expand Down
5 changes: 5 additions & 0 deletions src/Abstractions/TaskName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ namespace Microsoft.DurableTask;
/// <param name="name">The name of the task. Providing <c>null</c> will yield the default struct.</param>
public TaskName(string name)
{
#pragma warning disable 0618
if (name is null)
{
// Force the default struct when null is passed in.
Expand All @@ -27,6 +28,7 @@ public TaskName(string name)
this.Name = name;
this.Version = string.Empty; // expose setting Version only when we actually consume it.
}
#pragma warning restore 0618
}

/// <summary>
Expand All @@ -44,6 +46,7 @@ public TaskName(string name)
/// Task versions is currently locked to <see cref="string.Empty" /> as it is not yet integrated into task
/// identification. This is being left here as we intend to support it soon.
/// </remarks>
[Obsolete("Refer to TaskVersion instead.")]
public string Version { get; }

/// <summary>
Expand Down Expand Up @@ -122,6 +125,7 @@ public override int GetHashCode()
/// <returns>The name and optional version of the current <see cref="TaskName"/> instance.</returns>
public override string ToString()
{
#pragma warning disable 0618
if (string.IsNullOrEmpty(this.Version))
{
return this.Name;
Expand All @@ -130,5 +134,6 @@ public override string ToString()
{
return this.Name + ":" + this.Version;
}
#pragma warning restore 0618
}
}
10 changes: 10 additions & 0 deletions src/Abstractions/TaskOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ public SubOrchestrationOptions(TaskOptions options, string? instanceId = null)
/// Gets the orchestration instance ID.
/// </summary>
public string? InstanceId { get; init; }

/// <summary>
/// Gets the version to associate with the sub-orchestration instance.
/// </summary>
public TaskVersion Version { get; init; } = default!;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we evaluated versioning for TaskActivity? DurableTask.Core has it. I personally haven't used it much myself, unlike orchestration versioning which I used extensively. Still wondering if it is planned?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also haven't used it much, the initial thought here is that differently versioned orchestrations can have the different activities and different version activities in the same orchestration were less likely.

That being said, we've left the door open for it, so if it's a requested feature or something we agree to as a team we can still implement it down the road.

}

/// <summary>
Expand All @@ -108,4 +113,9 @@ public record StartOrchestrationOptions(string? InstanceId = null, DateTimeOffse
/// Gets the tags to associate with the orchestration instance.
/// </summary>
public IReadOnlyDictionary<string, string> Tags { get; init; } = ImmutableDictionary.Create<string, string>();

/// <summary>
/// Gets the version to associate with the orchestration instance.
/// </summary>
public TaskVersion? Version { get; init; }
}
101 changes: 101 additions & 0 deletions src/Abstractions/TaskVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DurableTask;

/// <summary>
/// The version of a durable task.
/// </summary>
public readonly struct TaskVersion : IEquatable<TaskVersion>
{
/// <summary>
/// Initializes a new instance of the <see cref="TaskVersion"/> struct.
/// </summary>
/// <param name="version">The version of the task. Providing <c>null</c> will result in the default struct.</param>
public TaskVersion(string version)
{
if (version == null)
{
this.Version = null!;
}
else
{
this.Version = version;
}
}

/// <summary>
/// Gets the version of a task.
/// </summary>
public string Version { get; }

/// <summary>
/// Implicitly converts a <see cref="TaskVersion"/> into a <see cref="string"/> of the <see cref="Version"/> property value.
/// </summary>
/// <param name="value">The <see cref="TaskVersion"/> to be converted into a <see cref="string"/>.</param>
public static implicit operator string(TaskVersion value) => value.Version;

/// <summary>
/// Implicitly converts a <see cref="string"/> into a <see cref="TaskVersion"/>.
/// </summary>
/// <param name="value">The <see cref="string"/> to convert into a <see cref="TaskVersion"/>.</param>
public static implicit operator TaskVersion(string value) => new TaskVersion(value);

/// <summary>
/// Compares two <see cref="TaskVersion"/> structs for equality.
/// </summary>
/// <param name="a">The first <see cref="TaskVersion"/> to compare.</param>
/// <param name="b">The second <see cref="TaskVersion"/> to compare.</param>
/// <returns><c>true</c> if the two <see cref="TaskVersion"/> objects are equal; otherwise <c>false</c>.</returns>
public static bool operator ==(TaskVersion a, TaskVersion b)
{
return a.Equals(b);
}

/// <summary>
/// Compares two <see cref="TaskVersion"/> structs for inequality.
/// </summary>
/// <param name="a">The first <see cref="TaskVersion"/> to compare.</param>
/// <param name="b">The second <see cref="TaskVersion"/> to compare.</param>
/// <returns><c>false</c> if the two <see cref="TaskVersion"/> objects are equal; otherwise <c>true</c>.</returns>
public static bool operator !=(TaskVersion a, TaskVersion b)
{
return !a.Equals(b);
}

/// <summary>
/// Gets a value indicating whether to <see cref="TaskVersion"/> objects
/// are equal using value semantics.
/// </summary>
/// <param name="other">The other <see cref="TaskVersion"/> to compare to.</param>
/// <returns><c>true</c> if the two <see cref="TaskVersion"/> are equal using value semantics; otherwise <c>false</c>.</returns>
public bool Equals(TaskVersion other)
{
return string.Equals(this.Version, other.Version, StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Gets a value indicating whether to <see cref="TaskVersion"/> objects
/// are equal using value semantics.
/// </summary>
/// <param name="obj">The other object to compare to.</param>
/// <returns><c>true</c> if the two objects are equal using value semantics; otherwise <c>false</c>.</returns>
public override bool Equals(object? obj)
{
if (obj is not TaskVersion other)
{
return false;
}

return this.Equals(other);
}

/// <summary>
/// Calculates a hash code value for the current <see cref="TaskVersion"/> instance.
/// </summary>
/// <returns>A 32-bit hash code value.</returns>
public override int GetHashCode()
{
return StringComparer.OrdinalIgnoreCase.GetHashCode(this.Version);
}
}
5 changes: 3 additions & 2 deletions src/Client/Grpc/GrpcDurableTaskClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,11 @@
{
Check.NotEntity(this.options.EnableEntitySupport, options?.InstanceId);

// We're explicitly OK with an empty version from the options as that had to be explicitly set. It should take precedence over the default.
string version = string.Empty;
if (!string.IsNullOrEmpty(orchestratorName.Version))
if (options?.Version is { } v)
{
version = orchestratorName.Version;
version = v;
}
else if (!string.IsNullOrEmpty(this.options.DefaultVersion))
{
Expand All @@ -107,7 +108,7 @@

if (Activity.Current?.Id != null || Activity.Current?.TraceStateString != null)
{
if (request.ParentTraceContext == null)

Check warning on line 111 in src/Client/Grpc/GrpcDurableTaskClient.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
{
request.ParentTraceContext = new P.TraceContext();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ public override async Task<string> ScheduleNewOrchestrationInstanceAsync(
Event = new ExecutionStartedEvent(-1, serializedInput)
{
Name = orchestratorName.Name,
Version = orchestratorName.Version,
Version = options?.Version ?? string.Empty,
OrchestrationInstance = instance,
ScheduledStartTime = options?.StartAt?.UtcDateTime,
Tags = options?.Tags != null ? options.Tags.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) : null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public static IDurableTaskWorkerBuilder UseVersioning(this IDurableTaskWorkerBui
options.Versioning = new VersioningOptions
{
Version = versionOptions.Version,
DefaultVersion = versionOptions.DefaultVersion,
MatchStrategy = versionOptions.MatchStrategy,
FailureStrategy = versionOptions.FailureStrategy,
};
Expand Down
5 changes: 5 additions & 0 deletions src/Worker/Core/DurableTaskWorkerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ public class VersioningOptions
/// </summary>
public string Version { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the default version that will be used for starting new orchestrations.
/// </summary>
public string DefaultVersion { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the versioning strategy for the Durable Task worker.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Worker/Core/Shims/TaskEntityShim.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ public override string ScheduleNewOrchestration(TaskName name, object? input = n
this.operationActions.Add(new StartNewOrchestrationOperationAction()
{
Name = name.Name,
Version = name.Version,
Version = options?.Version ?? string.Empty,
InstanceId = instanceId,
Input = this.dataConverter.Serialize(input),
ScheduledStartTime = options?.StartAt?.UtcDateTime,
Expand Down
12 changes: 8 additions & 4 deletions src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
try
{
// TODO: Cancellation (https://github.com/microsoft/durabletask-dotnet/issues/7)
#pragma warning disable 0618
if (options?.Retry?.Policy is RetryPolicy policy)
{
return await this.innerContext.ScheduleWithRetry<T>(
Expand All @@ -165,6 +166,7 @@
// Hide the core DTFx types and instead use our own
throw new TaskFailedException(name, e.ScheduleId, e);
}
#pragma warning restore 0618
}

/// <inheritdoc/>
Expand All @@ -177,10 +179,12 @@
static string? GetInstanceId(TaskOptions? options)
=> options is SubOrchestrationOptions derived ? derived.InstanceId : null;
string instanceId = GetInstanceId(options) ?? this.NewGuid().ToString("N");
string defaultVersion = this.invocationContext.Options?.Versioning?.DefaultVersion ?? string.Empty;
string version = options is SubOrchestrationOptions subOptions ? subOptions.Version : defaultVersion;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just want to leave a comment regarding what we discussed offline here. I don't think it is a good idea to conflate worker version and orchestration version. I see these as separate concerns.

Worker version is purely for routing and supporting rolling updates.
Orchestration version is for enabling an API-versioning-esque scheme for backcompat.

Having worker version apply as a default to orchestration version unnecessarily complicates that scenario. Now customers need to think about how their individual orchestrations version in relation to their worker version as a whole. Which one takes precedence? How does a worker version less than an orchestration version behave? What if they aren't comparable at all?

All of these questions will need to be considered, answered, and documented if we keep these intertangled. Whereas if we just keep them entirely separate then answering all of that becomes much simpler.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the comment and the conversation we had offline about this! I'll make sure that this is all clearly documented/explained.

Having worker version apply as a default to orchestration version unnecessarily complicates that scenario. Now customers need to think about how their individual orchestrations version in relation to their worker version as a whole. Which one takes precedence? How does a worker version less than an orchestration version behave? What if they aren't comparable at all?

The default version only really applies to sub-orchestrations from a Worker point of view. It's matching the client side's default version. In my opinion, the worker shouldn't be starting sub-orchestrations without the client but since it does, we provide a means of setting it. For functions apps, this is the same host.json value. For apps just using this SDK, it does look a little weird as you have to supply it in both the client and the worker options.

In regard to what takes precedence, it's always the version set by the options when scheduling an orchestration. The worker version is only used for the matching/accepting of an orchestration. The way they interact is set with the version matching strategy and the version failure strategy. The docs aren't published to learn yet as we wanted support across the SDKs and DTFx first, but this sample readme talks about the interaction: Orchestration versioning sample.

Copy link
Member

@cgillum cgillum May 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this was covered in the offline discussion, but it's probably best to not think of or describe this as a "worker version", but rather as a "default orchestration version" which happens to be something that can be configured at a client and/or worker level.


Check.NotEntity(this.invocationContext.Options.EnableEntitySupport, instanceId);

Check warning on line 185 in src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Dereference of a possibly null reference.

// if this orchestration uses entities, first validate that the suborchsestration call is allowed in the current context
// if this orchestration uses entities, first validate that the suborchestration call is allowed in the current context
if (this.entityFeature != null && !this.entityFeature.EntityContext.ValidateSuborchestrationTransition(out string? errorMsg))
{
throw new InvalidOperationException(errorMsg);
Expand All @@ -192,7 +196,7 @@
{
return await this.innerContext.CreateSubOrchestrationInstanceWithRetry<TResult>(
orchestratorName.Name,
orchestratorName.Version,
version,
instanceId,
policy.ToDurableTaskCoreRetryOptions(),
input);
Expand All @@ -202,7 +206,7 @@
return await this.InvokeWithCustomRetryHandler(
() => this.innerContext.CreateSubOrchestrationInstance<TResult>(
orchestratorName.Name,
orchestratorName.Version,
version,
instanceId,
input),
orchestratorName.Name,
Expand All @@ -213,7 +217,7 @@
{
return await this.innerContext.CreateSubOrchestrationInstance<TResult>(
orchestratorName.Name,
orchestratorName.Version,
version,
instanceId,
input);
}
Expand Down
2 changes: 2 additions & 0 deletions test/Abstractions.Tests/TaskNameTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ public void Ctor_EmptyName_Okay()
{
TaskName name = new(string.Empty);
name.Name.Should().Be(string.Empty);
#pragma warning disable 0618
name.Version.Should().Be(string.Empty);
#pragma warning restore 0618
}

[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ static TaskMessage MatchStartExecutionMessage(TaskName name, object? input, Star
}

return Guid.TryParse(m.OrchestrationInstance.ExecutionId, out _)
&& @event.Name == name.Name && @event.Version == name.Version
&& @event.Name == name.Name && @event.Version == (options?.Version ?? string.Empty)
&& @event.OrchestrationInstance == m.OrchestrationInstance
&& @event.EventId == -1
&& @event.Input == JsonDataConverter.Default.Serialize(input);
Expand Down
Loading
Loading