diff --git a/Microsoft.DurableTask.sln b/Microsoft.DurableTask.sln
index 383f2e15..53f5f4bc 100644
--- a/Microsoft.DurableTask.sln
+++ b/Microsoft.DurableTask.sln
@@ -97,6 +97,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LargePayloadConsoleApp", "s
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureBlobPayloads", "src\Extensions\AzureBlobPayloads\AzureBlobPayloads.csproj", "{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InProcessTestHost", "src\InProcessTestHost\InProcessTestHost.csproj", "{5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InProcessTestHost.Tests", "test\InProcessTestHost.Tests\InProcessTestHost.Tests.csproj", "{B894780C-338F-475E-8E84-56AFA8197A06}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -251,6 +255,14 @@ Global
{D2779F32-A548-44F8-B60A-6AC018966C79}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D2779F32-A548-44F8-B60A-6AC018966C79}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D2779F32-A548-44F8-B60A-6AC018966C79}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B894780C-338F-475E-8E84-56AFA8197A06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B894780C-338F-475E-8E84-56AFA8197A06}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B894780C-338F-475E-8E84-56AFA8197A06}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B894780C-338F-475E-8E84-56AFA8197A06}.Release|Any CPU.Build.0 = Release|Any CPU
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -305,6 +317,8 @@ Global
{A89B766C-987F-4C9F-8937-D0AB9FE640C8} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
{100348B5-4D97-4A3F-B777-AB14F276F8FE} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
{D2779F32-A548-44F8-B60A-6AC018966C79} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
+ {5F1E1662-D2D1-4325-BFE3-6AE23A8A4D7E} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
+ {B894780C-338F-475E-8E84-56AFA8197A06} = {E5637F81-2FB9-4CD7-900D-455363B142A7}
{6EB9D002-62C8-D6C1-62A8-14C54CA6DBBC} = {EFF7632B-821E-4CFC-B4A0-ED4B24296B17}
{FE1DA748-D6DB-E168-BC42-6DBBCEAF229C} = {8AFC9781-F6F1-4696-BB4A-9ED7CA9D612B}
EndGlobalSection
diff --git a/src/InProcessTestHost/DurableTaskTestHost.cs b/src/InProcessTestHost/DurableTaskTestHost.cs
new file mode 100644
index 00000000..ec075b94
--- /dev/null
+++ b/src/InProcessTestHost/DurableTaskTestHost.cs
@@ -0,0 +1,168 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using DurableTask.Core;
+using Grpc.Net.Client;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.DurableTask.Client;
+using Microsoft.DurableTask.Testing.Sidecar;
+using Microsoft.DurableTask.Testing.Sidecar.Grpc;
+using Microsoft.DurableTask.Worker;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.DurableTask.Testing;
+
+///
+/// In-process test host for testing class-based durable task orchestrations and activities
+/// without requiring any external backend (Azure Storage, SQL, etc).
+///
+public sealed class DurableTaskTestHost : IAsyncDisposable
+{
+ readonly IWebHost sidecarHost;
+ readonly IHost workerHost;
+ readonly GrpcChannel grpcChannel;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The gRPC sidecar host.
+ /// The worker host.
+ /// The gRPC channel.
+ /// The durable task client.
+ public DurableTaskTestHost(IWebHost sidecarHost, IHost workerHost, GrpcChannel grpcChannel, DurableTaskClient client)
+ {
+ this.sidecarHost = sidecarHost;
+ this.workerHost = workerHost;
+ this.grpcChannel = grpcChannel;
+ this.Client = client;
+ }
+
+ ///
+ /// Gets the durable task client for scheduling and managing orchestrations.
+ ///
+ public DurableTaskClient Client { get; }
+
+ ///
+ /// Starts a new in-process test host with the specified orchestrators and activities.
+ ///
+ /// Action to configure the task registry by adding orchestrators and activities.
+ /// Optional configuration options.
+ /// Cancellation token.
+ /// A running test host ready to execute orchestrations.
+ public static async Task StartAsync(
+ Action registry,
+ DurableTaskTestHostOptions? options = null,
+ CancellationToken cancellationToken = default)
+ {
+ options ??= new DurableTaskTestHostOptions();
+
+ // Create in-memory orchestration service
+ var orchestrationService = new InMemoryOrchestrationService(options.LoggerFactory);
+
+ // Start gRPC sidecar server in-process
+ string address = options.Port.HasValue
+ ? $"http://localhost:{options.Port.Value}"
+ : $"http://localhost:{Random.Shared.Next(30000, 40000)}";
+
+ var sidecarHost = new WebHostBuilder()
+ .UseKestrel(kestrelOptions =>
+ {
+ // Configure for HTTP/2 (required for gRPC)
+ kestrelOptions.ConfigureEndpointDefaults(listenOptions =>
+ listenOptions.Protocols = HttpProtocols.Http2);
+ })
+ .UseUrls(address)
+ .ConfigureServices(services =>
+ {
+ services.AddGrpc();
+ services.AddSingleton(orchestrationService);
+ services.AddSingleton(orchestrationService);
+ services.AddSingleton();
+ })
+ .Configure(app =>
+ {
+ app.UseRouting();
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapGrpcService();
+ });
+ })
+ .Build();
+
+ sidecarHost.Start();
+ var grpcChannel = GrpcChannel.ForAddress(address);
+
+ // Create worker host with user's orchestrators and activities
+ var workerHost = Host.CreateDefaultBuilder()
+ .ConfigureLogging(logging =>
+ {
+ logging.ClearProviders();
+ if (options.LoggerFactory != null)
+ {
+ logging.Services.AddSingleton(options.LoggerFactory);
+ }
+ })
+ .ConfigureServices(services =>
+ {
+ // Register worker that connects to our in-process sidecar
+ services.AddDurableTaskWorker(builder =>
+ {
+ builder.UseGrpc(grpcChannel);
+ builder.AddTasks(registry);
+ });
+
+ // Register client that connects to the same sidecar
+ services.AddDurableTaskClient(builder =>
+ {
+ builder.UseGrpc(grpcChannel);
+ builder.RegisterDirectly();
+ });
+ })
+ .Build();
+
+ await workerHost.StartAsync(cancellationToken);
+
+ // Get the client from the worker host
+ var client = workerHost.Services.GetRequiredService();
+
+ return new DurableTaskTestHost(sidecarHost, workerHost, grpcChannel, client);
+ }
+
+ ///
+ /// Clean up all resources.
+ ///
+ /// A task representing the asynchronous dispose operation.
+ public async ValueTask DisposeAsync()
+ {
+ await this.workerHost.StopAsync();
+ this.workerHost.Dispose();
+
+ await this.grpcChannel.ShutdownAsync();
+ this.grpcChannel.Dispose();
+
+ await this.sidecarHost.StopAsync();
+ this.sidecarHost.Dispose();
+ }
+}
+
+///
+/// Configuration options for .
+///
+public class DurableTaskTestHostOptions
+{
+ ///
+ /// Gets or sets the specific port to use for the gRPC sidecar.
+ /// If not set, a random port between 30000-40000 will be used.
+ ///
+ public int? Port { get; set; }
+
+ ///
+ /// Gets or sets an optional logger factory for capturing logs during tests.
+ /// Null by default.
+ ///
+ public ILoggerFactory? LoggerFactory { get; set; }
+}
diff --git a/src/InProcessTestHost/InProcessTestHost.csproj b/src/InProcessTestHost/InProcessTestHost.csproj
new file mode 100644
index 00000000..8839ef0f
--- /dev/null
+++ b/src/InProcessTestHost/InProcessTestHost.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net6.0
+ Microsoft.DurableTask.Testing
+ Microsoft.DurableTask.InProcessTestHost
+ Microsoft.DurableTask.InProcessTestHost
+ 0.1.0-preview.1
+
+
+ $(NoWarn);CA1848
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/InProcessTestHost/README.md b/src/InProcessTestHost/README.md
new file mode 100644
index 00000000..754f556a
--- /dev/null
+++ b/src/InProcessTestHost/README.md
@@ -0,0 +1,43 @@
+# DurableTaskTestHost - Testing Durable Orchestrations In-Process
+
+`DurableTaskTestHost` is a simple API for testing your durable task orchestrations and activities **in-process** without requiring any external backend.
+
+Supports both **class-based** and **function-based** syntax.
+
+## Quick Start
+
+1. Configure options
+```csharp
+var options = new DurableTaskTestHostOptions
+{
+ Port = 31000, // Optional: specific port (random by default)
+ LoggerFactory = myLoggerFactory // Optional: pass logger factory for logging
+};
+
+```
+
+2. Register test orchestrations and activities.
+
+```csharp
+await using var testHost = await DurableTaskTestHost.StartAsync(registry =>
+{
+ // Class-based
+ registry.AddOrchestrator();
+ registry.AddActivity();
+
+ // Function-based
+ registry.AddOrchestratorFunc("MyFunc", (ctx, input) => Task.FromResult("done"));
+ registry.AddActivityFunc("MyActivity", (ctx, input) => Task.FromResult("result"));
+});
+
+```
+
+3. Test
+```csharp
+string instanceId = await testHost.Client.ScheduleNewOrchestrationInstanceAsync("MyOrchestrator");
+var result = await testHost.Client.WaitForInstanceCompletionAsync(instanceId);
+```
+ .
+## More Samples
+
+See [BasicOrchestrationTests.cs](../../test/InProcessTestHost.Tests/BasicOrchestrationTests.cs) for complete samples showing both class-syntax and function-syntax orchestrations.
diff --git a/test/Grpc.IntegrationTests/GrpcSidecar/AsyncManualResetEvent.cs b/src/InProcessTestHost/Sidecar/AsyncManualResetEvent.cs
similarity index 62%
rename from test/Grpc.IntegrationTests/GrpcSidecar/AsyncManualResetEvent.cs
rename to src/InProcessTestHost/Sidecar/AsyncManualResetEvent.cs
index b7cf6dc4..4589a584 100644
--- a/test/Grpc.IntegrationTests/GrpcSidecar/AsyncManualResetEvent.cs
+++ b/src/InProcessTestHost/Sidecar/AsyncManualResetEvent.cs
@@ -1,13 +1,20 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-namespace Microsoft.DurableTask.Sidecar;
+namespace Microsoft.DurableTask.Testing.Sidecar;
+///
+/// Helper class for fetching TaskHub events.
+///
class AsyncManualResetEvent
{
readonly object mutex = new();
TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Whether the event should start in the signaled state.
public AsyncManualResetEvent(bool isSignaled)
{
if (isSignaled)
@@ -16,6 +23,17 @@ public AsyncManualResetEvent(bool isSignaled)
}
}
+ ///
+ /// Gets a value indicating whether the event is in the signaled state.
+ ///
+ public bool IsSignaled => this.tcs.Task.IsCompleted;
+
+ ///
+ /// Waits for the event to be signaled with a timeout.
+ ///
+ /// The timeout duration.
+ /// The cancellation token.
+ /// True if the event was signaled, false if the timeout occurred.
public async Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToken)
{
Task delayTask = Task.Delay(timeout, cancellationToken);
@@ -29,11 +47,10 @@ public async Task WaitAsync(TimeSpan timeout, CancellationToken cancellati
return winner == waitTask;
}
- public bool IsSignaled => this.tcs.Task.IsCompleted;
-
///
/// Puts the event in the signaled state, unblocking any waiting threads.
///
+ /// True if result is set.
public bool Set()
{
lock (this.mutex)
diff --git a/test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/GrpcCreateSubOrchestrationAction.cs b/src/InProcessTestHost/Sidecar/Dispatcher/GrpcCreateSubOrchestrationAction.cs
similarity index 56%
rename from test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/GrpcCreateSubOrchestrationAction.cs
rename to src/InProcessTestHost/Sidecar/Dispatcher/GrpcCreateSubOrchestrationAction.cs
index ce916d7a..9c22e132 100644
--- a/test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/GrpcCreateSubOrchestrationAction.cs
+++ b/src/InProcessTestHost/Sidecar/Dispatcher/GrpcCreateSubOrchestrationAction.cs
@@ -4,9 +4,15 @@
using DurableTask.Core.Command;
using DurableTask.Core.Tracing;
-namespace Microsoft.DurableTask.Sidecar.Dispatcher;
+namespace Microsoft.DurableTask.Testing.Sidecar.Dispatcher;
+///
+/// Action for creating sub-orchestration.
+///
public class GrpcCreateSubOrchestrationAction : CreateSubOrchestrationAction
{
+ ///
+ /// Gets or sets distributed parent trace context.
+ ///
public DistributedTraceContext? ParentTraceContext { get; set; }
}
diff --git a/src/InProcessTestHost/Sidecar/Dispatcher/GrpcOrchestratorExecutionResult.cs b/src/InProcessTestHost/Sidecar/Dispatcher/GrpcOrchestratorExecutionResult.cs
new file mode 100644
index 00000000..77ff454d
--- /dev/null
+++ b/src/InProcessTestHost/Sidecar/Dispatcher/GrpcOrchestratorExecutionResult.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using DurableTask.Core;
+
+namespace Microsoft.DurableTask.Testing.Sidecar.Dispatcher;
+
+///
+/// Grpc orchestration execution result.
+///
+public class GrpcOrchestratorExecutionResult : OrchestratorExecutionResult
+{
+ ///
+ /// Gets or sets the orcehstration activity spanId.
+ ///
+ public string? OrchestrationActivitySpanId { get; set; }
+
+ ///
+ /// Gets or sets the orchestration activity start time.
+ ///
+ public DateTimeOffset? OrchestrationActivityStartTime { get; set; }
+}
diff --git a/src/InProcessTestHost/Sidecar/Dispatcher/GrpcScheduleTaskOrchestratorAction.cs b/src/InProcessTestHost/Sidecar/Dispatcher/GrpcScheduleTaskOrchestratorAction.cs
new file mode 100644
index 00000000..c7646f93
--- /dev/null
+++ b/src/InProcessTestHost/Sidecar/Dispatcher/GrpcScheduleTaskOrchestratorAction.cs
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using DurableTask.Core.Command;
+using DurableTask.Core.Tracing;
+
+namespace Microsoft.DurableTask.Testing.Sidecar.Dispatcher;
+
+///
+/// gRPC-specific implementation of ScheduleTaskOrchestratorAction that includes distributed tracing context.
+///
+public class GrpcScheduleTaskOrchestratorAction : ScheduleTaskOrchestratorAction
+{
+ ///
+ /// Gets or sets the parent trace context for distributed tracing.
+ ///
+ public DistributedTraceContext? ParentTraceContext { get; set; }
+}
diff --git a/src/InProcessTestHost/Sidecar/Dispatcher/GrpcSubOrchestrationInstanceCreatedEvent.cs b/src/InProcessTestHost/Sidecar/Dispatcher/GrpcSubOrchestrationInstanceCreatedEvent.cs
new file mode 100644
index 00000000..e502b369
--- /dev/null
+++ b/src/InProcessTestHost/Sidecar/Dispatcher/GrpcSubOrchestrationInstanceCreatedEvent.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Runtime.Serialization;
+using DurableTask.Core.History;
+using DurableTask.Core.Tracing;
+
+namespace Microsoft.DurableTask.Testing.Sidecar.Dispatcher;
+
+///
+/// gRPC-specific implementation of SubOrchestrationInstanceCreatedEvent that includes distributed tracing context.
+///
+public class GrpcSubOrchestrationInstanceCreatedEvent : SubOrchestrationInstanceCreatedEvent
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The event ID.
+ public GrpcSubOrchestrationInstanceCreatedEvent(int eventId)
+ : base(eventId)
+ {
+ }
+
+ ///
+ /// Gets or sets the parent trace context for distributed tracing.
+ ///
+ [DataMember]
+ public DistributedTraceContext? ParentTraceContext { get; set; }
+}
diff --git a/test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/ITaskExecutor.cs b/src/InProcessTestHost/Sidecar/Dispatcher/ITaskExecutor.cs
similarity index 91%
rename from test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/ITaskExecutor.cs
rename to src/InProcessTestHost/Sidecar/Dispatcher/ITaskExecutor.cs
index 2a8f144e..cfb2f76d 100644
--- a/test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/ITaskExecutor.cs
+++ b/src/InProcessTestHost/Sidecar/Dispatcher/ITaskExecutor.cs
@@ -1,11 +1,14 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using DurableTask.Core;
using DurableTask.Core.History;
-namespace Microsoft.DurableTask.Sidecar.Dispatcher;
+namespace Microsoft.DurableTask.Testing.Sidecar.Dispatcher;
+///
+/// Task Executor.
+///
interface ITaskExecutor
{
///
diff --git a/test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/ITrafficSignal.cs b/src/InProcessTestHost/Sidecar/Dispatcher/ITrafficSignal.cs
similarity index 78%
rename from test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/ITrafficSignal.cs
rename to src/InProcessTestHost/Sidecar/Dispatcher/ITrafficSignal.cs
index fa6a092b..b073fe65 100644
--- a/test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/ITrafficSignal.cs
+++ b/src/InProcessTestHost/Sidecar/Dispatcher/ITrafficSignal.cs
@@ -1,7 +1,7 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-namespace Microsoft.DurableTask.Sidecar.Dispatcher;
+namespace Microsoft.DurableTask.Testing.Sidecar.Dispatcher;
///
/// A simple primitive that can be used to block logical threads until some condition occurs.
@@ -9,12 +9,12 @@ namespace Microsoft.DurableTask.Sidecar.Dispatcher;
interface ITrafficSignal
{
///
- /// Provides a human-friendly reason for why the signal is in the "wait" state.
+ /// Gets a human-friendly reason for why the signal is in the "wait" state.
///
string WaitReason { get; }
///
- /// Blocks the caller until the method is called.
+ /// Blocks the caller until the Set method is called.
///
/// The amount of time to wait until the signal is unblocked.
/// A cancellation token that can be used to interrupt a waiting caller.
@@ -26,4 +26,3 @@ interface ITrafficSignal
///
Task WaitAsync(TimeSpan waitTime, CancellationToken cancellationToken);
}
-
diff --git a/src/InProcessTestHost/Sidecar/Dispatcher/TaskActivityDispatcher.cs b/src/InProcessTestHost/Sidecar/Dispatcher/TaskActivityDispatcher.cs
new file mode 100644
index 00000000..bb728255
--- /dev/null
+++ b/src/InProcessTestHost/Sidecar/Dispatcher/TaskActivityDispatcher.cs
@@ -0,0 +1,110 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using DurableTask.Core;
+using DurableTask.Core.History;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.DurableTask.Testing.Sidecar.Dispatcher;
+
+///
+/// Dispatches and manages the execution of instances.
+///
+class TaskActivityDispatcher : WorkItemDispatcher
+{
+ readonly IOrchestrationService service;
+ readonly ITaskExecutor taskExecutor;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger used for diagnostic output.
+ /// A signal used to control dispatcher activity.
+ /// The orchestration service that manages task activity work items.
+ /// The task executor responsible for running activity code.
+ public TaskActivityDispatcher(ILogger log, ITrafficSignal trafficSignal, IOrchestrationService service, ITaskExecutor taskExecutor)
+ : base(log, trafficSignal)
+ {
+ this.service = service;
+ this.taskExecutor = taskExecutor;
+ }
+
+ ///
+ /// Gets the maximum number of concurrent work items allowed by the underlying orchestration service.
+ ///
+ public override int MaxWorkItems => this.service.MaxConcurrentTaskActivityWorkItems;
+
+ ///
+ /// Abandons a task activity work item, releasing any associated locks.
+ ///
+ /// The work item to abandon.
+ /// A task that represents the asynchronous operation.
+ public override Task AbandonWorkItemAsync(TaskActivityWorkItem workItem) =>
+ this.service.AbandonTaskActivityWorkItemAsync(workItem);
+
+ ///
+ /// Attempts to fetch the next available from the orchestration service.
+ ///
+ /// The maximum duration to wait for an available work item.
+ /// A token to signal operation cancellation.
+ ///
+ /// A task that returns the locked work item, or null if none is available.
+ ///
+ public override Task FetchWorkItemAsync(TimeSpan timeout, CancellationToken cancellationToken) =>
+ this.service.LockNextTaskActivityWorkItem(timeout, cancellationToken);
+
+ ///
+ /// Determines the delay, in seconds, to wait before retrying after a fetch exception occurs.
+ ///
+ /// The exception that occurred during fetch.
+ /// The delay duration in seconds before the next fetch attempt.
+ public override int GetDelayInSecondsOnFetchException(Exception ex) =>
+ this.service.GetDelayInSecondsAfterOnFetchException(ex);
+
+ ///
+ /// Retrieves a unique identifier for the specified work item.
+ ///
+ /// The work item for which to retrieve the ID.
+ /// The unique identifier for the work item.
+ public override string GetWorkItemId(TaskActivityWorkItem workItem) => workItem.Id;
+
+ ///
+ /// Not Implemented.
+ ///
+ /// The work item to release.
+ /// A completed task.
+ public override Task ReleaseWorkItemAsync(TaskActivityWorkItem workItem) => Task.CompletedTask;
+
+ ///
+ /// Renews the lock on the specified task activity work item to prevent expiration.
+ ///
+ /// The work item to renew.
+ ///
+ /// A task that returns the renewed work item upon successful lock renewal.
+ ///
+ public override Task RenewWorkItemAsync(TaskActivityWorkItem workItem) =>
+ this.service.RenewTaskActivityWorkItemLockAsync(workItem);
+
+ ///
+ /// Executes the specified using the configured .
+ ///
+ /// The work item to execute.
+ /// A task that represents the asynchronous operation.
+ protected override async Task ExecuteWorkItemAsync(TaskActivityWorkItem workItem)
+ {
+ TaskScheduledEvent scheduledEvent = (TaskScheduledEvent)workItem.TaskMessage.Event;
+
+ // TODO: Error handling for internal errors (user code exceptions are handled by the executor).
+ ActivityExecutionResult result = await this.taskExecutor.ExecuteActivity(
+ instance: workItem.TaskMessage.OrchestrationInstance,
+ activityEvent: scheduledEvent);
+
+ TaskMessage responseMessage = new()
+ {
+ Event = result.ResponseEvent,
+ OrchestrationInstance = workItem.TaskMessage.OrchestrationInstance,
+ };
+
+ await this.service.CompleteTaskActivityWorkItemAsync(workItem, responseMessage);
+ }
+}
diff --git a/test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/TaskHubDispatcherHost.cs b/src/InProcessTestHost/Sidecar/Dispatcher/TaskHubDispatcherHost.cs
similarity index 76%
rename from test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/TaskHubDispatcherHost.cs
rename to src/InProcessTestHost/Sidecar/Dispatcher/TaskHubDispatcherHost.cs
index 89a54d02..6946fc0f 100644
--- a/test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/TaskHubDispatcherHost.cs
+++ b/src/InProcessTestHost/Sidecar/Dispatcher/TaskHubDispatcherHost.cs
@@ -1,12 +1,12 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using DurableTask.Core;
using Microsoft.Extensions.Logging;
-namespace Microsoft.DurableTask.Sidecar.Dispatcher;
+namespace Microsoft.DurableTask.Testing.Sidecar.Dispatcher;
-class TaskHubDispatcherHost
+class TaskHubDispatcherHost : IDisposable
{
readonly TaskOrchestrationDispatcher orchestrationDispatcher;
readonly TaskActivityDispatcher activityDispatcher;
@@ -23,8 +23,8 @@ public TaskHubDispatcherHost(
this.orchestrationService = orchestrationService ?? throw new ArgumentNullException(nameof(orchestrationService));
this.log = loggerFactory.CreateLogger("Microsoft.DurableTask.Sidecar");
- this.orchestrationDispatcher = new TaskOrchestrationDispatcher(log, trafficSignal, orchestrationService, taskExecutor);
- this.activityDispatcher = new TaskActivityDispatcher(log, trafficSignal, orchestrationService, taskExecutor);
+ this.orchestrationDispatcher = new TaskOrchestrationDispatcher(this.log, trafficSignal, orchestrationService, taskExecutor);
+ this.activityDispatcher = new TaskActivityDispatcher(this.log, trafficSignal, orchestrationService, taskExecutor);
}
public async Task StartAsync(CancellationToken cancellationToken)
@@ -48,4 +48,12 @@ await Task.WhenAll(
// Tell the storage provider to stop doing any background work.
await this.orchestrationService.StopAsync();
}
+
+ public void Dispose()
+ {
+ // Dispose owned disposable resources
+ this.activityDispatcher?.Dispose();
+ this.orchestrationDispatcher?.Dispose();
+ }
}
+
diff --git a/test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/TaskOrchestrationDispatcher.cs b/src/InProcessTestHost/Sidecar/Dispatcher/TaskOrchestrationDispatcher.cs
similarity index 81%
rename from test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/TaskOrchestrationDispatcher.cs
rename to src/InProcessTestHost/Sidecar/Dispatcher/TaskOrchestrationDispatcher.cs
index 3b16d1c4..f19ca532 100644
--- a/test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/TaskOrchestrationDispatcher.cs
+++ b/src/InProcessTestHost/Sidecar/Dispatcher/TaskOrchestrationDispatcher.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Text;
@@ -7,14 +7,24 @@
using DurableTask.Core.History;
using Microsoft.Extensions.Logging;
-namespace Microsoft.DurableTask.Sidecar.Dispatcher;
+namespace Microsoft.DurableTask.Testing.Sidecar.Dispatcher;
+///
+/// Dispatches and manages the execution of instances.
+///
class TaskOrchestrationDispatcher : WorkItemDispatcher
{
readonly ILogger log;
readonly IOrchestrationService service;
readonly ITaskExecutor taskExecutor;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger used for diagnostic output.
+ /// A signal used to control dispatcher activity and flow.
+ /// The orchestration service used to fetch and manage orchestration work items.
+ /// The task executor used for executing orchestration tasks.
public TaskOrchestrationDispatcher(ILogger log, ITrafficSignal trafficSignal, IOrchestrationService service, ITaskExecutor taskExecutor)
: base(log, trafficSignal)
{
@@ -23,14 +33,70 @@ public TaskOrchestrationDispatcher(ILogger log, ITrafficSignal trafficSignal, IO
this.taskExecutor = taskExecutor;
}
+ ///
+ /// Gets the maximum number of concurrent orchestration work items allowed by the orchestration service.
+ ///
public override int MaxWorkItems => this.service.MaxConcurrentTaskOrchestrationWorkItems;
+ ///
+ /// Abandons the specified orchestration work item, releasing any held locks or resources.
+ ///
+ /// The orchestration work item to abandon.
+ /// A task that represents the asynchronous abandon operation.
public override Task AbandonWorkItemAsync(TaskOrchestrationWorkItem workItem) =>
this.service.AbandonTaskOrchestrationWorkItemAsync(workItem);
+ ///
+ /// Attempts to fetch the next available from the orchestration service.
+ ///
+ /// The maximum duration to wait for an available orchestration work item.
+ /// A token to signal cancellation of the fetch operation.
+ ///
+ /// A task that returns the locked orchestration work item if available, or null if no items are ready.
+ ///
public override Task FetchWorkItemAsync(TimeSpan timeout, CancellationToken cancellationToken) =>
this.service.LockNextTaskOrchestrationWorkItemAsync(timeout, cancellationToken);
+ ///
+ /// Determines the delay, in seconds, before retrying after a fetch exception.
+ ///
+ /// The exception that occurred while fetching work.
+ /// The number of seconds to delay before retrying.
+ public override int GetDelayInSecondsOnFetchException(Exception ex) =>
+ this.service.GetDelayInSecondsAfterOnFetchException(ex);
+
+ ///
+ /// Get work item id.
+ ///
+ /// The orchestration work item.
+ /// Work item id.
+ public override string GetWorkItemId(TaskOrchestrationWorkItem workItem) => workItem.InstanceId;
+
+ ///
+ /// Releases the specified orchestration work item.
+ ///
+ /// The work item to release.
+ /// A task that completes when the release is finished.
+ public override Task ReleaseWorkItemAsync(TaskOrchestrationWorkItem workItem) =>
+ this.service.ReleaseTaskOrchestrationWorkItemAsync(workItem);
+
+ ///
+ /// Renew work item.
+ ///
+ /// The work item to be renewed.
+ /// Work item renewed.
+ public override async Task RenewWorkItemAsync(TaskOrchestrationWorkItem workItem)
+ {
+ await this.service.RenewTaskOrchestrationWorkItemLockAsync(workItem);
+ return workItem;
+ }
+
+ ///
+ /// Execute the work item.
+ ///
+ /// Work item to execute.
+ /// Completed task.
+ /// Throw when work item doesn't contain required message.
protected override async Task ExecuteWorkItemAsync(TaskOrchestrationWorkItem workItem)
{
// Convert the new messages into new history events
@@ -72,9 +138,9 @@ protected override async Task ExecuteWorkItemAsync(TaskOrchestrationWorkItem wor
this.ApplyOrchestratorActions(
result,
ref workItem.OrchestrationRuntimeState,
- out IList activityMessages,
- out IList orchestratorMessages,
- out IList timerMessages,
+ out List activityMessages,
+ out List orchestratorMessages,
+ out List timerMessages,
out OrchestrationState? updatedStatus,
out bool continueAsNew);
if (continueAsNew)
@@ -166,12 +232,24 @@ static int GetSortOrderWithinGroup(TaskMessage msg)
}
}
+ static string GetShortHistoryEventDescription(HistoryEvent e)
+ {
+ if (Utils.TryGetTaskScheduledId(e, out int taskScheduledId))
+ {
+ return $"{e.EventType}#{taskScheduledId}";
+ }
+ else
+ {
+ return e.EventType.ToString();
+ }
+ }
+
void ApplyOrchestratorActions(
OrchestratorExecutionResult result,
ref OrchestrationRuntimeState runtimeState,
- out IList activityMessages,
- out IList orchestratorMessages,
- out IList timerMessages,
+ out List activityMessages, // CA1859: Use concrete types for better performance
+ out List orchestratorMessages, // CA1859: Use concrete types for better performance
+ out List timerMessages, // CA1859: Use concrete types for better performance
out OrchestrationState? updatedStatus,
out bool continueAsNew)
{
@@ -180,9 +258,9 @@ void ApplyOrchestratorActions(
throw new ArgumentException($"The provided {nameof(OrchestrationRuntimeState)} doesn't contain an instance ID!", nameof(runtimeState));
}
- IList? newActivityMessages = null;
- IList? newTimerMessages = null;
- IList? newOrchestratorMessages = null;
+ List? newActivityMessages = null; // CA1859: Use concrete types for better performance
+ List? newTimerMessages = null; // CA1859: Use concrete types for better performance
+ List? newOrchestratorMessages = null; // CA1859: Use concrete types for better performance
FailureDetails? failureDetails = null;
continueAsNew = false;
@@ -245,7 +323,7 @@ void ApplyOrchestratorActions(
Version = subOrchestrationAction.Version,
InstanceId = subOrchestrationAction.InstanceId,
Input = subOrchestrationAction.Input,
- ParentTraceContext = grpcAction?.ParentTraceContext
+ ParentTraceContext = grpcAction?.ParentTraceContext,
});
ExecutionStartedEvent startedEvent = new(-1, subOrchestrationAction.Input)
@@ -315,7 +393,7 @@ void ApplyOrchestratorActions(
Tags = runtimeState.Tags,
ParentInstance = runtimeState.ParentInstance,
Name = runtimeState.Name,
- Version = completeAction.NewVersion ?? runtimeState.Version
+ Version = completeAction.NewVersion ?? runtimeState.Version,
});
newRuntimeState.Status = runtimeState.Status;
@@ -330,9 +408,9 @@ void ApplyOrchestratorActions(
}
runtimeState = newRuntimeState;
- activityMessages = Array.Empty();
- orchestratorMessages = Array.Empty();
- timerMessages = Array.Empty();
+ activityMessages = new List();
+ orchestratorMessages = new List();
+ timerMessages = new List();
continueAsNew = true;
updatedStatus = null;
return;
@@ -397,9 +475,9 @@ void ApplyOrchestratorActions(
runtimeState.AddEvent(new OrchestratorCompletedEvent(-1));
- activityMessages = newActivityMessages ?? Array.Empty();
- timerMessages = newTimerMessages ?? Array.Empty();
- orchestratorMessages = newOrchestratorMessages ?? Array.Empty();
+ activityMessages = newActivityMessages ?? new List();
+ timerMessages = newTimerMessages ?? new List();
+ orchestratorMessages = newOrchestratorMessages ?? new List();
updatedStatus = new OrchestrationState
{
@@ -421,30 +499,4 @@ void ApplyOrchestratorActions(
FailureDetails = failureDetails,
};
}
-
- static string GetShortHistoryEventDescription(HistoryEvent e)
- {
- if (Utils.TryGetTaskScheduledId(e, out int taskScheduledId))
- {
- return $"{e.EventType}#{taskScheduledId}";
- }
- else
- {
- return e.EventType.ToString();
- }
- }
-
- public override int GetDelayInSecondsOnFetchException(Exception ex) =>
- this.service.GetDelayInSecondsAfterOnFetchException(ex);
-
- public override string GetWorkItemId(TaskOrchestrationWorkItem workItem) => workItem.InstanceId;
-
- public override Task ReleaseWorkItemAsync(TaskOrchestrationWorkItem workItem) =>
- this.service.ReleaseTaskOrchestrationWorkItemAsync(workItem);
-
- public override async Task RenewWorkItemAsync(TaskOrchestrationWorkItem workItem)
- {
- await this.service.RenewTaskOrchestrationWorkItemLockAsync(workItem);
- return workItem;
- }
}
diff --git a/test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/WorkItemDispatcher.cs b/src/InProcessTestHost/Sidecar/Dispatcher/WorkItemDispatcher.cs
similarity index 71%
rename from test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/WorkItemDispatcher.cs
rename to src/InProcessTestHost/Sidecar/Dispatcher/WorkItemDispatcher.cs
index 3a9039bb..d026bc3b 100644
--- a/test/Grpc.IntegrationTests/GrpcSidecar/Dispatcher/WorkItemDispatcher.cs
+++ b/src/InProcessTestHost/Sidecar/Dispatcher/WorkItemDispatcher.cs
@@ -1,14 +1,18 @@
-// Copyright (c) Microsoft Corporation.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Diagnostics;
using Microsoft.Extensions.Logging;
-namespace Microsoft.DurableTask.Sidecar.Dispatcher;
+namespace Microsoft.DurableTask.Testing.Sidecar.Dispatcher;
-abstract class WorkItemDispatcher where T : class
+///
+/// Base class for dispatching and managing work items.
+///
+/// The type of work item to dispatch.
+abstract class WorkItemDispatcher : IDisposable where T : class
{
- static int nextDispatcherId = 0;
+ static int nextDispatcherId; // CA1805: Remove explicit initialization
readonly string name;
readonly ILogger log;
@@ -17,7 +21,13 @@ abstract class WorkItemDispatcher where T : class
CancellationTokenSource? shutdownTcs;
Task? workItemExecuteLoop;
int currentWorkItems;
+ bool disposed;
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger.
+ /// The traffic signal.
public WorkItemDispatcher(ILogger log, ITrafficSignal trafficSignal)
{
this.log = log ?? throw new ArgumentNullException(nameof(log));
@@ -26,22 +36,66 @@ public WorkItemDispatcher(ILogger log, ITrafficSignal trafficSignal)
this.name = $"{this.GetType().Name}-{Interlocked.Increment(ref nextDispatcherId)}";
}
+ ///
+ /// Gets the maximum number of concurrent work items.
+ ///
public virtual int MaxWorkItems => 10;
+ ///
+ /// Fetches the next work item asynchronously.
+ ///
+ /// The timeout for the operation.
+ /// The cancellation token.
+ /// The next work item, or null if none available.
public abstract Task FetchWorkItemAsync(TimeSpan timeout, CancellationToken cancellationToken);
+ ///
+ /// Executes the specified work item.
+ ///
+ /// The work item to execute.
+ /// A task representing the execution.
protected abstract Task ExecuteWorkItemAsync(T workItem);
+ ///
+ /// Releases the specified work item.
+ ///
+ /// The work item to release.
+ /// A task representing the release operation.
public abstract Task ReleaseWorkItemAsync(T workItem);
+ ///
+ /// Abandons the specified work item.
+ ///
+ /// The work item to abandon.
+ /// A task representing the abandon operation.
public abstract Task AbandonWorkItemAsync(T workItem);
+ ///
+ /// Renews the specified work item.
+ ///
+ /// The work item to renew.
+ /// The renewed work item.
public abstract Task RenewWorkItemAsync(T workItem);
+ ///
+ /// Gets the ID of the specified work item.
+ ///
+ /// The work item.
+ /// The work item ID.
public abstract string GetWorkItemId(T workItem);
+ ///
+ /// Gets the delay in seconds after a fetch exception.
+ ///
+ /// The exception that occurred.
+ /// The delay in seconds.
public abstract int GetDelayInSecondsOnFetchException(Exception ex);
+ ///
+ /// Starts the dispatcher.
+ ///
+ /// The cancellation token.
+ /// A task representing the start operation.
public virtual Task StartAsync(CancellationToken cancellationToken)
{
// Dispatchers can be stopped and started back up again
@@ -55,6 +109,11 @@ public virtual Task StartAsync(CancellationToken cancellationToken)
return Task.CompletedTask;
}
+ ///
+ /// Stops the dispatcher.
+ ///
+ /// The cancellation token.
+ /// A task representing the stop operation.
public virtual async Task StopAsync(CancellationToken cancellationToken)
{
// Trigger the cancellation tokens being used for background processing.
@@ -71,6 +130,28 @@ public virtual async Task StopAsync(CancellationToken cancellationToken)
await this.WaitForOutstandingWorkItems(cancellationToken);
}
+ ///
+ /// Disposes the dispatcher resources.
+ ///
+ public void Dispose()
+ {
+ this.Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Disposes the dispatcher resources.
+ ///
+ /// Whether disposing from Dispose method.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!this.disposed && disposing)
+ {
+ this.shutdownTcs?.Dispose();
+ this.disposed = true;
+ }
+ }
+
async Task WaitForAllClear(CancellationToken cancellationToken)
{
TimeSpan logInterval = TimeSpan.FromMinutes(1);
@@ -128,11 +209,12 @@ async Task WaitForOutstandingWorkItems(CancellationToken cancellationToken)
}
// This method does not throw
+ // CA1068: CancellationToken should be last parameter
async Task DelayOnException(
Exception exception,
string workItemId,
- CancellationToken cancellationToken,
- Func delayInSecondsPolicy)
+ Func delayInSecondsPolicy,
+ CancellationToken cancellationToken)
{
try
{
@@ -208,7 +290,7 @@ async Task FetchAndExecuteLoop(CancellationToken cancellationToken)
action: "fetchWorkItem",
workItemId: unknownWorkItemId,
details: ex.ToString());
- await this.DelayOnException(ex, unknownWorkItemId, cancellationToken, this.GetDelayInSecondsOnFetchException);
+ await this.DelayOnException(ex, unknownWorkItemId, this.GetDelayInSecondsOnFetchException, cancellationToken);
continue;
}
}
diff --git a/test/Grpc.IntegrationTests/GrpcSidecar/Grpc/ProtobufUtils.cs b/src/InProcessTestHost/Sidecar/Grpc/ProtobufUtils.cs
similarity index 88%
rename from test/Grpc.IntegrationTests/GrpcSidecar/Grpc/ProtobufUtils.cs
rename to src/InProcessTestHost/Sidecar/Grpc/ProtobufUtils.cs
index 86b98030..fe4e093c 100644
--- a/test/Grpc.IntegrationTests/GrpcSidecar/Grpc/ProtobufUtils.cs
+++ b/src/InProcessTestHost/Sidecar/Grpc/ProtobufUtils.cs
@@ -1,547 +1,598 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-using System.Buffers;
-using System.Collections;
-using System.Globalization;
-using System.Text.Json;
-using DurableTask.Core;
-using DurableTask.Core.Command;
-using DurableTask.Core.History;
-using DurableTask.Core.Query;
-using DurableTask.Core.Tracing;
-using Google.Protobuf;
-using Google.Protobuf.Collections;
-using Google.Protobuf.WellKnownTypes;
-using Microsoft.DurableTask.Sidecar.Dispatcher;
-using Proto = Microsoft.DurableTask.Protobuf;
-
-namespace Microsoft.DurableTask.Sidecar.Grpc;
-
-public static class ProtobufUtils
-{
- public static Proto.HistoryEvent ToHistoryEventProto(HistoryEvent e)
- {
- var payload = new Proto.HistoryEvent()
- {
- EventId = e.EventId,
- Timestamp = Timestamp.FromDateTime(e.Timestamp),
- };
-
- switch (e.EventType)
- {
- case EventType.ContinueAsNew:
- var continueAsNew = (ContinueAsNewEvent)e;
- payload.ContinueAsNew = new Proto.ContinueAsNewEvent
- {
- Input = continueAsNew.Result,
- };
- break;
- case EventType.EventRaised:
- var eventRaised = (EventRaisedEvent)e;
- payload.EventRaised = new Proto.EventRaisedEvent
- {
- Name = eventRaised.Name,
- Input = eventRaised.Input,
- };
- break;
- case EventType.EventSent:
- var eventSent = (EventSentEvent)e;
- payload.EventSent = new Proto.EventSentEvent
- {
- Name = eventSent.Name,
- Input = eventSent.Input,
- InstanceId = eventSent.InstanceId,
- };
- break;
- case EventType.ExecutionCompleted:
- var completedEvent = (ExecutionCompletedEvent)e;
- payload.ExecutionCompleted = new Proto.ExecutionCompletedEvent
- {
- OrchestrationStatus = Proto.OrchestrationStatus.Completed,
- Result = completedEvent.Result,
- };
- break;
- case EventType.ExecutionFailed:
- var failedEvent = (ExecutionCompletedEvent)e;
- payload.ExecutionCompleted = new Proto.ExecutionCompletedEvent
- {
- OrchestrationStatus = Proto.OrchestrationStatus.Failed,
- Result = failedEvent.Result,
- };
- break;
- case EventType.ExecutionStarted:
- // Start of a new orchestration instance
- var startedEvent = (ExecutionStartedEvent)e;
- startedEvent.Tags ??= new Dictionary();
- payload.ExecutionStarted = new Proto.ExecutionStartedEvent
- {
- Name = startedEvent.Name,
- Version = startedEvent.Version,
- Input = startedEvent.Input,
- Tags = { startedEvent.Tags },
- OrchestrationInstance = new Proto.OrchestrationInstance
- {
- InstanceId = startedEvent.OrchestrationInstance.InstanceId,
- ExecutionId = startedEvent.OrchestrationInstance.ExecutionId,
- },
- ParentInstance = startedEvent.ParentInstance == null ? null : new Proto.ParentInstanceInfo
- {
- Name = startedEvent.ParentInstance.Name,
- Version = startedEvent.ParentInstance.Version,
- TaskScheduledId = startedEvent.ParentInstance.TaskScheduleId,
- OrchestrationInstance = new Proto.OrchestrationInstance
- {
- InstanceId = startedEvent.ParentInstance.OrchestrationInstance.InstanceId,
- ExecutionId = startedEvent.ParentInstance.OrchestrationInstance.ExecutionId,
- },
- },
- ScheduledStartTimestamp = startedEvent.ScheduledStartTime == null ? null : Timestamp.FromDateTime(startedEvent.ScheduledStartTime.Value),
- ParentTraceContext = startedEvent.ParentTraceContext is null ? null : new Proto.TraceContext
- {
- TraceParent = startedEvent.ParentTraceContext.TraceParent,
- TraceState = startedEvent.ParentTraceContext.TraceState,
- }
- };
- break;
- case EventType.ExecutionTerminated:
- var terminatedEvent = (ExecutionTerminatedEvent)e;
- payload.ExecutionTerminated = new Proto.ExecutionTerminatedEvent
- {
- Input = terminatedEvent.Input,
- };
- break;
- case EventType.TaskScheduled:
- var taskScheduledEvent = (TaskScheduledEvent)e;
- payload.TaskScheduled = new Proto.TaskScheduledEvent
- {
- Name = taskScheduledEvent.Name,
- Version = taskScheduledEvent.Version,
- Input = taskScheduledEvent.Input,
- ParentTraceContext = taskScheduledEvent.ParentTraceContext is null ? null : new Proto.TraceContext
- {
- TraceParent = taskScheduledEvent.ParentTraceContext.TraceParent,
- TraceState = taskScheduledEvent.ParentTraceContext.TraceState,
- },
- };
- break;
- case EventType.TaskCompleted:
- var taskCompletedEvent = (TaskCompletedEvent)e;
- payload.TaskCompleted = new Proto.TaskCompletedEvent
- {
- Result = taskCompletedEvent.Result,
- TaskScheduledId = taskCompletedEvent.TaskScheduledId,
- };
- break;
- case EventType.TaskFailed:
- var taskFailedEvent = (TaskFailedEvent)e;
- payload.TaskFailed = new Proto.TaskFailedEvent
- {
- FailureDetails = GetFailureDetails(taskFailedEvent.FailureDetails),
- TaskScheduledId = taskFailedEvent.TaskScheduledId,
- };
- break;
- case EventType.SubOrchestrationInstanceCreated:
- var subOrchestrationCreated = (SubOrchestrationInstanceCreatedEvent)e;
- payload.SubOrchestrationInstanceCreated = new Proto.SubOrchestrationInstanceCreatedEvent
- {
- Input = subOrchestrationCreated.Input,
- InstanceId = subOrchestrationCreated.InstanceId,
- Name = subOrchestrationCreated.Name,
- Version = subOrchestrationCreated.Version,
- };
-
- if (subOrchestrationCreated is GrpcSubOrchestrationInstanceCreatedEvent { ParentTraceContext: not null } grpcEvent)
- {
- payload.SubOrchestrationInstanceCreated.ParentTraceContext = new Proto.TraceContext
- {
- TraceParent = grpcEvent.ParentTraceContext.TraceParent,
- TraceState = grpcEvent.ParentTraceContext.TraceState,
- };
- }
-
- break;
- case EventType.SubOrchestrationInstanceCompleted:
- var subOrchestrationCompleted = (SubOrchestrationInstanceCompletedEvent)e;
- payload.SubOrchestrationInstanceCompleted = new Proto.SubOrchestrationInstanceCompletedEvent
- {
- Result = subOrchestrationCompleted.Result,
- TaskScheduledId = subOrchestrationCompleted.TaskScheduledId,
- };
- break;
- case EventType.SubOrchestrationInstanceFailed:
- var subOrchestrationFailed = (SubOrchestrationInstanceFailedEvent)e;
- payload.SubOrchestrationInstanceFailed = new Proto.SubOrchestrationInstanceFailedEvent
- {
- FailureDetails = GetFailureDetails(subOrchestrationFailed.FailureDetails),
- TaskScheduledId = subOrchestrationFailed.TaskScheduledId,
- };
- break;
- case EventType.TimerCreated:
- var timerCreatedEvent = (TimerCreatedEvent)e;
- payload.TimerCreated = new Proto.TimerCreatedEvent
- {
- FireAt = Timestamp.FromDateTime(timerCreatedEvent.FireAt),
- };
- break;
- case EventType.TimerFired:
- var timerFiredEvent = (TimerFiredEvent)e;
- payload.TimerFired = new Proto.TimerFiredEvent
- {
- FireAt = Timestamp.FromDateTime(timerFiredEvent.FireAt),
- TimerId = timerFiredEvent.TimerId,
- };
- break;
- case EventType.OrchestratorStarted:
- // This event has no data
- payload.OrchestratorStarted = new Proto.OrchestratorStartedEvent();
- break;
- case EventType.OrchestratorCompleted:
- // This event has no data
- payload.OrchestratorCompleted = new Proto.OrchestratorCompletedEvent();
- break;
- case EventType.GenericEvent:
- var genericEvent = (GenericEvent)e;
- payload.GenericEvent = new Proto.GenericEvent
- {
- Data = genericEvent.Data,
- };
- break;
- case EventType.HistoryState:
- var historyStateEvent = (HistoryStateEvent)e;
- payload.HistoryState = new Proto.HistoryStateEvent
- {
- OrchestrationState = new Proto.OrchestrationState
- {
- InstanceId = historyStateEvent.State.OrchestrationInstance.InstanceId,
- Name = historyStateEvent.State.Name,
- Version = historyStateEvent.State.Version,
- Input = historyStateEvent.State.Input,
- Output = historyStateEvent.State.Output,
- ScheduledStartTimestamp = historyStateEvent.State.ScheduledStartTime == null ? null : Timestamp.FromDateTime(historyStateEvent.State.ScheduledStartTime.Value),
- CreatedTimestamp = Timestamp.FromDateTime(historyStateEvent.State.CreatedTime),
- LastUpdatedTimestamp = Timestamp.FromDateTime(historyStateEvent.State.LastUpdatedTime),
- OrchestrationStatus = (Proto.OrchestrationStatus)historyStateEvent.State.OrchestrationStatus,
- CustomStatus = historyStateEvent.State.Status,
- Tags = { historyStateEvent.State.Tags },
- },
- };
- break;
- case EventType.ExecutionSuspended:
- var suspendedEvent = (ExecutionSuspendedEvent)e;
- payload.ExecutionSuspended = new Proto.ExecutionSuspendedEvent
- {
- Input = suspendedEvent.Reason,
- };
- break;
- case EventType.ExecutionResumed:
- var resumedEvent = (ExecutionResumedEvent)e;
- payload.ExecutionResumed = new Proto.ExecutionResumedEvent
- {
- Input = resumedEvent.Reason,
- };
- break;
- default:
- throw new NotSupportedException($"Found unsupported history event '{e.EventType}'.");
- }
-
- return payload;
- }
-
- public static OrchestratorAction ToOrchestratorAction(Proto.OrchestratorAction a)
- {
- switch (a.OrchestratorActionTypeCase)
- {
- case Proto.OrchestratorAction.OrchestratorActionTypeOneofCase.ScheduleTask:
- return new GrpcScheduleTaskOrchestratorAction
- {
- Id = a.Id,
- Input = a.ScheduleTask.Input,
- Name = a.ScheduleTask.Name,
- Version = a.ScheduleTask.Version,
- ParentTraceContext = a.ScheduleTask.ParentTraceContext is not null
- ? new DistributedTraceContext(a.ScheduleTask.ParentTraceContext.TraceParent, a.ScheduleTask.ParentTraceContext.TraceState)
- : null,
- };
- case Proto.OrchestratorAction.OrchestratorActionTypeOneofCase.CreateSubOrchestration:
- return new GrpcCreateSubOrchestrationAction
- {
- Id = a.Id,
- Input = a.CreateSubOrchestration.Input,
- Name = a.CreateSubOrchestration.Name,
- InstanceId = a.CreateSubOrchestration.InstanceId,
- ParentTraceContext = a.CreateSubOrchestration.ParentTraceContext is not null
- ? new DistributedTraceContext(a.CreateSubOrchestration.ParentTraceContext.TraceParent, a.CreateSubOrchestration.ParentTraceContext.TraceState)
- : null,
- Tags = null, // TODO
- Version = a.CreateSubOrchestration.Version,
- };
- case Proto.OrchestratorAction.OrchestratorActionTypeOneofCase.CreateTimer:
- return new CreateTimerOrchestratorAction
- {
- Id = a.Id,
- FireAt = a.CreateTimer.FireAt.ToDateTime(),
- };
- case Proto.OrchestratorAction.OrchestratorActionTypeOneofCase.SendEvent:
- return new SendEventOrchestratorAction
- {
- Id = a.Id,
- Instance = new OrchestrationInstance
- {
- InstanceId = a.SendEvent.Instance.InstanceId,
- ExecutionId = a.SendEvent.Instance.ExecutionId,
- },
- EventName = a.SendEvent.Name,
- EventData = a.SendEvent.Data,
- };
- case Proto.OrchestratorAction.OrchestratorActionTypeOneofCase.CompleteOrchestration:
- var completedAction = a.CompleteOrchestration;
- var action = new OrchestrationCompleteOrchestratorAction
- {
- Id = a.Id,
- OrchestrationStatus = (OrchestrationStatus)completedAction.OrchestrationStatus,
- Result = completedAction.Result,
- Details = completedAction.Details,
- FailureDetails = GetFailureDetails(completedAction.FailureDetails),
- NewVersion = completedAction.NewVersion,
- };
-
- if (completedAction.CarryoverEvents?.Count > 0)
- {
- foreach (var e in completedAction.CarryoverEvents)
- {
- // Only raised events are supported for carryover
- if (e.EventRaised is Proto.EventRaisedEvent eventRaised)
- {
- action.CarryoverEvents.Add(new EventRaisedEvent(e.EventId, eventRaised.Input)
- {
- Name = eventRaised.Name,
- });
- }
-
- }
- }
-
- return action;
- default:
- throw new NotSupportedException($"Received unsupported action type '{a.OrchestratorActionTypeCase}'.");
- }
- }
-
- public static string Base64Encode(IMessage message)
- {
- // Create a serialized payload using lower-level protobuf APIs. We do this to avoid allocating
- // byte[] arrays for every request, which would otherwise put a heavy burden on the GC. Unfortunately
- // the protobuf API version we're using doesn't currently have memory-efficient serialization APIs.
- int messageSize = message.CalculateSize();
- byte[] rentedBuffer = ArrayPool.Shared.Rent(messageSize);
- try
- {
- using MemoryStream intermediateBufferStream = new(rentedBuffer, 0, messageSize);
- CodedOutputStream protobufOutputStream = new(intermediateBufferStream);
- protobufOutputStream.WriteRawMessage(message);
- protobufOutputStream.Flush();
- return Convert.ToBase64String(rentedBuffer, 0, messageSize);
- }
- finally
- {
- ArrayPool.Shared.Return(rentedBuffer);
- }
- }
-
- internal static FailureDetails? GetFailureDetails(Proto.TaskFailureDetails? failureDetails)
- {
- if (failureDetails == null)
- {
- return null;
- }
-
- return new FailureDetails(
- failureDetails.ErrorType,
- failureDetails.ErrorMessage,
- failureDetails.StackTrace,
- GetFailureDetails(failureDetails.InnerFailure),
- failureDetails.IsNonRetriable,
- ConvertMapToDictionary(failureDetails.Properties));
- }
-
- internal static Proto.TaskFailureDetails? GetFailureDetails(FailureDetails? failureDetails)
- {
- if (failureDetails == null)
- {
- return null;
- }
-
- var taskFailureDetails = new Proto.TaskFailureDetails
- {
- ErrorType = failureDetails.ErrorType,
- ErrorMessage = failureDetails.ErrorMessage,
- StackTrace = failureDetails.StackTrace,
- InnerFailure = GetFailureDetails(failureDetails.InnerFailure),
- IsNonRetriable = failureDetails.IsNonRetriable,
- };
-
- // Add properties if they exist
- if (failureDetails.Properties != null)
- {
- foreach (var kvp in failureDetails.Properties)
- {
- taskFailureDetails.Properties.Add(kvp.Key, ConvertObjectToValue(kvp.Value));
- }
- }
-
- return taskFailureDetails;
- }
-
- internal static OrchestrationQuery ToOrchestrationQuery(Proto.QueryInstancesRequest request)
- {
- var query = new OrchestrationQuery()
- {
- RuntimeStatus = request.Query.RuntimeStatus?.Select(status => (OrchestrationStatus)status).ToList(),
- CreatedTimeFrom = request.Query.CreatedTimeFrom?.ToDateTime(),
- CreatedTimeTo = request.Query.CreatedTimeTo?.ToDateTime(),
- TaskHubNames = request.Query.TaskHubNames,
- PageSize = request.Query.MaxInstanceCount,
- ContinuationToken = request.Query.ContinuationToken,
- InstanceIdPrefix = request.Query.InstanceIdPrefix,
- FetchInputsAndOutputs = request.Query.FetchInputsAndOutputs,
- };
-
- return query;
- }
-
- internal static Proto.QueryInstancesResponse CreateQueryInstancesResponse(OrchestrationQueryResult result, Proto.QueryInstancesRequest request)
- {
- Proto.QueryInstancesResponse response = new Proto.QueryInstancesResponse
- {
- ContinuationToken = result.ContinuationToken
- };
- foreach (OrchestrationState state in result.OrchestrationState)
- {
- var orchestrationState = new Proto.OrchestrationState
- {
- InstanceId = state.OrchestrationInstance.InstanceId,
- Name = state.Name,
- Version = state.Version,
- Input = state.Input,
- Output = state.Output,
- ScheduledStartTimestamp = state.ScheduledStartTime == null ? null : Timestamp.FromDateTime(state.ScheduledStartTime.Value),
- CreatedTimestamp = Timestamp.FromDateTime(state.CreatedTime),
- LastUpdatedTimestamp = Timestamp.FromDateTime(state.LastUpdatedTime),
- OrchestrationStatus = (Proto.OrchestrationStatus)state.OrchestrationStatus,
- CustomStatus = state.Status,
- };
- response.OrchestrationState.Add(orchestrationState);
- }
- return response;
- }
-
- internal static PurgeInstanceFilter ToPurgeInstanceFilter(Proto.PurgeInstancesRequest request)
- {
- var purgeInstanceFilter = new PurgeInstanceFilter(
- request.PurgeInstanceFilter.CreatedTimeFrom.ToDateTime(),
- request.PurgeInstanceFilter.CreatedTimeTo?.ToDateTime(),
- request.PurgeInstanceFilter.RuntimeStatus?.Select(status => (OrchestrationStatus)status).ToList()
- );
- return purgeInstanceFilter;
- }
-
- internal static Proto.PurgeInstancesResponse CreatePurgeInstancesResponse(PurgeResult result)
- {
- Proto.PurgeInstancesResponse response = new Proto.PurgeInstancesResponse
- {
- DeletedInstanceCount = result.DeletedInstanceCount
- };
- return response;
- }
-
- ///
- /// Converts a MapField into a IDictionary.
- ///
- ///
- ///
- public static IDictionary ConvertMapToDictionary(MapField properties)
- {
- return properties.ToDictionary(
- kvp => kvp.Key,
- kvp => ConvertValueToObject(kvp.Value)
- );
- }
-
- ///
- /// Converts a C# object to a protobuf Value.
- ///
- /// The object to convert.
- /// The converted protobuf Value.
- internal static Value ConvertObjectToValue(object? obj)
- {
- return obj switch
- {
- null => Value.ForNull(),
- string str => Value.ForString(str),
- bool b => Value.ForBool(b),
- int i => Value.ForNumber(i),
- long l => Value.ForNumber(l),
- float f => Value.ForNumber(f),
- double d => Value.ForNumber(d),
- decimal dec => Value.ForNumber((double)dec),
-
- // For DateTime and DateTimeOffset, add prefix to distinguish from normal string.
- DateTime dt => Value.ForString($"dt:{dt.ToString("O")}"),
- DateTimeOffset dto => Value.ForString($"dto:{dto.ToString("O")}"),
- IDictionary dict => Value.ForStruct(new Struct
- {
- Fields = { dict.ToDictionary(kvp => kvp.Key, kvp => ConvertObjectToValue(kvp.Value)) },
- }),
- IEnumerable e => Value.ForList(e.Cast