Incremental work on proto actor and service bus

This commit is contained in:
Sipke Schoorstra 2022-04-10 14:35:53 +02:00
parent 51b7b4b1f1
commit 78a799edb9
71 changed files with 612 additions and 319 deletions

View file

@ -70,7 +70,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "modules", "modules", "{5BA4
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Modules.Quartz", "src\modules\Elsa.Modules.Quartz\Elsa.Modules.Quartz.csproj", "{7D5A49B4-9A9B-496E-803B-DEB85B2C3132}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Jobs", "src\core\Elsa.Jobs\Elsa.Jobs.csproj", "{50697882-E38B-4CE7-B041-8009C18490E0}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Jobs.Abstractions", "src\core\Elsa.Jobs.Abstractions\Elsa.Jobs.Abstractions.csproj", "{50697882-E38B-4CE7-B041-8009C18490E0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Modules.Http", "src\modules\Elsa.Modules.Http\Elsa.Modules.Http.csproj", "{82E26BF9-5F3A-4365-899A-AB1FFD54AA45}"
EndProject
@ -90,6 +90,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Modules.SetName", "src
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Modules.Activities", "src\modules\Elsa.Modules.Activities\Elsa.Modules.Activities.csproj", "{05249066-9125-47D8-847B-A86887B1D269}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.ServiceBus.Abstractions", "src\core\Elsa.ServiceBus.Abstractions\Elsa.ServiceBus.Abstractions.csproj", "{61FAFEED-A477-4D8F-A293-C0196F11EA0F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Modules.MassTransit", "src\modules\Elsa.Modules.MassTransit\Elsa.Modules.MassTransit.csproj", "{BB983D0B-A939-4008-B9E3-C7C172BCF83A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -212,6 +216,14 @@ Global
{05249066-9125-47D8-847B-A86887B1D269}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05249066-9125-47D8-847B-A86887B1D269}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05249066-9125-47D8-847B-A86887B1D269}.Release|Any CPU.Build.0 = Release|Any CPU
{61FAFEED-A477-4D8F-A293-C0196F11EA0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{61FAFEED-A477-4D8F-A293-C0196F11EA0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{61FAFEED-A477-4D8F-A293-C0196F11EA0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{61FAFEED-A477-4D8F-A293-C0196F11EA0F}.Release|Any CPU.Build.0 = Release|Any CPU
{BB983D0B-A939-4008-B9E3-C7C172BCF83A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB983D0B-A939-4008-B9E3-C7C172BCF83A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB983D0B-A939-4008-B9E3-C7C172BCF83A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB983D0B-A939-4008-B9E3-C7C172BCF83A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{155227F0-A33B-40AA-A4B4-06F813EB921B} = {61017E64-6D00-49CB-9E81-5002DC8F7D5F}
@ -254,5 +266,7 @@ Global
{302BFC43-ED2F-43AE-8AD4-FCD481B0AC67} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{E4EB94A0-E6F6-4EFA-BE55-7CEED1D8431A} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{05249066-9125-47D8-847B-A86887B1D269} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
{61FAFEED-A477-4D8F-A293-C0196F11EA0F} = {C6658DE0-2B2F-47F0-BB61-2CA66D435C09}
{BB983D0B-A939-4008-B9E3-C7C172BCF83A} = {5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}
EndGlobalSection
EndGlobal

View file

@ -17,9 +17,9 @@ public class DispatchWorkflowResult : IResult
public async Task ExecuteAsync(HttpContext httpContext)
{
var response = httpContext.Response;
var workflowInvoker = httpContext.RequestServices.GetRequiredService<IWorkflowInvoker>();
var workflowDispatcher = httpContext.RequestServices.GetRequiredService<IWorkflowDispatcher>();
var definitionId = Workflow.Identity.DefinitionId;
var result = await workflowInvoker.DispatchAsync(new DispatchWorkflowDefinitionRequest(definitionId, VersionOptions.Published));
var result = await workflowDispatcher.DispatchAsync(new DispatchWorkflowDefinitionRequest(definitionId, VersionOptions.Published));
response.StatusCode = (int)HttpStatusCode.OK;
await response.WriteAsJsonAsync(result, httpContext.RequestAborted);

View file

@ -19,8 +19,8 @@ public class ExecuteWorkflowResult : IResult
var response = httpContext.Response;
var workflowInvoker = httpContext.RequestServices.GetRequiredService<IWorkflowInvoker>();
var definitionId = Workflow.Identity.DefinitionId;
var executeRequest = new ExecuteWorkflowDefinitionRequest(definitionId, VersionOptions.Published);
var result = await workflowInvoker.ExecuteAsync(executeRequest, CancellationToken.None);
var executeRequest = new InvokeWorkflowDefinitionRequest(definitionId, VersionOptions.Published);
var result = await workflowInvoker.InvokeAsync(executeRequest, CancellationToken.None);
if (!response.HasStarted)
await response.WriteAsJsonAsync(result, httpContext.RequestAborted);

View file

@ -5,8 +5,8 @@ namespace Elsa.Contracts;
public interface IWorkflowRunner
{
Task<ExecuteWorkflowResult> RunAsync(Workflow workflow, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
Task<ExecuteWorkflowResult> RunAsync(Workflow workflow, WorkflowState workflowState, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
Task<ExecuteWorkflowResult> RunAsync(Workflow workflow, WorkflowState workflowState, Bookmark? bookmark, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
Task<ExecuteWorkflowResult> RunAsync(WorkflowExecutionContext workflowExecutionContext);
Task<InvokeWorkflowResult> RunAsync(Workflow workflow, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
Task<InvokeWorkflowResult> RunAsync(Workflow workflow, WorkflowState workflowState, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
Task<InvokeWorkflowResult> RunAsync(Workflow workflow, WorkflowState workflowState, Bookmark? bookmark, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
Task<InvokeWorkflowResult> RunAsync(WorkflowExecutionContext workflowExecutionContext);
}

View file

@ -1,3 +0,0 @@
namespace Elsa.Models;
public record DispatchWorkflowResult();

View file

@ -1,5 +0,0 @@
using Elsa.State;
namespace Elsa.Models;
public record ExecuteWorkflowResult(WorkflowState WorkflowState, IReadOnlyCollection<Bookmark> Bookmarks);

View file

@ -0,0 +1,5 @@
using Elsa.State;
namespace Elsa.Models;
public record InvokeWorkflowResult(WorkflowState WorkflowState, IReadOnlyCollection<Bookmark> Bookmarks);

View file

@ -30,7 +30,7 @@ public class WorkflowRunner : IWorkflowRunner
_schedulerFactory = schedulerFactory;
}
public async Task<ExecuteWorkflowResult> RunAsync(Workflow workflow, IDictionary<string, object>? input, CancellationToken cancellationToken = default)
public async Task<InvokeWorkflowResult> RunAsync(Workflow workflow, IDictionary<string, object>? input, CancellationToken cancellationToken = default)
{
// Create a child scope.
using var scope = _serviceScopeFactory.CreateScope();
@ -44,7 +44,7 @@ public class WorkflowRunner : IWorkflowRunner
return await RunAsync(workflowExecutionContext);
}
public async Task<ExecuteWorkflowResult> RunAsync(Workflow workflow, WorkflowState workflowState, IDictionary<string, object>? input, CancellationToken cancellationToken = default)
public async Task<InvokeWorkflowResult> RunAsync(Workflow workflow, WorkflowState workflowState, IDictionary<string, object>? input, CancellationToken cancellationToken = default)
{
// Create a child scope.
using var scope = _serviceScopeFactory.CreateScope();
@ -58,7 +58,7 @@ public class WorkflowRunner : IWorkflowRunner
return await RunAsync(workflowExecutionContext);
}
public async Task<ExecuteWorkflowResult> RunAsync(Workflow workflow, WorkflowState workflowState, Bookmark? bookmark, IDictionary<string, object>? input, CancellationToken cancellationToken = default)
public async Task<InvokeWorkflowResult> RunAsync(Workflow workflow, WorkflowState workflowState, Bookmark? bookmark, IDictionary<string, object>? input, CancellationToken cancellationToken = default)
{
// Create a child scope.
using var scope = _serviceScopeFactory.CreateScope();
@ -72,7 +72,7 @@ public class WorkflowRunner : IWorkflowRunner
return await RunAsync(workflowExecutionContext);
}
public async Task<ExecuteWorkflowResult> RunAsync(WorkflowExecutionContext workflowExecutionContext)
public async Task<InvokeWorkflowResult> RunAsync(WorkflowExecutionContext workflowExecutionContext)
{
// Execute the activity execution pipeline.
await _pipeline.ExecuteAsync(workflowExecutionContext);
@ -81,7 +81,7 @@ public class WorkflowRunner : IWorkflowRunner
var workflowState = _workflowStateSerializer.ReadState(workflowExecutionContext);
// Return workflow execution result containing state + bookmarks.
return new ExecuteWorkflowResult(workflowState, workflowExecutionContext.Bookmarks);
return new InvokeWorkflowResult(workflowState, workflowExecutionContext.Bookmarks);
}
private WorkflowExecutionContext CreateWorkflowExecutionContext(

View file

@ -4,6 +4,7 @@
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>Latest</LangVersion>
<RootNamespace>Elsa.Jobs</RootNamespace>
</PropertyGroup>
<ItemGroup>

View file

@ -0,0 +1,7 @@
namespace Elsa.ServiceBus.Abstractions.Contracts;
public interface IServiceBus
{
Task SendAsync(object message, CancellationToken cancellationToken = default);
Task PublishAsync(object message, CancellationToken cancellationToken = default);
}

View file

@ -0,0 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.ServiceBus.Abstractions.Contracts;
public interface IServiceBusProvider
{
void ConfigureServices(IServiceCollection services);
}

View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
</ItemGroup>
</Project>

View file

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\core\Elsa.Jobs\Elsa.Jobs.csproj" />
<ProjectReference Include="..\..\core\Elsa.Jobs.Abstractions\Elsa.Jobs.Abstractions.csproj" />
<ProjectReference Include="..\..\runtime\Elsa.Runtime\Elsa.Runtime.csproj" />
</ItemGroup>

View file

@ -11,7 +11,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\core\Elsa.Jobs\Elsa.Jobs.csproj" />
<ProjectReference Include="..\..\core\Elsa.Jobs.Abstractions\Elsa.Jobs.Abstractions.csproj" />
</ItemGroup>
</Project>

View file

@ -53,7 +53,7 @@ public class HttpTriggerMiddleware
var model = new
{
workflowInstanceIds = executionResults.Select(x => x.ExecuteWorkflowResult.WorkflowState.Id).ToArray()
workflowInstanceIds = executionResults.Select(x => x.InvokeWorkflowResult.WorkflowState.Id).ToArray()
};
var json = JsonSerializer.Serialize(model);

View file

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\core\Elsa.ServiceBus.Abstractions\Elsa.ServiceBus.Abstractions.csproj" />
<ProjectReference Include="..\..\runtime\Elsa.Runtime\Elsa.Runtime.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MassTransit" Version="8.0.1" />
<PackageReference Include="MassTransit.Extensions.DependencyInjection" Version="7.3.1" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,13 @@
using Elsa.Modules.MassTransit.Services;
using Elsa.Runtime.Contracts;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Modules.MassTransit.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMassTransitServices(this IServiceCollection services)
{
return services.AddSingleton<IWorkflowDispatcher, MassTransitWorkflowDispatcher>();
}
}

View file

@ -0,0 +1,12 @@
using Elsa.ServiceBus.Abstractions.Contracts;
using MassTransit;
namespace Elsa.Modules.MassTransit.Services;
public class MassTransitServiceBus : IServiceBus
{
private readonly IBus _bus;
public MassTransitServiceBus(IBus bus) => _bus = bus;
public async Task SendAsync(object message, CancellationToken cancellationToken = default) => await _bus.Send(message, cancellationToken);
public async Task PublishAsync(object message, CancellationToken cancellationToken = default) => await _bus.Publish(message, cancellationToken);
}

View file

@ -0,0 +1,16 @@
using Elsa.ServiceBus.Abstractions.Contracts;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Modules.MassTransit.Services;
/// <summary>
/// A MassTransit implementation for <see cref="IServiceBus"/>.
/// </summary>
public class MassTransitServiceBusProvider : IServiceBusProvider
{
public void ConfigureServices(IServiceCollection services)
{
services
.AddSingleton<IServiceBus, MassTransitServiceBus>();
}
}

View file

@ -0,0 +1,27 @@
using Elsa.Runtime.Contracts;
using Elsa.Runtime.Models;
using MassTransit;
namespace Elsa.Modules.MassTransit.Services;
public class MassTransitWorkflowDispatcher : IWorkflowDispatcher
{
private readonly IBus _bus;
public MassTransitWorkflowDispatcher(IBus bus)
{
_bus = bus;
}
public async Task<DispatchWorkflowDefinitionResponse> DispatchAsync(DispatchWorkflowDefinitionRequest request, CancellationToken cancellationToken = default)
{
await _bus.Send(request, cancellationToken);
return new();
}
public async Task<DispatchWorkflowInstanceResponse> DispatchAsync(DispatchWorkflowInstanceRequest request, CancellationToken cancellationToken = default)
{
await _bus.Send(request, cancellationToken);
return new();
}
}

View file

@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\core\Elsa.Jobs\Elsa.Jobs.csproj" />
<ProjectReference Include="..\..\core\Elsa.Jobs.Abstractions\Elsa.Jobs.Abstractions.csproj" />
</ItemGroup>
</Project>

View file

@ -27,7 +27,7 @@ public class ResumeWorkflowJob : Job
protected override async ValueTask ExecuteAsync(JobExecutionContext context)
{
var request = new DispatchWorkflowInstanceRequest(WorkflowInstanceId, Bookmark);
var workflowInvoker = context.GetRequiredService<IWorkflowInvoker>();
await workflowInvoker.DispatchAsync(request, context.CancellationToken);
var workflowDispatcher = context.GetRequiredService<IWorkflowDispatcher>();
await workflowDispatcher.DispatchAsync(request, context.CancellationToken);
}
}

View file

@ -25,7 +25,7 @@ public class RunWorkflowJob : Job
protected override async ValueTask ExecuteAsync(JobExecutionContext context)
{
var request = new DispatchWorkflowDefinitionRequest(WorkflowId, VersionOptions.Published);
var workflowInvoker = context.GetRequiredService<IWorkflowInvoker>();
await workflowInvoker.DispatchAsync(request, context.CancellationToken);
var workflowDispatcher = context.GetRequiredService<IWorkflowDispatcher>();
await workflowDispatcher.DispatchAsync(request, context.CancellationToken);
}
}

View file

@ -1,60 +0,0 @@
using System.Linq;
using System.Threading.Tasks;
using Elsa.Runtime.ProtoActor.Messages;
using Proto;
using Proto.DependencyInjection;
namespace Elsa.Runtime.ProtoActor.Actors;
/// <summary>
/// Dispatches a workflow instance for execution. The workflow instance will be executed by <see cref="WorkflowOperatorActor"/>.
/// </summary>
public class WorkflowInstanceActor : IActor
{
public Task ReceiveAsync(IContext context) => context.Message switch
{
ExecuteWorkflowInstance m => OnExecuteWorkflowInstanceAsync(context, m),
DispatchWorkflowInstance m => OnDispatchWorkflowInstanceAsync(context, m),
_ => Task.CompletedTask
};
private async Task OnExecuteWorkflowInstanceAsync(IContext context, ExecuteWorkflowInstance message)
{
var workflowInstanceId = message.Id;
var pid = GetWorkflowOperatorPid(context, workflowInstanceId);
var cancellationToken = context.CancellationToken;
var response = await context.RequestAsync<ExecuteWorkflowResponse>(pid, message, cancellationToken);
context.Respond(response);
}
private Task OnDispatchWorkflowInstanceAsync(IContext context, DispatchWorkflowInstance message)
{
var workflowInstanceId = message.Id;
var pid = GetWorkflowOperatorPid(context, workflowInstanceId);
var executeWorkflowInstanceMessage = new ExecuteWorkflowInstance
{
Id = workflowInstanceId,
Bookmark = message.Bookmark,
Input = message.Input
};
context.Send(pid, executeWorkflowInstanceMessage);
context.Respond(new Unit());
return Task.CompletedTask;
}
private PID GetWorkflowOperatorPid(IContext context, string workflowInstanceId)
{
var pid = context.System.ProcessRegistry.Find($"partition-activator/workflow-instance:{workflowInstanceId}").FirstOrDefault();
if (pid != null)
return pid;
var props = context.System.DI().PropsFor<WorkflowOperatorActor>();
pid = context.SpawnNamed(props, workflowInstanceId);
return pid;
}
}

View file

@ -18,6 +18,7 @@
<ItemGroup Label="ProtoActor">
<PackageReference Include="Proto.Actor" Version="0.29.0" />
<PackageReference Include="Proto.Cluster" Version="0.29.0" />
<PackageReference Include="Proto.Cluster.CodeGen" Version="0.29.0" />
<PackageReference Include="Proto.Cluster.Consul" Version="0.29.0" />
<PackageReference Include="Proto.Cluster.Identity.Redis" Version="0.29.0" />
<PackageReference Include="Proto.Cluster.TestProvider" Version="0.29.0" />
@ -37,12 +38,16 @@
<ProjectReference Include="..\Elsa.Runtime\Elsa.Runtime.csproj" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="messages.proto" />
</ItemGroup>
<ItemGroup>
<Folder Include="Contracts" />
<ItemGroup Label="Protos">
<Protobuf Include="Protos\Messages.proto">
<GrpcServices>Both</GrpcServices>
<Access>Public</Access>
<ProtoCompile>True</ProtoCompile>
<CompileOutputs>True</CompileOutputs>
<OutputDir>obj\Debug\net6.0\</OutputDir>
<Generator>MSBuild:Compile</Generator>
</Protobuf>
<ProtoGrain Include="Protos\Grains.proto" AdditionalImportDirs="Protos" />
</ItemGroup>
</Project>

View file

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Elsa.Runtime.ProtoActor.Messages;
using Elsa.Runtime.Protos;
namespace Elsa.Runtime.ProtoActor.Extensions;

View file

@ -1,8 +1,12 @@
using System;
using Elsa.Helpers;
using Elsa.Persistence.Models;
using Elsa.Runtime.Contracts;
using Elsa.Runtime.ProtoActor.Actors;
using Elsa.Runtime.Extensions;
using Elsa.Runtime.ProtoActor.Grains;
using Elsa.Runtime.ProtoActor.HostedServices;
using Elsa.Runtime.ProtoActor.Services;
using Elsa.Runtime.Protos;
using Microsoft.Extensions.DependencyInjection;
using Proto;
using Proto.Cluster;
@ -20,12 +24,13 @@ public static class ServiceCollectionExtensions
{
var systemConfig = GetSystemConfig();
// Actor System.
services.AddSingleton(sp =>
{
var system = new ActorSystem(systemConfig).WithServiceProvider(sp);
var remoteConfig = GetRemoteConfig();
var clusterConfig = GetClusterConfig(system, "my-cluster");
system
.WithRemote(remoteConfig)
.WithCluster(clusterConfig);
@ -33,37 +38,43 @@ public static class ServiceCollectionExtensions
return system;
});
// Cluster.
services.AddSingleton(sp => sp.GetRequiredService<ActorSystem>().Cluster());
return services
.AddHostedService<WorkflowServerHost>()
.AddSingleton<IWorkflowInvoker, ProtoActorWorkflowInvoker>()
.AddTransient<WorkflowDefinitionActor>()
.AddTransient<WorkflowInstanceActor>()
.AddTransient<WorkflowOperatorActor>();
}
private static ActorSystemConfig GetSystemConfig() =>
// Actors.
services
.AddSingleton(sp => new WorkflowDefinitionGrainActor((context, _) => ActivatorUtilities.CreateInstance<WorkflowDefinitionGrain>(sp, context)))
.AddSingleton(sp => new WorkflowInstanceGrainActor((context, _) => ActivatorUtilities.CreateInstance<WorkflowInstanceGrain>(sp, context)));
// Client factory.
services.AddSingleton<GrainClientFactory>();
// Configure runtime with ProtoActor workflow invoker.
services.ConfigureWorkflowRuntime(options => options.WorkflowInvokerFactory = sp => ActivatorUtilities.CreateInstance<ProtoActorWorkflowInvoker>(sp));
return services
.AddHostedService<WorkflowServerHost>();
}
private static ActorSystemConfig GetSystemConfig() =>
ActorSystemConfig
.Setup()
.WithDeadLetterThrottleCount(3)
.WithDeadLetterThrottleInterval(TimeSpan.FromSeconds(10000))
.WithDeveloperSupervisionLogging(true)
.WithDeadLetterRequestLogging(true);
private static GrpcCoreRemoteConfig GetRemoteConfig() => GrpcCoreRemoteConfig
.BindToLocalhost()
.WithProtoMessages(Messages.MessagesReflection.Descriptor)
;
.WithProtoMessages(Protos.MessagesReflection.Descriptor);
private static ClusterConfig GetClusterConfig(ActorSystem system, string clusterName)
{
//var clusterProvider = new ConsulProvider(new ConsulProviderConfig{});
var clusterProvider = new TestProvider(new TestProviderOptions(), new InMemAgent());
var workflowDefinitionProps = system.DI().PropsFor<WorkflowDefinitionActor>();
var workflowInstanceProps = system.DI().PropsFor<WorkflowInstanceActor>();
var workflowDefinitionProps = system.DI().PropsFor<WorkflowDefinitionGrainActor>();
var workflowInstanceProps = system.DI().PropsFor<WorkflowInstanceGrainActor>();
var clusterConfig =
ClusterConfig
@ -73,12 +84,12 @@ public static class ServiceCollectionExtensions
.WithActorRequestTimeout(TimeSpan.FromHours(1))
.WithActorActivationTimeout(TimeSpan.FromHours(1))
.WithActorSpawnTimeout(TimeSpan.FromHours(1))
.WithClusterKind(GrainKinds.WorkflowDefinition, workflowDefinitionProps)
.WithClusterKind(GrainKinds.WorkflowInstance, workflowInstanceProps)
.WithClusterKind(WorkflowDefinitionGrainActor.Kind, workflowDefinitionProps)
.WithClusterKind(WorkflowInstanceGrainActor.Kind, workflowInstanceProps)
;
return clusterConfig;
}
// private static IIdentityStorage GetIdentityLookup(string clusterName) =>
// new RedisIdentityStorage(clusterName, ConnectionMultiplexer
// .Connect("localhost:6379" /* use proper config */)

View file

@ -1,7 +0,0 @@
namespace Elsa.Runtime.ProtoActor;
public static class GrainKinds
{
public const string WorkflowDefinition = "WorkflowDefinition";
public const string WorkflowInstance = "WorkflowInstance";
}

View file

@ -1,3 +1,4 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Contracts;
@ -6,69 +7,64 @@ using Elsa.Persistence.Commands;
using Elsa.Persistence.Entities;
using Elsa.Persistence.Models;
using Elsa.Runtime.Contracts;
using Elsa.Runtime.ProtoActor.Messages;
using Elsa.Runtime.ProtoActor.Services;
using Elsa.Runtime.Protos;
using Elsa.State;
using Proto;
using Proto.Cluster;
using Unit = Elsa.Runtime.ProtoActor.Messages.Unit;
namespace Elsa.Runtime.ProtoActor.Actors;
namespace Elsa.Runtime.ProtoActor.Grains;
/// <summary>
/// Instantiates and dispatches a workflow instance for execution. The workflow instance will be executed by <see cref="WorkflowOperatorActor"/>.
/// Instantiates and executes a workflow instance for execution.
/// </summary>
public class WorkflowDefinitionActor : IActor
public class WorkflowDefinitionGrain : WorkflowDefinitionGrainBase
{
private readonly IWorkflowRegistry _workflowRegistry;
private readonly ICommandSender _commandSender;
private readonly IIdentityGenerator _identityGenerator;
private readonly ISystemClock _systemClock;
private readonly GrainClientFactory _grainClientFactory;
public WorkflowDefinitionActor(IWorkflowRegistry workflowRegistry, ICommandSender commandSender, IIdentityGenerator identityGenerator, ISystemClock systemClock)
public WorkflowDefinitionGrain(
IWorkflowRegistry workflowRegistry,
ICommandSender commandSender,
IIdentityGenerator identityGenerator,
ISystemClock systemClock,
GrainClientFactory grainClientFactory,
IContext context) : base(context)
{
_workflowRegistry = workflowRegistry;
_commandSender = commandSender;
_identityGenerator = identityGenerator;
_systemClock = systemClock;
_grainClientFactory = grainClientFactory;
}
public Task ReceiveAsync(IContext context) => context.Message switch
public override async Task<ExecuteWorkflowDefinitionResponse> Execute(ExecuteWorkflowDefinitionRequest request)
{
ExecuteWorkflowDefinition i => OnExecuteWorkflowDefinitionAsync(context, i),
DispatchWorkflowDefinition i => OnDispatchWorkflowDefinitionAsync(context, i),
_ => Task.CompletedTask
};
private async Task OnExecuteWorkflowDefinitionAsync(IContext context, ExecuteWorkflowDefinition message)
{
var workflowDefinitionId = message.Id;
var cancellationToken = context.CancellationToken;
var workflowDefinitionId = request.Id;
var cancellationToken = Context.CancellationToken;
var workflowInstance = await CreateWorkflowInstanceAsync(workflowDefinitionId, cancellationToken);
var executeWorkflowInstanceMessage = new ExecuteWorkflowInstance
var executeWorkflowInstanceMessage = new ExecuteWorkflowInstanceRequest
{
Id = workflowInstance.Id,
Input = message.Input
Input = request.Input
};
var response = await context.ClusterRequestAsync<ExecuteWorkflowResponse>(workflowInstance.Id, GrainKinds.WorkflowInstance, executeWorkflowInstanceMessage, cancellationToken);
context.Respond(response);
}
var workflowInstanceGrainClient = Context.GetWorkflowInstanceGrain(workflowInstance.Id);
var workflowInstanceResponse = await workflowInstanceGrainClient.Execute(executeWorkflowInstanceMessage, cancellationToken);
if (workflowInstanceResponse == null)
throw new TimeoutException("Did not receive a response from the WorkflowInstance actor within the configured amount of time.");
private async Task OnDispatchWorkflowDefinitionAsync(IContext context, DispatchWorkflowDefinition message)
{
var workflowDefinitionId = message.Id;
var cancellationToken = context.CancellationToken;
var workflowInstance = await CreateWorkflowInstanceAsync(workflowDefinitionId, cancellationToken);
var dispatchWorkflowInstanceMessage = new DispatchWorkflowInstance
var response = new ExecuteWorkflowDefinitionResponse
{
Id = workflowInstance.Id,
Input = message.Input
WorkflowState = workflowInstanceResponse.WorkflowState,
};
await context.ClusterRequestAsync<Unit>(workflowInstance.Id, GrainKinds.WorkflowInstance, dispatchWorkflowInstanceMessage, cancellationToken);
context.Respond(new Unit());
response.Bookmarks.AddRange(workflowInstanceResponse.Bookmarks);
return response;
}
private async Task<WorkflowInstance> CreateWorkflowInstanceAsync(string workflowDefinitionId, CancellationToken cancellationToken)

View file

@ -11,42 +11,45 @@ using Elsa.Persistence.Models;
using Elsa.Persistence.Requests;
using Elsa.Runtime.Contracts;
using Elsa.Runtime.ProtoActor.Extensions;
using Elsa.Runtime.ProtoActor.Messages;
using Elsa.Runtime.ProtoActor.Services;
using Elsa.Runtime.Protos;
using Elsa.Serialization;
using Elsa.State;
using Proto;
using Bookmark = Elsa.Runtime.ProtoActor.Messages.Bookmark;
using Bookmark = Elsa.Runtime.Protos.Bookmark;
namespace Elsa.Runtime.ProtoActor.Actors;
namespace Elsa.Runtime.ProtoActor.Grains;
/// <summary>
/// Executes a workflow instance.
/// </summary>
public class WorkflowOperatorActor : IActor
public class WorkflowInstanceGrain : WorkflowInstanceGrainBase
{
private readonly IRequestSender _requestSender;
private readonly IWorkflowRegistry _workflowRegistry;
private readonly GrainClientFactory _grainClientFactory;
private readonly IWorkflowRunner _workflowRunner;
private readonly WorkflowSerializerOptionsProvider _workflowSerializerOptionsProvider;
public WorkflowOperatorActor(IRequestSender requestSender, IWorkflowRegistry workflowRegistry, IWorkflowRunner workflowRunner, WorkflowSerializerOptionsProvider workflowSerializerOptionsProvider)
public WorkflowInstanceGrain(
IRequestSender requestSender,
IWorkflowRegistry workflowRegistry,
GrainClientFactory grainClientFactory,
IWorkflowRunner workflowRunner,
WorkflowSerializerOptionsProvider workflowSerializerOptionsProvider,
IContext context) : base(context)
{
_requestSender = requestSender;
_workflowRegistry = workflowRegistry;
_grainClientFactory = grainClientFactory;
_workflowRunner = workflowRunner;
_workflowSerializerOptionsProvider = workflowSerializerOptionsProvider;
}
public Task ReceiveAsync(IContext context) => context.Message switch
public override async Task<ExecuteWorkflowInstanceResponse> Execute(ExecuteWorkflowInstanceRequest request)
{
ExecuteWorkflowInstance m => OnExecuteWorkflowInstance(context, m),
_ => Task.CompletedTask
};
private async Task OnExecuteWorkflowInstance(IContext context, ExecuteWorkflowInstance message)
{
var workflowInstanceId = message.Id;
var cancellationToken = context.CancellationToken;
var workflowInstanceId = request.Id;
var cancellationToken = Context.CancellationToken;
var workflowInstance = await _requestSender.RequestAsync(new FindWorkflowInstance(workflowInstanceId), cancellationToken);
if (workflowInstance == null)
@ -59,15 +62,30 @@ public class WorkflowOperatorActor : IActor
throw new Exception($"No workflow definition found with ID {workflowDefinitionId}");
var workflowState = workflowInstance.WorkflowState;
var bookmarkMessage = message.Bookmark;
var input = message.Input?.Deserialize();
var executionResult = await ExecuteAsync(workflow, workflowState, bookmarkMessage, input, cancellationToken);
var bookmarkMessage = request.Bookmark;
var input = request.Input?.Deserialize();
var executionResult = await ExecuteAsync(workflow, workflowState, bookmarkMessage, input!, cancellationToken);
var response = MapResult(executionResult);
context.Respond(response);
return response;
}
private ExecuteWorkflowResponse MapResult(ExecuteWorkflowResult result)
// public override async Task Dispatch(DispatchWorkflowInstanceRequest request)
// {
// var workflowInstanceId = request.Id;
// //var pid = GetWorkflowOperatorPid(Context, workflowInstanceId);
//
// var executeWorkflowInstanceMessage = new ExecuteWorkflowInstanceRequest
// {
// Id = workflowInstanceId,
// Bookmark = request.Bookmark,
// Input = request.Input
// };
//Context.Send(pid, executeWorkflowInstanceMessage);
//}
private ExecuteWorkflowInstanceResponse MapResult(InvokeWorkflowResult result)
{
var bookmarks = result.Bookmarks.Select(x => new Bookmark
{
@ -79,10 +97,10 @@ public class WorkflowOperatorActor : IActor
ActivityInstanceId = x.ActivityInstanceId,
CallbackMethodName = x.CallbackMethodName
});
var options = _workflowSerializerOptionsProvider.CreatePersistenceOptions();
var response = new ExecuteWorkflowResponse
var response = new ExecuteWorkflowInstanceResponse
{
WorkflowState = new Json
{
@ -95,7 +113,7 @@ public class WorkflowOperatorActor : IActor
return response;
}
private async Task<ExecuteWorkflowResult> ExecuteAsync(Workflow workflow, WorkflowState workflowState, Bookmark? bookmarkMessage, IDictionary<string, object>? input, CancellationToken cancellationToken)
private async Task<InvokeWorkflowResult> ExecuteAsync(Workflow workflow, WorkflowState workflowState, Bookmark? bookmarkMessage, IDictionary<string, object>? input, CancellationToken cancellationToken)
{
if (bookmarkMessage == null)
return await _workflowRunner.RunAsync(workflow, workflowState, input, cancellationToken);

View file

@ -6,17 +6,13 @@ using Proto.Cluster;
namespace Elsa.Runtime.ProtoActor.HostedServices;
public class WorkflowServerHost : BackgroundService
/// <summary>
/// Starts the current member in the cluster.
/// </summary>
public class WorkflowServerHost : IHostedService
{
private readonly ActorSystem _actorSystem;
public WorkflowServerHost(ActorSystem actorSystem)
{
_actorSystem = actorSystem;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await _actorSystem.Cluster().StartMemberAsync();
}
public WorkflowServerHost(ActorSystem actorSystem) => _actorSystem = actorSystem;
public async Task StartAsync(CancellationToken cancellationToken) => await _actorSystem.Cluster().StartMemberAsync();
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View file

@ -0,0 +1,14 @@
syntax = "proto3";
option csharp_namespace = "Elsa.Runtime.Protos";
import "google/protobuf/empty.proto";
import "Messages.proto";
service WorkflowDefinitionGrain {
rpc Execute (ExecuteWorkflowDefinitionRequest) returns (ExecuteWorkflowDefinitionResponse);
}
service WorkflowInstanceGrain {
rpc Execute (ExecuteWorkflowInstanceRequest) returns (ExecuteWorkflowInstanceResponse);
}

View file

@ -1,41 +1,31 @@
syntax = "proto3";
import "google/protobuf/wrappers.proto";
package Elsa.Runtime.ProtoActor.Messages;
option csharp_namespace = "Elsa.Runtime.ProtoActor.Messages";
option csharp_namespace = "Elsa.Runtime.Protos";
// Empty response type.
message Unit {}
message ExecuteWorkflowDefinition {
message ExecuteWorkflowDefinitionRequest {
string id = 1;
string versionOptions = 2;
optional Input input = 3;
}
message DispatchWorkflowDefinition {
string id = 1;
string versionOptions = 2;
optional Input input = 3;
}
message ExecuteWorkflowInstance {
string id = 1;
optional Bookmark bookmark = 2;
optional Input input = 3;
}
message DispatchWorkflowInstance {
string id = 1;
optional Bookmark bookmark = 2;
optional Input input = 3;
}
message ExecuteWorkflowResponse {
message ExecuteWorkflowDefinitionResponse {
Json workflowState = 1;
repeated Bookmark bookmarks = 2;
}
message DispatchWorkflowResponse {
message ExecuteWorkflowInstanceRequest {
string id = 1;
optional Bookmark bookmark = 2;
optional Input input = 3;
}
message ExecuteWorkflowInstanceResponse {
Json workflowState = 1;
repeated Bookmark bookmarks = 2;
}
message HandleStimulusRequest {

View file

@ -0,0 +1,18 @@
using Elsa.Persistence.Models;
using Elsa.Runtime.Protos;
using Proto.Cluster;
namespace Elsa.Runtime.ProtoActor.Services;
public class GrainClientFactory
{
private readonly Cluster _cluster;
public GrainClientFactory(Cluster cluster)
{
_cluster = cluster;
}
public WorkflowDefinitionGrainClient CreateWorkflowDefinitionGrainClient(string workflowDefinitionId, VersionOptions versionOptions) => _cluster.GetWorkflowDefinitionGrain($"workflow-definition:{workflowDefinitionId}:{versionOptions.ToString()}");
public WorkflowInstanceGrainClient CreateWorkflowInstanceGrainClient(string workflowInstanceId) => _cluster.GetWorkflowInstanceGrain($"workflow-instance:{workflowInstanceId}");
}

View file

@ -1,110 +1,85 @@
using System;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Elsa.Models;
using Elsa.Persistence.Models;
using Elsa.Runtime.Contracts;
using Elsa.Runtime.Models;
using Elsa.Runtime.ProtoActor.Extensions;
using Elsa.Runtime.ProtoActor.Messages;
using Elsa.Runtime.Protos;
using Elsa.State;
using Proto;
using Proto.Cluster;
using Bookmark = Elsa.Models.Bookmark;
using ProtoBookmark = Elsa.Runtime.Protos.Bookmark;
namespace Elsa.Runtime.ProtoActor.Services;
using Bookmark = Elsa.Models.Bookmark;
using BookmarkMessage = Messages.Bookmark;
public class ProtoActorWorkflowInvoker : IWorkflowInvoker
{
private readonly Cluster _cluster;
private readonly GrainClientFactory _grainClientFactory;
public ProtoActorWorkflowInvoker(Cluster cluster)
public ProtoActorWorkflowInvoker(Cluster cluster, GrainClientFactory grainClientFactory)
{
_cluster = cluster;
_grainClientFactory = grainClientFactory;
}
public async Task<ExecuteWorkflowResult> ExecuteAsync(ExecuteWorkflowDefinitionRequest request, CancellationToken cancellationToken = default)
public async Task<InvokeWorkflowResult> InvokeAsync(InvokeWorkflowDefinitionRequest request, CancellationToken cancellationToken = default)
{
var (definitionId, versionOptions, input) = request;
var name = GetActorName(definitionId, versionOptions);
var message = new ExecuteWorkflowDefinition
var message = new ExecuteWorkflowDefinitionRequest
{
Id = definitionId,
VersionOptions = versionOptions.ToString(),
Input = input!?.Serialize()
};
var response = await _cluster.RequestAsync<ExecuteWorkflowResponse>(name, GrainKinds.WorkflowDefinition, message, cancellationToken);
var client = _grainClientFactory.CreateWorkflowDefinitionGrainClient(definitionId, versionOptions);
var response = await client.Execute(message, CancellationTokens.FromSeconds(100));
if (response == null)
throw new TimeoutException("Did not receive a response from the WorkflowDefinition actor within the configured amount of time.");
var workflowState = JsonSerializer.Deserialize<WorkflowState>(response.WorkflowState.Text)!;
var bookmarks = response.Bookmarks.Select(MapBookmark).ToList();
return new ExecuteWorkflowResult(workflowState, bookmarks);
return new InvokeWorkflowResult(workflowState, bookmarks);
}
public async Task<ExecuteWorkflowResult> ExecuteAsync(ExecuteWorkflowInstanceRequest request, CancellationToken cancellationToken = default)
public async Task<InvokeWorkflowResult> InvokeAsync(InvokeWorkflowInstanceRequest request, CancellationToken cancellationToken = default)
{
var (instanceId, bookmark, input) = request;
var name = GetActorName(instanceId);
var bookmarkMessage = MapBookmark(bookmark);
var message = new ExecuteWorkflowInstance
{
Id = instanceId,
Bookmark = bookmarkMessage,
Input = input?.Serialize()
};
var response = await _cluster.RequestAsync<ExecuteWorkflowResponse>(name, GrainKinds.WorkflowInstance, message, cancellationToken);
var workflowState = JsonSerializer.Deserialize<WorkflowState>(response.WorkflowState.Text)!;
var bookmarks = response.Bookmarks.Select(MapBookmark).ToList();
return new ExecuteWorkflowResult(workflowState, bookmarks);
}
public async Task<DispatchWorkflowResult> DispatchAsync(DispatchWorkflowDefinitionRequest request, CancellationToken cancellationToken = default)
{
var (definitionId, versionOptions, input) = request;
var name = GetActorName(definitionId, versionOptions);
var message = new DispatchWorkflowDefinition
{
Id = definitionId,
VersionOptions = versionOptions.ToString(),
Input = input!?.Serialize()
};
await _cluster.RequestAsync<Unit>(name, GrainKinds.WorkflowDefinition, message, cancellationToken);
return new DispatchWorkflowResult();
}
public async Task<DispatchWorkflowResult> DispatchAsync(DispatchWorkflowInstanceRequest request, CancellationToken cancellationToken = default)
{
var (instanceId, bookmark, input) = request;
var name = GetActorName(instanceId);
var bookmarkMessage = MapBookmark(bookmark);
var message = new DispatchWorkflowInstance
var message = new ExecuteWorkflowInstanceRequest
{
Id = instanceId,
Bookmark = bookmarkMessage,
Input = input!?.Serialize()
};
await _cluster.RequestAsync<Unit>(name, GrainKinds.WorkflowInstance, message, cancellationToken);
var client = _grainClientFactory.CreateWorkflowInstanceGrainClient(instanceId);
var response = await client.Execute(message, cancellationToken);
if (response == null)
throw new TimeoutException("Did not receive a response from the WorkflowInstance actor within the configured amount of time.");
var workflowState = JsonSerializer.Deserialize<WorkflowState>(response.WorkflowState.Text)!;
var bookmarks = response.Bookmarks.Select(MapBookmark).ToList();
return new DispatchWorkflowResult();
return new InvokeWorkflowResult(workflowState, bookmarks);
}
private BookmarkMessage? MapBookmark(Bookmark? bookmark)
private ProtoBookmark? MapBookmark(Bookmark? bookmark)
{
if (bookmark == null)
return null;
return new BookmarkMessage
return new ProtoBookmark
{
Id = bookmark.Id,
Name = bookmark.Name,
@ -116,19 +91,16 @@ public class ProtoActorWorkflowInvoker : IWorkflowInvoker
};
}
private Bookmark MapBookmark(BookmarkMessage bookmarkMessage)
private Bookmark MapBookmark(ProtoBookmark protoBookmark)
{
return new Bookmark(
bookmarkMessage.Id,
bookmarkMessage.Name,
bookmarkMessage.Hash,
bookmarkMessage.Payload,
bookmarkMessage.ActivityId,
bookmarkMessage.ActivityInstanceId,
bookmarkMessage.CallbackMethodName
protoBookmark.Id,
protoBookmark.Name,
protoBookmark.Hash,
protoBookmark.Payload,
protoBookmark.ActivityId,
protoBookmark.ActivityInstanceId,
protoBookmark.CallbackMethodName
);
}
private static string GetActorName(string definitionId, VersionOptions versionOptions) => $"workflow-definition:{definitionId}:{versionOptions.ToString()}";
private static string GetActorName(string instanceId) => $"workflow-instance:{instanceId}";
}

View file

@ -2,4 +2,4 @@ using Elsa.Models;
namespace Elsa.Runtime.Contracts;
public record ExecuteWorkflowInstructionResult(Workflow Workflow, ExecuteWorkflowResult ExecuteWorkflowResult);
public record ExecuteWorkflowInstructionResult(Workflow Workflow, InvokeWorkflowResult InvokeWorkflowResult);

View file

@ -0,0 +1,13 @@
using Elsa.Models;
using Elsa.Runtime.Models;
namespace Elsa.Runtime.Contracts;
/// <summary>
/// Posts a message to a queue to invoke a specified workflow.
/// </summary>
public interface IWorkflowDispatcher
{
Task<DispatchWorkflowDefinitionResponse> DispatchAsync(DispatchWorkflowDefinitionRequest request, CancellationToken cancellationToken = default);
Task<DispatchWorkflowInstanceResponse> DispatchAsync(DispatchWorkflowInstanceRequest request, CancellationToken cancellationToken = default);
}

View file

@ -3,10 +3,18 @@ using Elsa.Runtime.Models;
namespace Elsa.Runtime.Contracts;
/// <summary>
/// Invokes a workflow using a configured runtime (e.g. ProtoActor).
/// </summary>
public interface IWorkflowInvoker
{
Task<ExecuteWorkflowResult> ExecuteAsync(ExecuteWorkflowDefinitionRequest request, CancellationToken cancellationToken = default);
Task<ExecuteWorkflowResult> ExecuteAsync(ExecuteWorkflowInstanceRequest request, CancellationToken cancellationToken = default);
Task<DispatchWorkflowResult> DispatchAsync(DispatchWorkflowDefinitionRequest request, CancellationToken cancellationToken = default);
Task<DispatchWorkflowResult> DispatchAsync(DispatchWorkflowInstanceRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Creates a workflow instance of the specified workflow definition and then invokes the workflow instance.
/// </summary>
Task<InvokeWorkflowResult> InvokeAsync(InvokeWorkflowDefinitionRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// Invokes the specified workflow instance.
/// </summary>
Task<InvokeWorkflowResult> InvokeAsync(InvokeWorkflowInstanceRequest request, CancellationToken cancellationToken = default);
}

View file

@ -1,5 +1,6 @@
using Elsa.Models;
using Elsa.Persistence.Models;
using Elsa.Runtime.Models;
namespace Elsa.Runtime.Contracts;
@ -8,10 +9,10 @@ namespace Elsa.Runtime.Contracts;
/// </summary>
public interface IWorkflowService
{
Task<ExecuteWorkflowResult> ExecuteWorkflowAsync(string definitionId, VersionOptions versionOptions, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
Task<ExecuteWorkflowResult> ExecuteWorkflowAsync(string instanceId, Bookmark bookmark, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
Task<DispatchWorkflowResult> DispatchWorkflowAsync(string definitionId, VersionOptions versionOptions, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
Task<DispatchWorkflowResult> DispatchWorkflowAsync(string instanceId, Bookmark bookmark, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
Task<InvokeWorkflowResult> ExecuteWorkflowAsync(string definitionId, VersionOptions versionOptions, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
Task<InvokeWorkflowResult> ExecuteWorkflowAsync(string instanceId, Bookmark bookmark, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
Task<DispatchWorkflowDefinitionResponse> DispatchWorkflowAsync(string definitionId, VersionOptions versionOptions, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
Task<DispatchWorkflowInstanceResponse> DispatchWorkflowAsync(string instanceId, Bookmark bookmark, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default);
Task<IEnumerable<ExecuteWorkflowInstructionResult>> ExecuteStimulusAsync(IStimulus stimulus, CancellationToken cancellationToken);
Task<IEnumerable<DispatchWorkflowInstructionResult>> DispatchStimulusAsync(IStimulus stimulus, CancellationToken cancellationToken);

View file

@ -1,13 +1,16 @@
using System.Threading.Channels;
using Elsa.Expressions;
using Elsa.Mediator.Extensions;
using Elsa.Runtime.Contracts;
using Elsa.Runtime.HostedServices;
using Elsa.Runtime.Interpreters;
using Elsa.Runtime.Models;
using Elsa.Runtime.Options;
using Elsa.Runtime.Services;
using Elsa.Runtime.Stimuli.Handlers;
using Elsa.Runtime.WorkflowProviders;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Elsa.Runtime.Extensions;
@ -24,6 +27,8 @@ public static class ServiceCollectionExtensions
.AddSingleton<IWorkflowInstructionExecutor, WorkflowInstructionExecutor>()
.AddSingleton<ITriggerIndexer, TriggerIndexer>()
.AddSingleton<IBookmarkManager, BookmarkManager>()
.AddSingleton(sp => sp.GetRequiredService<IOptions<WorkflowRuntimeOptions>>().Value.WorkflowInvokerFactory(sp))
.AddSingleton(sp => sp.GetRequiredService<IOptions<WorkflowRuntimeOptions>>().Value.WorkflowDispatcherFactory(sp))
// Stimulus handlers.
.AddStimulusHandler<TriggerWorkflowsStimulusHandler>()
@ -42,13 +47,29 @@ public static class ServiceCollectionExtensions
// Domain event handlers.
.AddNotificationHandlersFrom(typeof(ServiceCollectionExtensions))
// Hosted Services.
.AddHostedService<RegisterDescriptors>()
.AddHostedService<RegisterExpressionSyntaxDescriptors>();
.AddHostedService<RegisterExpressionSyntaxDescriptors>()
.AddHostedService<DispatchedWorkflowDefinitionWorker>()
.AddHostedService<DispatchedWorkflowInstanceWorker>()
// Channels for dispatching workflows in-memory.
.CreateChannel<DispatchWorkflowDefinitionRequest>()
.CreateChannel<DispatchWorkflowInstanceRequest>()
;
}
private static IServiceCollection CreateChannel<T>(this IServiceCollection services) =>
services
.AddSingleton(CreateChannel<T>())
.AddSingleton(CreateChannelReader<T>)
.AddSingleton(CreateChannelWriter<T>);
private static Channel<T> CreateChannel<T>() => Channel.CreateUnbounded<T>(new UnboundedChannelOptions());
private static ChannelReader<T> CreateChannelReader<T>(IServiceProvider serviceProvider) => serviceProvider.GetRequiredService<Channel<T>>().Reader;
private static ChannelWriter<T> CreateChannelWriter<T>(IServiceProvider serviceProvider) => serviceProvider.GetRequiredService<Channel<T>>().Writer;
private static IServiceCollection AddDefaultExpressionHandlers(this IServiceCollection services) =>
services
.AddExpressionHandler<LiteralExpressionHandler, LiteralExpression>()

View file

@ -0,0 +1,42 @@
using System.Threading.Channels;
using Elsa.Runtime.Contracts;
using Elsa.Runtime.Models;
using Elsa.Runtime.Services;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Elsa.Runtime.HostedServices;
public class DispatchedWorkflowDefinitionWorker : BackgroundService
{
private readonly ChannelReader<DispatchWorkflowDefinitionRequest> _channelReader;
private readonly IWorkflowInvoker _workflowInvoker;
private readonly ILogger _logger;
public DispatchedWorkflowDefinitionWorker(
ChannelReader<DispatchWorkflowDefinitionRequest> channelReader,
IWorkflowInvoker workflowInvoker,
ILogger<DispatchedWorkflowDefinitionWorker> logger)
{
_channelReader = channelReader;
_workflowInvoker = workflowInvoker;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await foreach (var (definitionId, versionOptions, input) in _channelReader.ReadAllAsync(cancellationToken))
{
var request = new InvokeWorkflowDefinitionRequest(definitionId, versionOptions, input);
try
{
await _workflowInvoker.InvokeAsync(request, cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, "An unhandled exception occured while processing the queue");
}
}
}
}

View file

@ -0,0 +1,41 @@
using System.Threading.Channels;
using Elsa.Runtime.Contracts;
using Elsa.Runtime.Models;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Elsa.Runtime.HostedServices;
public class DispatchedWorkflowInstanceWorker : BackgroundService
{
private readonly ChannelReader<DispatchWorkflowInstanceRequest> _channelReader;
private readonly IWorkflowInvoker _workflowInvoker;
private readonly ILogger _logger;
public DispatchedWorkflowInstanceWorker(
ChannelReader<DispatchWorkflowInstanceRequest> channelReader,
IWorkflowInvoker workflowInvoker,
ILogger<DispatchedWorkflowInstanceWorker> logger)
{
_channelReader = channelReader;
_workflowInvoker = workflowInvoker;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
{
await foreach (var (instanceId, bookmark, input) in _channelReader.ReadAllAsync(cancellationToken))
{
var request = new InvokeWorkflowInstanceRequest(instanceId, bookmark, input);
try
{
await _workflowInvoker.InvokeAsync(request, cancellationToken);
}
catch (Exception e)
{
_logger.LogError(e, "An unhandled exception occured while processing the queue");
}
}
}
}

View file

@ -13,12 +13,18 @@ public record TriggerWorkflowInstruction(WorkflowTrigger WorkflowTrigger, IDicti
public class TriggerWorkflowInstructionInterpreter : WorkflowInstructionInterpreter<TriggerWorkflowInstruction>
{
private readonly IWorkflowInvoker _workflowInvoker;
private readonly IWorkflowDispatcher _workflowDispatcher;
private readonly IWorkflowRegistry _workflowRegistry;
private readonly ILogger _logger;
public TriggerWorkflowInstructionInterpreter(IWorkflowInvoker workflowInvoker, IWorkflowRegistry workflowRegistry, ILogger<TriggerWorkflowInstructionInterpreter> logger)
public TriggerWorkflowInstructionInterpreter(
IWorkflowInvoker workflowInvoker,
IWorkflowDispatcher workflowDispatcher,
IWorkflowRegistry workflowRegistry,
ILogger<TriggerWorkflowInstructionInterpreter> logger)
{
_workflowInvoker = workflowInvoker;
_workflowDispatcher = workflowDispatcher;
_workflowRegistry = workflowRegistry;
_logger = logger;
}
@ -35,8 +41,8 @@ public class TriggerWorkflowInstructionInterpreter : WorkflowInstructionInterpre
return null;
// Execute workflow.
var executeRequest = new ExecuteWorkflowDefinitionRequest(workflowId, VersionOptions.Published, instruction.Input);
var workflowExecutionResult = await _workflowInvoker.ExecuteAsync(executeRequest, cancellationToken);
var executeRequest = new InvokeWorkflowDefinitionRequest(workflowId, VersionOptions.Published, instruction.Input);
var workflowExecutionResult = await _workflowInvoker.InvokeAsync(executeRequest, cancellationToken);
return new ExecuteWorkflowInstructionResult(workflow, workflowExecutionResult);
}
@ -54,7 +60,7 @@ public class TriggerWorkflowInstructionInterpreter : WorkflowInstructionInterpre
// Execute workflow.
var dispatchRequest = new DispatchWorkflowDefinitionRequest(definitionId, VersionOptions.Published, instruction.Input);
await _workflowInvoker.DispatchAsync(dispatchRequest, cancellationToken);
await _workflowDispatcher.DispatchAsync(dispatchRequest, cancellationToken);
return new DispatchWorkflowInstructionResult();
}

View file

@ -0,0 +1,3 @@
namespace Elsa.Runtime.Models;
public record DispatchWorkflowDefinitionResponse();

View file

@ -0,0 +1,3 @@
namespace Elsa.Runtime.Models;
public record DispatchWorkflowInstanceResponse();

View file

@ -1,5 +0,0 @@
using Elsa.Persistence.Models;
namespace Elsa.Runtime.Models;
public record ExecuteWorkflowDefinitionRequest(string DefinitionId, VersionOptions VersionOptions, IDictionary<string, object>? Input = default);

View file

@ -1,5 +0,0 @@
using Elsa.Models;
namespace Elsa.Runtime.Models;
public record ExecuteWorkflowInstanceRequest(string InstanceId, Bookmark? Bookmark = default, IDictionary<string, object>? Input = default);

View file

@ -0,0 +1,5 @@
using Elsa.Persistence.Models;
namespace Elsa.Runtime.Models;
public record InvokeWorkflowDefinitionRequest(string DefinitionId, VersionOptions VersionOptions, IDictionary<string, object>? Input = default);

View file

@ -0,0 +1,5 @@
using Elsa.Models;
namespace Elsa.Runtime.Models;
public record InvokeWorkflowInstanceRequest(string InstanceId, Bookmark? Bookmark = default, IDictionary<string, object>? Input = default);

View file

@ -1,11 +1,23 @@
using Elsa.Runtime.Contracts;
using Elsa.Runtime.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Elsa.Runtime.Options;
public class WorkflowRuntimeOptions
{
/// <summary>
/// A list of workflow builders configured at application startup.
/// A list of workflow builders configured during application startup.
/// </summary>
public IDictionary<string, Func<IServiceProvider, IWorkflow>> Workflows { get; set; } = new Dictionary<string, Func<IServiceProvider, IWorkflow>>();
/// <summary>
/// A factory that instantiates a concrete <see cref="IWorkflowInvoker"/>.
/// </summary>
public Func<IServiceProvider, IWorkflowInvoker> WorkflowInvokerFactory { get; set; } = sp => ActivatorUtilities.CreateInstance<DefaultWorkflowInvoker>(sp);
/// <summary>
/// A factory that instantiates a concrete <see cref="IWorkflowDispatcher"/>.
/// </summary>
public Func<IServiceProvider, IWorkflowDispatcher> WorkflowDispatcherFactory { get; set; } = sp => ActivatorUtilities.CreateInstance<TaskBasedWorkflowDispatcher>(sp);
}

View file

@ -0,0 +1,21 @@
using Elsa.Models;
using Elsa.Runtime.Contracts;
using Elsa.Runtime.Models;
namespace Elsa.Runtime.Services;
/// <summary>
/// A basic implementation that directly executes the specified workflow in local memory (as opposed elsewhere in some cluster).
/// </summary>
public class DefaultWorkflowInvoker : IWorkflowInvoker
{
public Task<InvokeWorkflowResult> InvokeAsync(InvokeWorkflowDefinitionRequest request, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public Task<InvokeWorkflowResult> InvokeAsync(InvokeWorkflowInstanceRequest request, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,34 @@
using System.Threading.Channels;
using Elsa.Runtime.Contracts;
using Elsa.Runtime.Models;
namespace Elsa.Runtime.Services;
/// <summary>
/// A simple implementation that queues the specified request for workflow execution on a non-durable background worker.
/// </summary>
public class TaskBasedWorkflowDispatcher : IWorkflowDispatcher
{
private readonly ChannelWriter<DispatchWorkflowDefinitionRequest> _workflowDefinitionChannelWriter;
private readonly ChannelWriter<DispatchWorkflowInstanceRequest> _workflowInstanceChannelWriter;
public TaskBasedWorkflowDispatcher(
ChannelWriter<DispatchWorkflowDefinitionRequest> workflowDefinitionChannelWriter,
ChannelWriter<DispatchWorkflowInstanceRequest> workflowInstanceChannelWriter)
{
_workflowDefinitionChannelWriter = workflowDefinitionChannelWriter;
_workflowInstanceChannelWriter = workflowInstanceChannelWriter;
}
public async Task<DispatchWorkflowDefinitionResponse> DispatchAsync(DispatchWorkflowDefinitionRequest request, CancellationToken cancellationToken = default)
{
await _workflowDefinitionChannelWriter.WriteAsync(request, cancellationToken);
return new DispatchWorkflowDefinitionResponse();
}
public async Task<DispatchWorkflowInstanceResponse> DispatchAsync(DispatchWorkflowInstanceRequest request, CancellationToken cancellationToken = default)
{
await _workflowInstanceChannelWriter.WriteAsync(request, cancellationToken);
return new DispatchWorkflowInstanceResponse();
}
}

View file

@ -8,38 +8,44 @@ namespace Elsa.Runtime.Services;
public class WorkflowService : IWorkflowService
{
private readonly IWorkflowInvoker _workflowInvoker;
private readonly IWorkflowDispatcher _workflowDispatcher;
private readonly IWorkflowInstructionExecutor _workflowInstructionExecutor;
private readonly IStimulusInterpreter _stimulusInterpreter;
public WorkflowService(IWorkflowInvoker workflowInvoker, IWorkflowInstructionExecutor workflowInstructionExecutor, IStimulusInterpreter stimulusInterpreter)
public WorkflowService(
IWorkflowInvoker workflowInvoker,
IWorkflowDispatcher workflowDispatcher,
IWorkflowInstructionExecutor workflowInstructionExecutor,
IStimulusInterpreter stimulusInterpreter)
{
_workflowInvoker = workflowInvoker;
_workflowDispatcher = workflowDispatcher;
_workflowInstructionExecutor = workflowInstructionExecutor;
_stimulusInterpreter = stimulusInterpreter;
}
public async Task<ExecuteWorkflowResult> ExecuteWorkflowAsync(string definitionId, VersionOptions versionOptions, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default)
public async Task<InvokeWorkflowResult> ExecuteWorkflowAsync(string definitionId, VersionOptions versionOptions, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default)
{
var executeRequest = new ExecuteWorkflowDefinitionRequest(definitionId, versionOptions, input);
return await _workflowInvoker.ExecuteAsync(executeRequest, cancellationToken);
var executeRequest = new InvokeWorkflowDefinitionRequest(definitionId, versionOptions, input);
return await _workflowInvoker.InvokeAsync(executeRequest, cancellationToken);
}
public async Task<ExecuteWorkflowResult> ExecuteWorkflowAsync(string instanceId, Bookmark bookmark, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default)
public async Task<InvokeWorkflowResult> ExecuteWorkflowAsync(string instanceId, Bookmark bookmark, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default)
{
var request = new ExecuteWorkflowInstanceRequest(instanceId, bookmark, input);
return await _workflowInvoker.ExecuteAsync(request, cancellationToken);
var request = new InvokeWorkflowInstanceRequest(instanceId, bookmark, input);
return await _workflowInvoker.InvokeAsync(request, cancellationToken);
}
public async Task<DispatchWorkflowResult> DispatchWorkflowAsync(string definitionId, VersionOptions versionOptions, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default)
public async Task<DispatchWorkflowDefinitionResponse> DispatchWorkflowAsync(string definitionId, VersionOptions versionOptions, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default)
{
var executeRequest = new DispatchWorkflowDefinitionRequest(definitionId, versionOptions, input);
return await _workflowInvoker.DispatchAsync(executeRequest, cancellationToken);
return await _workflowDispatcher.DispatchAsync(executeRequest, cancellationToken);
}
public async Task<DispatchWorkflowResult> DispatchWorkflowAsync(string instanceId, Bookmark bookmark, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default)
public async Task<DispatchWorkflowInstanceResponse> DispatchWorkflowAsync(string instanceId, Bookmark bookmark, IDictionary<string, object>? input = default, CancellationToken cancellationToken = default)
{
var request = new DispatchWorkflowInstanceRequest(instanceId, bookmark, input);
return await _workflowInvoker.DispatchAsync(request, cancellationToken);
return await _workflowDispatcher.DispatchAsync(request, cancellationToken);
}
public async Task<IEnumerable<ExecuteWorkflowInstructionResult>> ExecuteStimulusAsync(IStimulus stimulus, CancellationToken cancellationToken = default)

View file

@ -6,11 +6,13 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\core\Elsa.ServiceBus.Abstractions\Elsa.ServiceBus.Abstractions.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Modules.Activities\Elsa.Modules.Activities.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Modules.AzureServiceBus\Elsa.Modules.AzureServiceBus.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Modules.Hangfire\Elsa.Modules.Hangfire.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Modules.Http\Elsa.Modules.Http.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Modules.JavaScript\Elsa.Modules.JavaScript.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Modules.MassTransit\Elsa.Modules.MassTransit.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Modules.Scheduling\Elsa.Modules.Scheduling.csproj" />
<ProjectReference Include="..\..\..\modules\Elsa.Modules.Quartz\Elsa.Modules.Quartz.csproj" />
<ProjectReference Include="..\..\..\api\Elsa.Api\Elsa.Api.csproj" />