Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions src/Abstractions/TaskName.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
{
// Force the default struct when null is passed in.
this.Name = null!;
this.Version = null!;

Check warning on line 23 in src/Abstractions/TaskName.cs

View workflow job for this annotation

GitHub Actions / build

'TaskName.Version' is obsolete: 'Refer to TaskVersion instead.'

Check warning on line 23 in src/Abstractions/TaskName.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'TaskName.Version' is obsolete: 'Refer to TaskVersion instead.'
}
else
{
this.Name = name;
this.Version = string.Empty; // expose setting Version only when we actually consume it.

Check warning on line 28 in src/Abstractions/TaskName.cs

View workflow job for this annotation

GitHub Actions / build

'TaskName.Version' is obsolete: 'Refer to TaskVersion instead.'

Check warning on line 28 in src/Abstractions/TaskName.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'TaskName.Version' is obsolete: 'Refer to TaskVersion instead.'
}
}

Expand All @@ -44,6 +44,7 @@
/// 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,13 +123,13 @@
/// <returns>The name and optional version of the current <see cref="TaskName"/> instance.</returns>
public override string ToString()
{
if (string.IsNullOrEmpty(this.Version))

Check warning on line 126 in src/Abstractions/TaskName.cs

View workflow job for this annotation

GitHub Actions / build

'TaskName.Version' is obsolete: 'Refer to TaskVersion instead.'

Check warning on line 126 in src/Abstractions/TaskName.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'TaskName.Version' is obsolete: 'Refer to TaskVersion instead.'
{
return this.Name;
}
else
{
return this.Name + ":" + this.Version;

Check warning on line 132 in src/Abstractions/TaskName.cs

View workflow job for this annotation

GitHub Actions / build

'TaskName.Version' is obsolete: 'Refer to TaskVersion instead.'

Check warning on line 132 in src/Abstractions/TaskName.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

'TaskName.Version' is obsolete: 'Refer to TaskVersion instead.'
}
}
}
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 @@ public override async Task<string> ScheduleNewOrchestrationInstanceAsync(
{
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 != null && options.Version != null)
{
version = orchestratorName.Version;
version = options.Version;
}
else if (!string.IsNullOrEmpty(this.options.DefaultVersion))
{
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
12 changes: 8 additions & 4 deletions src/Worker/Core/Shims/TaskOrchestrationContextWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,14 @@
{
return await this.innerContext.ScheduleWithRetry<T>(
name.Name,
name.Version,

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

View workflow job for this annotation

GitHub Actions / build

'TaskName.Version' is obsolete: 'Refer to TaskVersion instead.'
policy.ToDurableTaskCoreRetryOptions(),
input);
}
else if (options?.Retry?.Handler is AsyncRetryHandler handler)
{
return await this.InvokeWithCustomRetryHandler(
() => this.innerContext.ScheduleTask<T>(name.Name, name.Version, input),

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

View workflow job for this annotation

GitHub Actions / build

'TaskName.Version' is obsolete: 'Refer to TaskVersion instead.'
name.Name,
handler,
default);
Expand All @@ -177,10 +177,14 @@
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 != null && this.invocationContext.Options.Versioning != null
? this.invocationContext.Options.Versioning.DefaultVersion
: string.Empty;
string version = options != null && options is SubOrchestrationOptions subOptions ? subOptions.Version : defaultVersion;

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

// 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
114 changes: 112 additions & 2 deletions test/Grpc.IntegrationTests/OrchestrationPatterns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.DurableTask.Abstractions;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Tests.Logging;
using Microsoft.DurableTask.Worker;
Expand Down Expand Up @@ -213,7 +214,7 @@ public async Task SingleActivity()

IReadOnlyCollection<LogEntry> workerLogs = this.GetLogs("Microsoft.DurableTask.Worker");
Assert.NotEmpty(workerLogs);

// Validate logs.
Assert.Single(workerLogs, log => MatchLog(
log,
Expand Down Expand Up @@ -652,7 +653,6 @@ public async Task OrchestrationVersionPassedThroughContext()

Assert.NotNull(output);
Assert.Equal(output, $"Orchestration version: {version}");

}

[Fact]
Expand Down Expand Up @@ -834,6 +834,116 @@ public async Task OrchestrationVersioning_MatchTypeCurrentOrOlder_VersionSuccess
Assert.Equal(output, $"Orchestration version: {clientVersion}");
}

[Fact]
public async Task SubOrchestrationInheritsDefaultVersion()
{
var version = "0.1";
await using HostTestLifetime server = await this.StartWorkerAsync(b =>
{
b.AddTasks(tasks => tasks
.AddOrchestratorFunc<string, string>("Versioned_Orchestration", (ctx, input) =>
{
return ctx.CallSubOrchestratorAsync<string>("Versioned_Sub_Orchestration");
})
.AddOrchestratorFunc<string, string>("Versioned_Sub_Orchestration", (ctx, input) =>
{
return ctx.CallActivityAsync<string>("Versioned_Activity", ctx.Version);
})
.AddActivityFunc<string, string>("Versioned_Activity", (ctx, input) =>
{
return $"Sub Orchestration version: {input}";
}));
b.UseVersioning(new DurableTaskWorkerOptions.VersioningOptions
{
DefaultVersion = version
});
}, 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<string>();

Assert.NotNull(output);
Assert.Equal($"Sub Orchestration version: {version}", output);
}

[Theory]
[InlineData("0.2")]
[InlineData("")]
public async Task OrchestrationTaskVersionOverridesDefaultVersion(string overrideVersion)
{
var version = "0.1";
await using HostTestLifetime server = await this.StartWorkerAsync(b =>
{
b.AddTasks(tasks => tasks
.AddOrchestratorFunc<string, string>("Versioned_Orchestration", (ctx, input) =>
{
return ctx.CallActivityAsync<string>("Versioned_Activity", ctx.Version);
})
.AddActivityFunc<string, string>("Versioned_Activity", (ctx, input) =>
{
return $"Orchestration version: {input}";
}));
}, c =>
{
c.UseDefaultVersion(version);
});

var instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync("Versioned_Orchestration", string.Empty, new StartOrchestrationOptions
{
Version = overrideVersion
});
var result = await server.Client.WaitForInstanceCompletionAsync(instanceId, getInputsAndOutputs: true, this.TimeoutToken);
var output = result.ReadOutputAs<string>();

Assert.NotNull(output);
Assert.Equal($"Orchestration version: {overrideVersion}", output);
}

[Theory]
[InlineData("0.2")]
[InlineData("")]
public async Task SubOrchestrationTaskVersionOverridesDefaultVersion(string overrideVersion)
{
var version = "0.1";
await using HostTestLifetime server = await this.StartWorkerAsync(b =>
{
b.AddTasks(tasks => tasks
.AddOrchestratorFunc<string, string>("Versioned_Orchestration", (ctx, input) =>
{
return ctx.CallSubOrchestratorAsync<string>("Versioned_Sub_Orchestration", new SubOrchestrationOptions
{
Version = overrideVersion
});
})
.AddOrchestratorFunc<string, string>("Versioned_Sub_Orchestration", (ctx, input) =>
{
return ctx.CallActivityAsync<string>("Versioned_Activity", ctx.Version);
})
.AddActivityFunc<string, string>("Versioned_Activity", (ctx, input) =>
{
return $"Sub Orchestration version: {input}";
}));
b.UseVersioning(new DurableTaskWorkerOptions.VersioningOptions
{
DefaultVersion = version,
});
}, 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<string>();

Assert.NotNull(output);
Assert.Equal($"Sub Orchestration version: {overrideVersion}", output);
}

// 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
Expand Down
Loading