Skip to content

Commit 89de668

Browse files
committed
Merge branch 'main' of https://github.com/microsoft/durabletask-dotnet into wangbill/exportfinal
2 parents 60aef2d + 3b7601e commit 89de668

File tree

17 files changed

+1047
-23
lines changed

17 files changed

+1047
-23
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## v1.19.1
4+
- Throw an `InvalidOperationException` for purge requests on running orchestrations by sophiatev ([#611](https://github.com/microsoft/durabletask-dotnet/pull/611))
5+
- Validate c# identifiers in durabletask source generator by Copilot ([#578](https://github.com/microsoft/durabletask-dotnet/pull/578))
6+
- Document orchestration discovery and method probing behavior in analyzers by Copilot ([#594](https://github.com/microsoft/durabletask-dotnet/pull/594))
7+
38
## v1.19.0
49
- Extended sessions for entities in .net isolated by sophiatev ([#507](https://github.com/microsoft/durabletask-dotnet/pull/507))
510
- Adding the ability to specify tags and a retry policy for suborchestrations by sophiatev ([#603](https://github.com/microsoft/durabletask-dotnet/pull/603))

Directory.Packages.props

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<!-- Azure.* Packages -->
2424
<ItemGroup>
2525
<PackageVersion Include="Azure.Identity" Version="1.17.1" />
26-
<PackageVersion Include="Azure.Storage.Blobs" Version="12.26.0" />
26+
<PackageVersion Include="Azure.Storage.Blobs" Version="12.27.0" />
2727
<PackageVersion Include="Microsoft.Azure.Functions.Worker" Version="2.51.0" />
2828
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.3.0" />
2929
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.7" />
@@ -51,11 +51,11 @@
5151

5252
<!-- Microsoft.CodeAnalysis.* Packages -->
5353
<ItemGroup>
54-
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.12.0" />
54+
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.14.0" />
5555
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
56-
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
57-
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.12.0" />
58-
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.12.0" />
56+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
57+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
58+
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.14.0" />
5959
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />
6060
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" Version="1.1.2" />
6161
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" />

eng/targets/Release.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
</PropertyGroup>
1818

1919
<PropertyGroup>
20-
<VersionPrefix>1.19.0</VersionPrefix>
20+
<VersionPrefix>1.19.1</VersionPrefix>
2121
<VersionSuffix></VersionSuffix>
2222
</PropertyGroup>
2323

src/Client/Core/DurableTaskClient.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,13 @@ public virtual Task<PurgeResult> PurgeInstanceAsync(string instanceId, Cancellat
370370
/// <see cref="PurgeResult.PurgedInstanceCount"/> indicating the number of orchestration instances that were purged,
371371
/// including the count of sub-orchestrations purged if any.
372372
/// </returns>
373+
/// <exception cref="ArgumentNullException">Thrown if <paramref name="instanceId"/> is null.</exception>
374+
/// <exception cref="ArgumentException">Thrown if <paramref name="instanceId"/> is empty or starts with the null character.</exception>
375+
/// <exception cref="InvalidOperationException">Thrown if the orchestration is not in a
376+
/// <see cref="OrchestrationRuntimeStatus.Completed"/>, <see cref="OrchestrationRuntimeStatus.Failed"/>,
377+
/// or <see cref="OrchestrationRuntimeStatus.Terminated"/> state.</exception>
378+
/// <exception cref="NotImplementedException">Thrown if the backend storage provider does not support purging instances.</exception>
379+
/// <exception cref="OperationCanceledException">Thrown if the operation is canceled via the <paramref name="cancellation"/> token.</exception>
373380
public virtual Task<PurgeResult> PurgeInstanceAsync(
374381
string instanceId, PurgeInstanceOptions? options = null, CancellationToken cancellation = default)
375382
{
@@ -447,7 +454,7 @@ public virtual Task<string> RestartAsync(
447454
/// The orchestration's history will be replaced with a new history that excludes the failed Activities and suborchestrations,
448455
/// and a new execution ID will be generated for the rewound orchestration instance. As the failed Activities and suborchestrations
449456
/// re-execute, the history will be appended with new TaskScheduled, TaskCompleted, and SubOrchestrationInstanceCompleted events.
450-
/// Note that only orchestrations in a "Failed" state can be rewound.
457+
/// Note that only orchestrations in a <see cref="OrchestrationRuntimeStatus.Failed"/> state can be rewound.
451458
/// </remarks>
452459
/// <param name="instanceId">The instance ID of the orchestration to rewind.</param>
453460
/// <param name="reason">The reason for the rewind.</param>
@@ -460,7 +467,7 @@ public virtual Task<string> RestartAsync(
460467
/// <exception cref="ArgumentException">Thrown if an orchestration with the specified <paramref name="instanceId"/> does not exist,
461468
/// or if <paramref name="instanceId"/> is the instance ID of an entity.</exception>
462469
/// <exception cref="InvalidOperationException">Thrown if a precondition of the operation fails, for example if the specified
463-
/// orchestration is not in a "Failed" state.</exception>
470+
/// orchestration is not in a <see cref="OrchestrationRuntimeStatus.Failed"/> state.</exception>
464471
/// <exception cref="OperationCanceledException">Thrown if the operation is canceled via the <paramref name="cancellation"/> token.</exception>
465472
public virtual Task RewindInstanceAsync(
466473
string instanceId,

src/Client/Grpc/GrpcDurableTaskClient.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,10 +441,16 @@ public override async Task<OrchestrationMetadata> WaitForInstanceCompletionAsync
441441
public override Task<PurgeResult> PurgeInstanceAsync(
442442
string instanceId, PurgeInstanceOptions? options = null, CancellationToken cancellation = default)
443443
{
444+
Check.NotNullOrEmpty(instanceId);
444445
bool recursive = options?.Recursive ?? false;
445446
this.logger.PurgingInstanceMetadata(instanceId);
446447

447-
P.PurgeInstancesRequest request = new() { InstanceId = instanceId, Recursive = recursive };
448+
P.PurgeInstancesRequest request = new()
449+
{
450+
InstanceId = instanceId,
451+
Recursive = recursive,
452+
IsOrchestration = !this.options.EnableEntitySupport || instanceId[0] != '@',
453+
};
448454
return this.PurgeInstancesCoreAsync(request, cancellation);
449455
}
450456

@@ -646,6 +652,14 @@ async Task<PurgeResult> PurgeInstancesCoreAsync(
646652
throw new OperationCanceledException(
647653
$"The {nameof(this.PurgeAllInstancesAsync)} operation was canceled.", e, cancellation);
648654
}
655+
catch (RpcException e) when (e.StatusCode == StatusCode.FailedPrecondition)
656+
{
657+
throw new InvalidOperationException(e.Status.Detail);
658+
}
659+
catch (RpcException e) when (e.StatusCode == StatusCode.Unimplemented)
660+
{
661+
throw new NotImplementedException(e.Status.Detail);
662+
}
649663
}
650664

651665
OrchestrationMetadata CreateMetadata(P.OrchestrationState state, bool includeInputsAndOutputs)

src/Client/OrchestrationServiceClientShim/ShimDurableTaskClient.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,21 @@ public override async Task<PurgeResult> PurgeInstanceAsync(
122122
string instanceId, PurgeInstanceOptions? options = null, CancellationToken cancellation = default)
123123
{
124124
Check.NotNullOrEmpty(instanceId);
125+
OrchestrationMetadata? orchestrationState = await this.GetInstanceAsync(instanceId, cancellation);
126+
127+
// The orchestration doesn't exist, nothing to purge
128+
if (orchestrationState == null)
129+
{
130+
return new PurgeResult(0);
131+
}
132+
133+
bool isEntity = this.options.EnableEntitySupport && instanceId[0] == '@';
134+
if (!isEntity && !orchestrationState.IsCompleted)
135+
{
136+
throw new InvalidOperationException($"Only orchestrations in a terminal state can be purged, " +
137+
$"but the orchestration with instance ID {instanceId} has status {orchestrationState.RuntimeStatus}");
138+
}
139+
125140
cancellation.ThrowIfCancellationRequested();
126141

127142
// TODO: Support recursive purge of sub-orchestrations

src/Grpc/orchestrator_service.proto

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ message SubOrchestrationInstanceCreatedEvent {
115115
google.protobuf.StringValue version = 3;
116116
google.protobuf.StringValue input = 4;
117117
TraceContext parentTraceContext = 5;
118+
map<string, string> tags = 6;
118119
}
119120

120121
message SubOrchestrationInstanceCompletedEvent {
@@ -225,6 +226,11 @@ message ExecutionRewoundEvent {
225226
google.protobuf.StringValue parentExecutionId = 2; // used only for rewinding suborchestrations, null otherwise
226227
google.protobuf.StringValue instanceId = 3; // used only for rewinding suborchestrations, null otherwise
227228
TraceContext parentTraceContext = 4; // used only for rewinding suborchestrations, null otherwise
229+
google.protobuf.StringValue name = 5; // used by DTS backend only
230+
google.protobuf.StringValue version = 6; // used by DTS backend only
231+
google.protobuf.StringValue input = 7; // used by DTS backend only
232+
ParentInstanceInfo parentInstance = 8; // used by DTS backend only
233+
map<string, string> tags = 9; // used by DTS backend only
228234
}
229235

230236
message HistoryEvent {
@@ -496,15 +502,15 @@ message ListInstanceIdsResponse {
496502
google.protobuf.StringValue lastInstanceKey = 2;
497503
}
498504

499-
// Removed ListTerminalInstances in favor of using QueryInstances
500-
501505
message PurgeInstancesRequest {
502506
oneof request {
503507
string instanceId = 1;
504508
PurgeInstanceFilter purgeInstanceFilter = 2;
505509
InstanceBatch instanceBatch = 4;
506510
}
507511
bool recursive = 3;
512+
// used in the case when an instanceId is specified to determine if the purge request is for an orchestration (as opposed to an entity)
513+
bool isOrchestration = 5;
508514
}
509515

510516
message PurgeInstanceFilter {
@@ -765,7 +771,9 @@ service TaskHubSidecarService {
765771
// rpc DeleteInstance(DeleteInstanceRequest) returns (DeleteInstanceResponse);
766772

767773
rpc QueryInstances(QueryInstancesRequest) returns (QueryInstancesResponse);
774+
768775
rpc ListInstanceIds(ListInstanceIdsRequest) returns (ListInstanceIdsResponse);
776+
769777
rpc PurgeInstances(PurgeInstancesRequest) returns (PurgeInstancesResponse);
770778

771779
rpc GetWorkItems(GetWorkItemsRequest) returns (stream WorkItem);
@@ -870,4 +878,4 @@ message HistoryChunk {
870878
message InstanceBatch {
871879
// A maximum of 500 instance IDs can be provided in this list.
872880
repeated string instanceIds = 1;
873-
}
881+
}

src/Grpc/versions.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
# The following files were downloaded from branch main at 2025-12-29 22:13:55 UTC
2-
https://raw.githubusercontent.com/microsoft/durabletask-protobuf/b7e260ad7b84740a2ed5cb4600ce73bef702a979/protos/orchestrator_service.proto
1+
# The following files were downloaded from branch main at 2026-01-13 00:01:21 UTC
2+
https://raw.githubusercontent.com/microsoft/durabletask-protobuf/026329c53fe6363985655857b9ca848ec7238bd2/protos/orchestrator_service.proto
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using DurableTask.Core;
5+
using Grpc.Net.Client;
6+
using Microsoft.AspNetCore.Builder;
7+
using Microsoft.AspNetCore.Hosting;
8+
using Microsoft.AspNetCore.Server.Kestrel.Core;
9+
using Microsoft.DurableTask.Client;
10+
using Microsoft.DurableTask.Client.Grpc;
11+
using Microsoft.DurableTask.Testing.Sidecar;
12+
using Microsoft.DurableTask.Testing.Sidecar.Grpc;
13+
using Microsoft.DurableTask.Worker;
14+
using Microsoft.DurableTask.Worker.Grpc;
15+
using Microsoft.Extensions.DependencyInjection;
16+
using Microsoft.Extensions.Hosting;
17+
using Microsoft.Extensions.Logging;
18+
19+
namespace Microsoft.DurableTask.Testing;
20+
21+
/// <summary>
22+
/// Extension methods for integrating in-memory durable task testing with your existing DI container,
23+
/// such as WebApplicationFactory.
24+
/// </summary>
25+
public static class DurableTaskTestExtensions
26+
{
27+
/// <summary>
28+
/// These extensions allow you to inject the <see cref="InMemoryOrchestrationService"/> into your
29+
/// existing test host so that your orchestrations and activities can resolve services from your DI container.
30+
/// </summary>
31+
/// <param name="services">The service collection (from your WebApplicationFactory or host).</param>
32+
/// <param name="configureTasks">Action to register orchestrators and activities.</param>
33+
/// <param name="options">Optional configuration options.</param>
34+
/// <returns>The service collection for chaining.</returns>
35+
public static IServiceCollection AddInMemoryDurableTask(
36+
this IServiceCollection services,
37+
Action<DurableTaskRegistry> configureTasks,
38+
InMemoryDurableTaskOptions? options = null)
39+
{
40+
ArgumentNullException.ThrowIfNull(services);
41+
ArgumentNullException.ThrowIfNull(configureTasks);
42+
43+
options ??= new InMemoryDurableTaskOptions();
44+
45+
// Determine port for the internal gRPC server
46+
int port = options.Port ?? Random.Shared.Next(30000, 40000);
47+
string address = $"http://localhost:{port}";
48+
49+
// Register the in-memory orchestration service as a singleton
50+
services.AddSingleton<InMemoryOrchestrationService>(sp =>
51+
{
52+
var loggerFactory = sp.GetService<ILoggerFactory>();
53+
return new InMemoryOrchestrationService(loggerFactory);
54+
});
55+
services.AddSingleton<IOrchestrationService>(sp => sp.GetRequiredService<InMemoryOrchestrationService>());
56+
services.AddSingleton<IOrchestrationServiceClient>(sp => sp.GetRequiredService<InMemoryOrchestrationService>());
57+
58+
// Register the gRPC sidecar server as a hosted service
59+
services.AddSingleton<TaskHubGrpcServer>();
60+
services.AddHostedService<InMemoryGrpcSidecarHost>(sp =>
61+
{
62+
return new InMemoryGrpcSidecarHost(
63+
address,
64+
sp.GetRequiredService<InMemoryOrchestrationService>(),
65+
sp.GetService<ILoggerFactory>());
66+
});
67+
68+
// Create a gRPC channel that will connect to our internal sidecar
69+
services.AddSingleton<GrpcChannel>(sp => GrpcChannel.ForAddress(address));
70+
71+
// Register the durable task worker (connects to our internal sidecar)
72+
services.AddDurableTaskWorker(builder =>
73+
{
74+
builder.UseGrpc(address);
75+
builder.AddTasks(configureTasks);
76+
});
77+
78+
// Register the durable task client (connects to our internal sidecar)
79+
services.AddDurableTaskClient(builder =>
80+
{
81+
builder.UseGrpc(address);
82+
builder.RegisterDirectly();
83+
});
84+
85+
return services;
86+
}
87+
88+
/// <summary>
89+
/// Gets the <see cref="InMemoryOrchestrationService"/> from the service provider.
90+
/// Useful for advanced scenarios like inspecting orchestration state.
91+
/// </summary>
92+
/// <param name="services">The service provider.</param>
93+
/// <returns>The in-memory orchestration service instance.</returns>
94+
public static InMemoryOrchestrationService GetInMemoryOrchestrationService(this IServiceProvider services)
95+
{
96+
return services.GetRequiredService<InMemoryOrchestrationService>();
97+
}
98+
}
99+
100+
/// <summary>
101+
/// Options for configuring in-memory durable task support.
102+
/// </summary>
103+
public class InMemoryDurableTaskOptions
104+
{
105+
/// <summary>
106+
/// Gets or sets the port for the internal gRPC server.
107+
/// If not set, a random port between 30000-40000 will be used.
108+
/// </summary>
109+
public int? Port { get; set; }
110+
}
111+
112+
/// <summary>
113+
/// Internal hosted service that runs the gRPC sidecar within the user's host.
114+
/// </summary>
115+
sealed class InMemoryGrpcSidecarHost : IHostedService, IAsyncDisposable
116+
{
117+
readonly string address;
118+
readonly InMemoryOrchestrationService orchestrationService;
119+
readonly ILoggerFactory? loggerFactory;
120+
IHost? inMemorySidecarHost;
121+
122+
public InMemoryGrpcSidecarHost(
123+
string address,
124+
InMemoryOrchestrationService orchestrationService,
125+
ILoggerFactory? loggerFactory)
126+
{
127+
this.address = address;
128+
this.orchestrationService = orchestrationService;
129+
this.loggerFactory = loggerFactory;
130+
}
131+
132+
public async Task StartAsync(CancellationToken cancellationToken)
133+
{
134+
// Build and start the gRPC sidecar
135+
this.inMemorySidecarHost = Host.CreateDefaultBuilder()
136+
.ConfigureLogging(logging =>
137+
{
138+
logging.ClearProviders();
139+
if (this.loggerFactory != null)
140+
{
141+
logging.Services.AddSingleton(this.loggerFactory);
142+
}
143+
})
144+
.ConfigureWebHostDefaults(webBuilder =>
145+
{
146+
webBuilder.UseUrls(this.address);
147+
webBuilder.ConfigureKestrel(kestrelOptions =>
148+
{
149+
kestrelOptions.ConfigureEndpointDefaults(listenOptions =>
150+
listenOptions.Protocols = HttpProtocols.Http2);
151+
});
152+
153+
webBuilder.ConfigureServices(services =>
154+
{
155+
services.AddGrpc();
156+
// Use the SAME orchestration service instance
157+
services.AddSingleton<IOrchestrationService>(this.orchestrationService);
158+
services.AddSingleton<IOrchestrationServiceClient>(this.orchestrationService);
159+
services.AddSingleton<TaskHubGrpcServer>();
160+
});
161+
162+
webBuilder.Configure(app =>
163+
{
164+
app.UseRouting();
165+
app.UseEndpoints(endpoints =>
166+
{
167+
endpoints.MapGrpcService<TaskHubGrpcServer>();
168+
});
169+
});
170+
})
171+
.Build();
172+
173+
await this.inMemorySidecarHost.StartAsync(cancellationToken);
174+
}
175+
176+
public async Task StopAsync(CancellationToken cancellationToken)
177+
{
178+
if (this.inMemorySidecarHost != null)
179+
{
180+
await this.inMemorySidecarHost.StopAsync(cancellationToken);
181+
}
182+
}
183+
184+
public async ValueTask DisposeAsync()
185+
{
186+
if (this.inMemorySidecarHost != null)
187+
{
188+
await this.inMemorySidecarHost.StopAsync();
189+
this.inMemorySidecarHost.Dispose();
190+
}
191+
}
192+
}

0 commit comments

Comments
 (0)