Incremental work on Azure Service Bus
This commit is contained in:
parent
88609d5a62
commit
0d06ae1823
|
|
@ -11,7 +11,7 @@ public static class Trigger
|
|||
public static IResult Handle(string eventName, IHasher hasher)
|
||||
{
|
||||
var hash = hasher.Hash(eventName);
|
||||
var stimulus = Stimuli.Standard(nameof(Event), hash);
|
||||
var stimulus = Stimulus.Standard(nameof(Event), hash);
|
||||
return new ProcessStimulusResult(stimulus);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,8 +15,4 @@
|
|||
<PackageReference Include="System.Text.Json" Version="6.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Converters" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
30
src/core/Elsa.Core/Helpers/Diff.cs
Normal file
30
src/core/Elsa.Core/Helpers/Diff.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
namespace Elsa.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// A simple utility that compares two collections and returns a set of added, removed and unchanged items.
|
||||
/// </summary>
|
||||
public class Diff<T>
|
||||
{
|
||||
internal Diff(ICollection<T> added, ICollection<T> removed, ICollection<T> unchanged)
|
||||
{
|
||||
Added = added;
|
||||
Removed = removed;
|
||||
Unchanged = unchanged;
|
||||
}
|
||||
|
||||
public ICollection<T> Added { get; }
|
||||
public ICollection<T> Removed { get; }
|
||||
public ICollection<T> Unchanged { get; }
|
||||
}
|
||||
|
||||
public static class Diff
|
||||
{
|
||||
public static Diff<T> For<T>(ICollection<T> firstSet, ICollection<T> secondSet)
|
||||
{
|
||||
var added = firstSet.Except(secondSet).ToList();
|
||||
var removed = secondSet.Except(firstSet).ToList();
|
||||
var unchanged = firstSet.Intersect(secondSet).ToList();
|
||||
|
||||
return new Diff<T>(added, removed, unchanged);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,11 +18,24 @@ public class TriggerActivity : Activity, ITrigger
|
|||
set => NodeType = value;
|
||||
}
|
||||
|
||||
public virtual ValueTask<IEnumerable<object>> GetPayloadsAsync(TriggerIndexingContext context, CancellationToken cancellationToken = default)
|
||||
ValueTask<IEnumerable<object>> ITrigger.GetPayloadsAsync(TriggerIndexingContext context, CancellationToken cancellationToken) => GetPayloadsAsync(context, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Override this method to return a list of trigger payloads.
|
||||
/// </summary>
|
||||
protected virtual ValueTask<IEnumerable<object>> GetPayloadsAsync(TriggerIndexingContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var hashes = GetHashInputs(context);
|
||||
var hashes = GetPayloads(context);
|
||||
return ValueTask.FromResult(hashes);
|
||||
}
|
||||
|
||||
protected virtual IEnumerable<object> GetHashInputs(TriggerIndexingContext context) => Enumerable.Empty<object>();
|
||||
/// <summary>
|
||||
/// Override this method to return a list of trigger payloads.
|
||||
/// </summary>
|
||||
protected virtual IEnumerable<object> GetPayloads(TriggerIndexingContext context) => new[]{ GetPayload(context) };
|
||||
|
||||
/// <summary>
|
||||
/// Override this method to return a trigger payloads.
|
||||
/// </summary>
|
||||
protected virtual object GetPayload(TriggerIndexingContext context) => new();
|
||||
}
|
||||
|
|
@ -14,4 +14,6 @@ public class TriggerIndexingContext
|
|||
ExpressionExecutionContext = expressionExecutionContext;
|
||||
Trigger = trigger;
|
||||
}
|
||||
|
||||
public T? Get<T>(Input<T> input) => ExpressionExecutionContext.Get<T>(input);
|
||||
}
|
||||
|
|
@ -76,5 +76,5 @@ public static class ServiceCollectionExtensions
|
|||
public static IServiceCollection AddStimulusHandler<T>(this IServiceCollection services) where T : class, IStimulusHandler => services.AddSingleton<IStimulusHandler, T>();
|
||||
public static IServiceCollection AddInstructionInterpreter<T>(this IServiceCollection services) where T : class, IWorkflowInstructionInterpreter => services.AddSingleton<IWorkflowInstructionInterpreter, T>();
|
||||
public static IServiceCollection ConfigureWorkflowRuntime(this IServiceCollection services, Action<WorkflowRuntimeOptions> configure) => services.Configure(configure);
|
||||
public static IServiceCollection IndexWorkflowTriggers(this IServiceCollection services) => services.AddHostedService<IndexWorkflowTriggersHostedService>();
|
||||
public static IServiceCollection IndexWorkflowTriggers(this IServiceCollection services) => services.AddHostedService<IndexWorkflowTriggers>();
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
using Elsa.Attributes;
|
||||
using Elsa.Models;
|
||||
using Elsa.Modules.AzureServiceBus.Models;
|
||||
|
||||
namespace Elsa.Modules.AzureServiceBus.Activities;
|
||||
|
||||
[Activity("Azure.ServiceBus.MessageReceived", "Executes when a message is received from the configured queue or topic and subscription", "Azure Service Bus")]
|
||||
public class MessageReceived : TriggerActivity
|
||||
{
|
||||
public Input<string> QueueOrTopic { get; set; } = default!;
|
||||
public Input<string>? Subscription { get; set; } = default!;
|
||||
|
||||
protected override object GetPayload(TriggerIndexingContext context)
|
||||
{
|
||||
var queueOrTopic = context.Get(QueueOrTopic)!;
|
||||
var subscription = context.Get(Subscription!)!;
|
||||
return new MessageReceivedTriggerPayload(queueOrTopic, subscription);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging;
|
|||
namespace Elsa.Modules.AzureServiceBus.Activities;
|
||||
|
||||
[Activity("Azure.ServiceBus.Send", "Send a message to a queue or topic", "Azure Service Bus")]
|
||||
public class Send : Activity
|
||||
public class SendMessage : Activity
|
||||
{
|
||||
public Input<object> MessageBody { get; set; } = default!;
|
||||
public Input<string> QueueOrTopic { get; set; } = default!;
|
||||
|
|
@ -55,7 +55,7 @@ public class Send : Activity
|
|||
|
||||
private static bool ValidatePreconditions(ActivityExecutionContext context, string? queueOrTopic, object? messageBody)
|
||||
{
|
||||
var logger = context.GetRequiredService<ILogger<Send>>();
|
||||
var logger = context.GetRequiredService<ILogger<SendMessage>>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(queueOrTopic))
|
||||
{
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
using Elsa.Modules.AzureServiceBus.Models;
|
||||
using Elsa.Modules.AzureServiceBus.Services;
|
||||
|
||||
namespace Elsa.Modules.AzureServiceBus.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Manages message workers.
|
||||
/// </summary>
|
||||
public interface IWorkerManager
|
||||
{
|
||||
IReadOnlyCollection<Worker> Workers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether a worker exists for the specified trigger.
|
||||
/// </summary>
|
||||
/// <param name="queueOrTopic"></param>
|
||||
/// <param name="subscription"></param>
|
||||
/// <param name="payload"></param>
|
||||
/// <returns></returns>
|
||||
Worker? FindWorkerFor(string queueOrTopic, string? subscription);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a worker for the specified queue or topic and subscription.
|
||||
/// </summary>
|
||||
Task StartWorkerAsync(string queueOrTopic, string? subscription, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Decrements the ref count for the worker for the specified queue or topic and subscription.
|
||||
/// If the worker's ref count reaches 0, it is removed.
|
||||
/// </summary>
|
||||
Task StopWorkerAsync(string queueOrTopic, string? subscription, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified worker.
|
||||
/// </summary>
|
||||
Task RemoveWorkerAsync(Worker worker);
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@
|
|||
<ProjectReference Include="..\..\core\Elsa.Core\Elsa.Core.csproj" />
|
||||
<ProjectReference Include="..\..\core\Elsa.Formatting\Elsa.Formatting.csproj" />
|
||||
<ProjectReference Include="..\..\core\Elsa.Mediator\Elsa.Mediator.csproj" />
|
||||
<ProjectReference Include="..\..\persistence\Elsa.Persistence.Abstractions\Elsa.Persistence.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\runtime\Elsa.Runtime\Elsa.Runtime.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using Azure.Messaging.ServiceBus;
|
||||
using Azure.Messaging.ServiceBus.Administration;
|
||||
using Elsa.Mediator.Extensions;
|
||||
using Elsa.Modules.AzureServiceBus.Contracts;
|
||||
using Elsa.Modules.AzureServiceBus.Handlers;
|
||||
using Elsa.Modules.AzureServiceBus.HostedServices;
|
||||
using Elsa.Modules.AzureServiceBus.Options;
|
||||
using Elsa.Modules.AzureServiceBus.Providers;
|
||||
|
|
@ -25,14 +27,22 @@ public static class ServiceCollectionExtensions
|
|||
.AddSingleton(CreateServiceBusManagementClient)
|
||||
.AddSingleton(CreateServiceBusClient)
|
||||
.AddSingleton<ConfigurationQueueTopicAndSubscriptionProvider>()
|
||||
.AddTransient<IServiceBusInitializer, ServiceBusInitializer>()
|
||||
.AddSingleton<IWorkerManager, WorkerManager>()
|
||||
.AddTransient<IServiceBusInitializer, ServiceBusInitializer>();
|
||||
|
||||
// Definition providers.
|
||||
services
|
||||
.AddSingleton<IQueueProvider>(sp => sp.GetRequiredService<ConfigurationQueueTopicAndSubscriptionProvider>())
|
||||
.AddSingleton<ITopicProvider>(sp => sp.GetRequiredService<ConfigurationQueueTopicAndSubscriptionProvider>())
|
||||
.AddSingleton<ISubscriptionProvider>(sp => sp.GetRequiredService<ConfigurationQueueTopicAndSubscriptionProvider>());
|
||||
|
||||
// Hosted services.
|
||||
if (autoCreateQueuesTopicsAndSubscriptions)
|
||||
services.AddHostedService<CreateQueuesTopicsAndSubscriptions>();
|
||||
|
||||
// Handlers.
|
||||
services.AddHandlersFrom<UpdateWorkers>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
using Elsa.Contracts;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Modules.AzureServiceBus.Activities;
|
||||
using Elsa.Modules.AzureServiceBus.Contracts;
|
||||
using Elsa.Modules.AzureServiceBus.Models;
|
||||
using Elsa.Persistence.Extensions;
|
||||
using Elsa.Runtime.Models;
|
||||
using Elsa.Runtime.Notifications;
|
||||
|
||||
namespace Elsa.Modules.AzureServiceBus.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// Creates workers for each trigger & bookmark in response to updated workflow trigger indexes and bookmarks.
|
||||
/// </summary>
|
||||
public class UpdateWorkers : INotificationHandler<WorkflowTriggersIndexed>, INotificationHandler<WorkflowBookmarksDeleted>, INotificationHandler<WorkflowBookmarksSaved>
|
||||
{
|
||||
private readonly IPayloadSerializer _serializer;
|
||||
private readonly IWorkerManager _workerManager;
|
||||
|
||||
public UpdateWorkers(IPayloadSerializer serializer, IWorkerManager workerManager)
|
||||
{
|
||||
_serializer = serializer;
|
||||
_workerManager = workerManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds, updates and removes workers based on added and removed triggers.
|
||||
/// </summary>
|
||||
public async Task HandleAsync(WorkflowTriggersIndexed notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var added = notification.IndexedWorkflow.AddedTriggers.Filter<MessageReceived>().Select(x => DeserializePayload(x.Payload!));
|
||||
var removed = notification.IndexedWorkflow.RemovedTriggers.Filter<MessageReceived>().Select(x => DeserializePayload(x.Payload!));
|
||||
|
||||
await StopWorkersAsync(removed, cancellationToken);
|
||||
await StartWorkersAsync(added, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes workers based on removed bookmarks.
|
||||
/// </summary>
|
||||
public async Task HandleAsync(WorkflowBookmarksDeleted notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var payloads = notification.Bookmarks.Filter<MessageReceived>().Select(x => DeserializePayload(x.Payload!));
|
||||
await StopWorkersAsync(payloads, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts workers based on added bookmarks.
|
||||
/// </summary>
|
||||
public async Task HandleAsync(WorkflowBookmarksSaved notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var payloads = notification.Bookmarks.Filter<MessageReceived>().Select(x => DeserializePayload(x.Payload!));
|
||||
await StartWorkersAsync(payloads, cancellationToken);
|
||||
}
|
||||
|
||||
private MessageReceivedTriggerPayload DeserializePayload(string payload) => _serializer.Deserialize<MessageReceivedTriggerPayload>(payload);
|
||||
private ICollection<MessageReceivedTriggerPayload> DeserializePayloads(IEnumerable<string> payloads) => payloads.Select(_serializer.Deserialize<MessageReceivedTriggerPayload>).ToList();
|
||||
|
||||
private async Task StartWorkersAsync(IEnumerable<MessageReceivedTriggerPayload> payloads, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var payload in payloads) await _workerManager.StartWorkerAsync(payload.QueueOrTopic, payload.Subscription, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task StopWorkersAsync(IEnumerable<MessageReceivedTriggerPayload> payloads, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var payload in payloads) await _workerManager.StopWorkerAsync(payload.QueueOrTopic, payload.Subscription, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Elsa.Modules.AzureServiceBus.Models;
|
||||
|
||||
public record MessageReceivedTriggerPayload
|
||||
{
|
||||
private readonly string _queueOrTopic = default!;
|
||||
private readonly string? _subscription;
|
||||
|
||||
public MessageReceivedTriggerPayload(string queueOrTopic, string? subscription)
|
||||
{
|
||||
QueueOrTopic = queueOrTopic;
|
||||
Subscription = subscription;
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public MessageReceivedTriggerPayload()
|
||||
{
|
||||
}
|
||||
|
||||
public string QueueOrTopic
|
||||
{
|
||||
get => _queueOrTopic;
|
||||
init => _queueOrTopic = value.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public string? Subscription
|
||||
{
|
||||
get => _subscription;
|
||||
init => _subscription = value?.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public void Deconstruct(out string queueOrTopic, out string? subscription)
|
||||
{
|
||||
queueOrTopic = QueueOrTopic;
|
||||
subscription = Subscription;
|
||||
}
|
||||
}
|
||||
80
src/modules/Elsa.Modules.AzureServiceBus/Services/Worker.cs
Normal file
80
src/modules/Elsa.Modules.AzureServiceBus/Services/Worker.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
using Azure.Messaging.ServiceBus;
|
||||
using Elsa.Contracts;
|
||||
using Elsa.Helpers;
|
||||
using Elsa.Modules.AzureServiceBus.Activities;
|
||||
using Elsa.Modules.AzureServiceBus.Models;
|
||||
using Elsa.Runtime.Contracts;
|
||||
using Elsa.Runtime.Models;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Modules.AzureServiceBus.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Processes messages received via a queue specified through the <see cref="MessageReceivedTriggerPayload"/>.
|
||||
/// When a message is received, the appropriate workflows are executed.
|
||||
/// </summary>
|
||||
public class Worker : IAsyncDisposable
|
||||
{
|
||||
private static readonly string BookmarkName = TypeNameHelper.GenerateTypeName<MessageReceived>();
|
||||
private readonly ServiceBusProcessor _processor;
|
||||
private readonly IHasher _hasher;
|
||||
private readonly IWorkflowServer _workflowServer;
|
||||
private readonly ILogger _logger;
|
||||
private int _refCount = 1;
|
||||
|
||||
public Worker(string queueOrTopic, string? subscription, ServiceBusClient client, IHasher hasher, IWorkflowServer workflowServer, ILogger<Worker> logger)
|
||||
{
|
||||
QueueOrTopic = queueOrTopic;
|
||||
Subscription = subscription;
|
||||
_hasher = hasher;
|
||||
_workflowServer = workflowServer;
|
||||
_logger = logger;
|
||||
|
||||
var options = new ServiceBusProcessorOptions();
|
||||
var processor = subscription == null ? client.CreateProcessor(queueOrTopic, options) : client.CreateProcessor(queueOrTopic, subscription, options);
|
||||
|
||||
processor.ProcessMessageAsync += OnMessageReceivedAsync;
|
||||
processor.ProcessErrorAsync += OnErrorAsync;
|
||||
_processor = processor;
|
||||
}
|
||||
|
||||
public string QueueOrTopic { get; }
|
||||
public string? Subscription { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Maintains the number of workflows are relying on this worker. When it goes to zero, the worker will be removed.
|
||||
/// </summary>
|
||||
public int RefCount
|
||||
{
|
||||
get => _refCount;
|
||||
private set
|
||||
{
|
||||
if (value < 0)
|
||||
throw new ArgumentException("RefCount cannot be less than zero");
|
||||
|
||||
_refCount = value;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken = default) => await _processor.StartProcessingAsync(cancellationToken);
|
||||
public void IncrementRefCount() => RefCount++;
|
||||
public void DecrementRefCount() => RefCount--;
|
||||
public async ValueTask DisposeAsync() => await _processor.DisposeAsync();
|
||||
private async Task OnMessageReceivedAsync(ProcessMessageEventArgs args) => await InvokeWorkflowsAsync(args.Message, args.CancellationToken);
|
||||
|
||||
private Task OnErrorAsync(ProcessErrorEventArgs args)
|
||||
{
|
||||
_logger.LogError(args.Exception, "An error occurred while processing {EnrityPath}", args.EntityPath);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task InvokeWorkflowsAsync(ServiceBusReceivedMessage message, CancellationToken cancellationToken)
|
||||
{
|
||||
var payload = new MessageReceivedTriggerPayload(QueueOrTopic, Subscription);
|
||||
var hash = _hasher.Hash(payload);
|
||||
var stimulus = Stimulus.Standard(BookmarkName, hash);
|
||||
var executionResults = (await _workflowServer.ExecuteStimulusAsync(stimulus, cancellationToken)).ToList();
|
||||
|
||||
_logger.LogInformation("Triggered {WorkflowCount} workflows", executionResults.Count);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
using Elsa.Modules.AzureServiceBus.Contracts;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Elsa.Modules.AzureServiceBus.Services;
|
||||
|
||||
public class WorkerManager : IWorkerManager, IAsyncDisposable
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ICollection<Worker> _workers = new List<Worker>();
|
||||
|
||||
public WorkerManager(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public IReadOnlyCollection<Worker> Workers => _workers.ToList();
|
||||
public Worker? FindWorkerFor(string queueOrTopic, string? subscription) => _workers.FirstOrDefault(x => x.QueueOrTopic == queueOrTopic && x.Subscription == subscription);
|
||||
|
||||
public async Task StartWorkerAsync(string queueOrTopic, string? subscription, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var worker = FindWorkerFor(queueOrTopic, subscription);
|
||||
|
||||
if (worker != null)
|
||||
{
|
||||
worker.IncrementRefCount();
|
||||
return;
|
||||
}
|
||||
|
||||
worker = ActivatorUtilities.CreateInstance<Worker>(_serviceProvider, queueOrTopic, subscription!);
|
||||
|
||||
_workers.Add(worker);
|
||||
await worker.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task StopWorkerAsync(string queueOrTopic, string? subscription, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var worker = FindWorkerFor(queueOrTopic, subscription);
|
||||
|
||||
if (worker == null)
|
||||
return;
|
||||
|
||||
worker.DecrementRefCount();
|
||||
|
||||
if (worker.RefCount == 0) await RemoveWorkerAsync(worker);
|
||||
}
|
||||
|
||||
public async Task RemoveWorkerAsync(Worker worker)
|
||||
{
|
||||
_workers.Remove(worker);
|
||||
await worker.DisposeAsync();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
foreach (var worker in Workers) await worker.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ using System.Net.Http;
|
|||
using Elsa.Attributes;
|
||||
using Elsa.Management.Models;
|
||||
using Elsa.Models;
|
||||
using Elsa.Modules.Http.Models;
|
||||
|
||||
namespace Elsa.Modules.Http;
|
||||
|
||||
|
|
@ -19,7 +20,7 @@ public class HttpTrigger : TriggerActivity
|
|||
|
||||
[Output] public Output<HttpRequestModel>? Result { get; set; }
|
||||
|
||||
protected override IEnumerable<object> GetHashInputs(TriggerIndexingContext context) => GetHashInputs(context.ExpressionExecutionContext);
|
||||
protected override IEnumerable<object> GetPayloads(TriggerIndexingContext context) => GetHashInputs(context.ExpressionExecutionContext);
|
||||
protected override void Execute(ActivityExecutionContext context) => context.SetBookmarks(GetHashInputs(context.ExpressionExecutionContext));
|
||||
|
||||
private IEnumerable<object> GetHashInputs(ExpressionExecutionContext context)
|
||||
|
|
@ -27,6 +28,6 @@ public class HttpTrigger : TriggerActivity
|
|||
// Generate a bookmark hash for path and selected methods.
|
||||
var path = context.Get(Path);
|
||||
var methods = context.Get(SupportedMethods);
|
||||
return methods!.Select(x => (path!.ToLowerInvariant(), x.ToLowerInvariant())).Cast<object>().ToArray();
|
||||
return methods!.Select(x => new HttpTriggerPayload(path!, x.ToLowerInvariant())).Cast<object>().ToArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ using System.Text.Json;
|
|||
using System.Threading.Tasks;
|
||||
using Elsa.Contracts;
|
||||
using Elsa.Helpers;
|
||||
using Elsa.Modules.Http.Models;
|
||||
using Elsa.Runtime.Contracts;
|
||||
using Elsa.Runtime.Models;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -25,11 +26,11 @@ public class HttpTriggerMiddleware
|
|||
var path = GetPath(httpContext);
|
||||
var method = httpContext.Request.Method!.ToLowerInvariant();
|
||||
var abortToken = httpContext.RequestAborted;
|
||||
var hash = _hasher.Hash((path.ToLowerInvariant(), method.ToLowerInvariant()));
|
||||
var hash = _hasher.Hash(new HttpTriggerPayload(path, method));
|
||||
var activityTypeName = TypeNameHelper.GenerateTypeName<HttpTrigger>();
|
||||
var stimulus = Stimuli.Standard(activityTypeName, hash);
|
||||
var stimulus = Stimulus.Standard(activityTypeName, hash);
|
||||
var executionResults = (await workflowServer.ExecuteStimulusAsync(stimulus, abortToken)).ToList();
|
||||
|
||||
|
||||
if (!executionResults.Any())
|
||||
{
|
||||
await _next(httpContext);
|
||||
|
|
@ -42,12 +43,12 @@ public class HttpTriggerMiddleware
|
|||
{
|
||||
response.ContentType = "application/json";
|
||||
response.StatusCode = StatusCodes.Status200OK;
|
||||
|
||||
|
||||
var model = new
|
||||
{
|
||||
workflowInstanceIds = executionResults.Select(x => x.ExecuteWorkflowResult.WorkflowState.Id).ToArray()
|
||||
};
|
||||
|
||||
|
||||
var json = JsonSerializer.Serialize(model);
|
||||
await response.WriteAsync(json, abortToken);
|
||||
}
|
||||
|
|
|
|||
25
src/modules/Elsa.Modules.Http/Models/HttpTriggerPayload.cs
Normal file
25
src/modules/Elsa.Modules.Http/Models/HttpTriggerPayload.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
namespace Elsa.Modules.Http.Models;
|
||||
|
||||
public record HttpTriggerPayload
|
||||
{
|
||||
private readonly string _path = default!;
|
||||
private readonly string _method = default!;
|
||||
|
||||
public HttpTriggerPayload(string path, string method)
|
||||
{
|
||||
Path = path;
|
||||
Method = method;
|
||||
}
|
||||
|
||||
public string Path
|
||||
{
|
||||
get => _path;
|
||||
init => _path = value.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public string Method
|
||||
{
|
||||
get => _method;
|
||||
init => _method = value.ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
|
@ -11,4 +11,5 @@ namespace Elsa.Modules.Scheduling.Contracts;
|
|||
public interface IWorkflowTriggerScheduler
|
||||
{
|
||||
Task ScheduleTriggersAsync(IEnumerable<WorkflowTrigger> triggers, CancellationToken cancellationToken = default);
|
||||
Task UnscheduleTriggersAsync(IEnumerable<WorkflowTrigger> triggers, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ public static class ServiceCollectionExtensions
|
|||
.AddSingleton<IWorkflowBookmarkScheduler, WorkflowBookmarkScheduler>()
|
||||
.AddJobHandler<RunWorkflowJobHandler>()
|
||||
.AddJobHandler<ResumeWorkflowJobHandler>()
|
||||
.AddNotificationHandlersFrom<ScheduleWorkflowsHandler>()
|
||||
.AddNotificationHandlersFrom<ScheduleWorkflows>()
|
||||
.AddHostedService<ScheduleWorkflowsHostedService>();
|
||||
|
||||
return services;
|
||||
|
|
|
|||
|
|
@ -2,23 +2,28 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Modules.Scheduling.Contracts;
|
||||
using Elsa.Runtime.Models;
|
||||
using Elsa.Runtime.Notifications;
|
||||
|
||||
namespace Elsa.Modules.Scheduling.Handlers;
|
||||
|
||||
// Updates scheduled jobs based on the updated workflow triggers.
|
||||
public class ScheduleWorkflowsHandler : INotificationHandler<TriggerIndexingFinished>, INotificationHandler<WorkflowExecuted>
|
||||
public class ScheduleWorkflows : INotificationHandler<WorkflowTriggersIndexed>, INotificationHandler<WorkflowExecuted>
|
||||
{
|
||||
private readonly IWorkflowTriggerScheduler _workflowTriggerScheduler;
|
||||
private readonly IWorkflowBookmarkScheduler _workflowBookmarkScheduler;
|
||||
|
||||
public ScheduleWorkflowsHandler(IWorkflowTriggerScheduler workflowTriggerScheduler, IWorkflowBookmarkScheduler workflowBookmarkScheduler)
|
||||
public ScheduleWorkflows(IWorkflowTriggerScheduler workflowTriggerScheduler, IWorkflowBookmarkScheduler workflowBookmarkScheduler)
|
||||
{
|
||||
_workflowTriggerScheduler = workflowTriggerScheduler;
|
||||
_workflowBookmarkScheduler = workflowBookmarkScheduler;
|
||||
}
|
||||
|
||||
public async Task HandleAsync(TriggerIndexingFinished notification, CancellationToken cancellationToken) => await _workflowTriggerScheduler.ScheduleTriggersAsync(notification.Triggers, cancellationToken);
|
||||
public async Task HandleAsync(WorkflowTriggersIndexed notification, CancellationToken cancellationToken)
|
||||
{
|
||||
await _workflowTriggerScheduler.UnscheduleTriggersAsync(notification.IndexedWorkflow.RemovedTriggers, cancellationToken);
|
||||
await _workflowTriggerScheduler.ScheduleTriggersAsync(notification.IndexedWorkflow.AddedTriggers, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task HandleAsync(WorkflowExecuted notification, CancellationToken cancellationToken)
|
||||
{
|
||||
|
|
@ -39,7 +39,7 @@ public class ScheduleWorkflowsHostedService : BackgroundService
|
|||
|
||||
private async Task ScheduleTriggersAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var timerTriggers = (await _requestSender.RequestAsync(FindWorkflowTriggers.ForTrigger<Timer>(), cancellationToken)).ToImmutableList();
|
||||
var timerTriggers = (await _requestSender.RequestAsync(FindWorkflowTriggersByName.ForTrigger<Timer>(), cancellationToken)).ToImmutableList();
|
||||
await _workflowTriggerScheduler.ScheduleTriggersAsync(timerTriggers, cancellationToken);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,20 +17,33 @@ namespace Elsa.Modules.Scheduling.Services;
|
|||
public class WorkflowTriggerScheduler : IWorkflowTriggerScheduler
|
||||
{
|
||||
private const string RootGroupKey = "WorkflowDefinition";
|
||||
|
||||
|
||||
private readonly IJobScheduler _jobScheduler;
|
||||
|
||||
public WorkflowTriggerScheduler(IJobScheduler jobScheduler)
|
||||
{
|
||||
_jobScheduler = jobScheduler;
|
||||
}
|
||||
|
||||
|
||||
public async Task ScheduleTriggersAsync(IEnumerable<WorkflowTrigger> triggers, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var triggerList = triggers.ToList();
|
||||
|
||||
// Select all Timer triggers.
|
||||
var timerTriggers = triggerList.Filter<Timer>().ToList();
|
||||
var timerTriggers = triggers.Filter<Timer>().ToList();
|
||||
|
||||
// Schedule each trigger.
|
||||
foreach (var trigger in timerTriggers)
|
||||
{
|
||||
// Schedule trigger.
|
||||
var (dateTime, timeSpan) = JsonSerializer.Deserialize<TimerPayload>(trigger.Payload!)!;
|
||||
var groupKeys = new[] { RootGroupKey, trigger.WorkflowDefinitionId };
|
||||
await _jobScheduler.ScheduleAsync(new RunWorkflowJob(trigger.WorkflowDefinitionId), new RecurringSchedule(dateTime, timeSpan), groupKeys, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UnscheduleTriggersAsync(IEnumerable<WorkflowTrigger> triggers, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Select all Timer triggers.
|
||||
var timerTriggers = triggers.Filter<Timer>().ToList();
|
||||
|
||||
// Unschedule all triggers for the distinct set of affected workflows.
|
||||
var workflowDefinitionIds = timerTriggers.Select(x => x.WorkflowDefinitionId).Distinct().ToList();
|
||||
|
|
@ -40,13 +53,5 @@ public class WorkflowTriggerScheduler : IWorkflowTriggerScheduler
|
|||
var groupKeys = new[] { RootGroupKey, workflowDefinitionId };
|
||||
await _jobScheduler.ClearAsync(groupKeys, cancellationToken);
|
||||
}
|
||||
|
||||
foreach (var trigger in timerTriggers)
|
||||
{
|
||||
// Schedule trigger.
|
||||
var (dateTime, timeSpan) = JsonSerializer.Deserialize<TimerPayload>(trigger.Payload!)!;
|
||||
var groupKeys = new[] { RootGroupKey, trigger.WorkflowDefinitionId };
|
||||
await _jobScheduler.ScheduleAsync(new RunWorkflowJob(trigger.WorkflowDefinitionId), new RecurringSchedule(dateTime, timeSpan), groupKeys, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,4 +4,4 @@ using Elsa.Persistence.Entities;
|
|||
|
||||
namespace Elsa.Persistence.Commands;
|
||||
|
||||
public record ReplaceWorkflowTriggers(Workflow Workflow, ICollection<WorkflowTrigger> WorkflowTriggers) : ICommand;
|
||||
public record ReplaceWorkflowTriggers(Workflow Workflow, ICollection<WorkflowTrigger> Removed, ICollection<WorkflowTrigger> Added) : ICommand;
|
||||
|
|
@ -15,4 +15,19 @@ public class WorkflowBookmark : Entity
|
|||
public string? CallbackMethodName { get; set; }
|
||||
|
||||
public Bookmark ToBookmark() => new(Id, Name, Hash, Payload, ActivityId, ActivityInstanceId, CallbackMethodName);
|
||||
|
||||
public static WorkflowBookmark FromBookmark(Bookmark bookmark, WorkflowInstance workflowInstance) =>
|
||||
new()
|
||||
{
|
||||
Id = bookmark.Id,
|
||||
WorkflowDefinitionId = workflowInstance.DefinitionId,
|
||||
WorkflowInstanceId = workflowInstance.Id,
|
||||
CorrelationId = workflowInstance.CorrelationId,
|
||||
Hash = bookmark.Hash,
|
||||
Payload = bookmark.Payload,
|
||||
Name = bookmark.Name,
|
||||
ActivityId = bookmark.ActivityId,
|
||||
ActivityInstanceId = bookmark.ActivityInstanceId,
|
||||
CallbackMethodName = bookmark.CallbackMethodName
|
||||
};
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
namespace Elsa.Persistence.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a trigger associated with a workflow definition.
|
||||
/// </summary>
|
||||
public class WorkflowTrigger : Entity
|
||||
{
|
||||
public string WorkflowDefinitionId { get; set; } = default!;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
using Elsa.Contracts;
|
||||
using Elsa.Helpers;
|
||||
using Elsa.Persistence.Entities;
|
||||
|
||||
namespace Elsa.Persistence.Extensions;
|
||||
|
||||
public static class WorkflowBookmarkExtensions
|
||||
{
|
||||
public static IEnumerable<WorkflowBookmark> Filter<T>(this IEnumerable<WorkflowBookmark> bookmarks) where T : ITrigger
|
||||
{
|
||||
var bookmarkName = TypeNameHelper.GenerateTypeName<T>();
|
||||
return bookmarks.Where(x => x.Name == bookmarkName);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
using Elsa.Contracts;
|
||||
using Elsa.Helpers;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Persistence.Entities;
|
||||
|
||||
namespace Elsa.Persistence.Requests;
|
||||
|
||||
public record FindWorkflowTriggers : IRequest<IEnumerable<WorkflowTrigger>>
|
||||
{
|
||||
public FindWorkflowTriggers(string name, string? hash = default)
|
||||
{
|
||||
Name = name;
|
||||
Hash = hash;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public string? Hash { get; }
|
||||
|
||||
public static FindWorkflowTriggers ForTrigger<T>(string? hash = default) where T : ITrigger => new(TypeNameHelper.GenerateTypeName<T>(), hash);
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
using Elsa.Contracts;
|
||||
using Elsa.Helpers;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Persistence.Entities;
|
||||
|
||||
namespace Elsa.Persistence.Requests;
|
||||
|
||||
public record FindWorkflowTriggersByName(string Name, string? Hash = default) : IRequest<ICollection<WorkflowTrigger>>
|
||||
{
|
||||
public static FindWorkflowTriggersByName ForTrigger<T>(string? hash = default) where T : ITrigger => new(TypeNameHelper.GenerateTypeName<T>(), hash);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Persistence.Entities;
|
||||
|
||||
namespace Elsa.Persistence.Requests;
|
||||
|
||||
public record FindWorkflowTriggersByWorkflowDefinition(string WorkflowDefinitionId) : IRequest<ICollection<WorkflowTrigger>>;
|
||||
|
|
@ -11,6 +11,7 @@ public interface IStore<TEntity> where TEntity : Entity
|
|||
Task<TEntity?> FindAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
|
||||
Task<IEnumerable<TEntity>> FindManyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(TEntity entity, CancellationToken cancellationToken = default);
|
||||
Task<int> DeleteManyAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default);
|
||||
Task<int> DeleteWhereAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);
|
||||
IEnumerable<TEntity> Load(ElsaDbContext dbContext, IEnumerable<TEntity> entities);
|
||||
Task<IEnumerable<TEntity>> QueryAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, CancellationToken cancellationToken = default);
|
||||
|
|
|
|||
|
|
@ -12,9 +12,8 @@ public class ReplaceWorkflowTriggersHandler : ICommandHandler<ReplaceWorkflowTri
|
|||
|
||||
public async Task<Unit> HandleAsync(ReplaceWorkflowTriggers command, CancellationToken cancellationToken)
|
||||
{
|
||||
var definitionId = command.Workflow.Identity.DefinitionId;
|
||||
await _store.DeleteWhereAsync(x => x.WorkflowDefinitionId == definitionId, cancellationToken);
|
||||
await _store.SaveManyAsync(command.WorkflowTriggers, cancellationToken);
|
||||
await _store.DeleteManyAsync(command.Removed, cancellationToken);
|
||||
await _store.SaveManyAsync(command.Added, cancellationToken);
|
||||
|
||||
return Unit.Instance;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ using Elsa.Persistence.Requests;
|
|||
|
||||
namespace Elsa.Persistence.EntityFrameworkCore.Handlers.Requests;
|
||||
|
||||
public class FindWorkflowTriggersHandler : IRequestHandler<FindWorkflowTriggers, IEnumerable<WorkflowTrigger>>
|
||||
public class FindWorkflowTriggersByNameHandler : IRequestHandler<FindWorkflowTriggersByName, ICollection<WorkflowTrigger>>
|
||||
{
|
||||
private readonly IStore<WorkflowTrigger> _store;
|
||||
public FindWorkflowTriggersHandler(IStore<WorkflowTrigger> store) => _store = store;
|
||||
public FindWorkflowTriggersByNameHandler(IStore<WorkflowTrigger> store) => _store = store;
|
||||
|
||||
public async Task<IEnumerable<WorkflowTrigger>> HandleAsync(FindWorkflowTriggers request, CancellationToken cancellationToken)
|
||||
public async Task<ICollection<WorkflowTrigger>> HandleAsync(FindWorkflowTriggersByName request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await _store.QueryAsync(query =>
|
||||
var triggers = await _store.QueryAsync(query =>
|
||||
{
|
||||
query = query.Where(x => x.Name == request.Name);
|
||||
|
||||
|
|
@ -21,5 +21,7 @@ public class FindWorkflowTriggersHandler : IRequestHandler<FindWorkflowTriggers,
|
|||
|
||||
return query;
|
||||
}, cancellationToken);
|
||||
|
||||
return triggers.ToList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Persistence.Entities;
|
||||
using Elsa.Persistence.EntityFrameworkCore.Contracts;
|
||||
using Elsa.Persistence.Requests;
|
||||
|
||||
namespace Elsa.Persistence.EntityFrameworkCore.Handlers.Requests;
|
||||
|
||||
public class FindWorkflowTriggersByWorkflowDefinitionHandler : IRequestHandler<FindWorkflowTriggersByWorkflowDefinition, ICollection<WorkflowTrigger>>
|
||||
{
|
||||
private readonly IStore<WorkflowTrigger> _store;
|
||||
public FindWorkflowTriggersByWorkflowDefinitionHandler(IStore<WorkflowTrigger> store) => _store = store;
|
||||
|
||||
public async Task<ICollection<WorkflowTrigger>> HandleAsync(FindWorkflowTriggersByWorkflowDefinition request, CancellationToken cancellationToken)
|
||||
{
|
||||
var triggers = await _store.QueryAsync(query => query.Where(x => x.WorkflowDefinitionId == request.WorkflowDefinitionId), cancellationToken);
|
||||
return triggers.ToList();
|
||||
}
|
||||
}
|
||||
|
|
@ -60,13 +60,21 @@ public class EFCoreStore<TEntity> : IStore<TEntity> where TEntity : Entity
|
|||
return await dbContext.SaveChangesAsync(cancellationToken) == 1;
|
||||
}
|
||||
|
||||
public async Task<int> DeleteManyAsync(IEnumerable<TEntity> entities, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = await CreateDbContextAsync(cancellationToken);
|
||||
var list = entities.ToList();
|
||||
await dbContext.BulkDeleteAsync(list, cancellationToken: cancellationToken);
|
||||
return list.Count;
|
||||
}
|
||||
|
||||
public async Task<int> DeleteWhereAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = await CreateDbContextAsync(cancellationToken);
|
||||
var set = dbContext.Set<TEntity>();
|
||||
return await set.Where(predicate).BatchDeleteWithWorkAroundAsync(dbContext, cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<TEntity>> QueryAsync(Func<IQueryable<TEntity>, IQueryable<TEntity>> query, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = await CreateDbContextAsync(cancellationToken);
|
||||
|
|
@ -74,12 +82,12 @@ public class EFCoreStore<TEntity> : IStore<TEntity> where TEntity : Entity
|
|||
var queryable = query(set.AsQueryable());
|
||||
|
||||
queryable = query(queryable);
|
||||
|
||||
|
||||
return Load(dbContext, queryable).ToList();
|
||||
}
|
||||
|
||||
public IEnumerable<TEntity> Load(ElsaDbContext dbContext, IEnumerable<TEntity> entities) => OnLoading(dbContext, entities.ToList());
|
||||
|
||||
|
||||
private void OnSaving(ElsaDbContext dbContext, TEntity entity)
|
||||
{
|
||||
var handler = _serviceProvider.GetService<IEntitySerializer<TEntity>>();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Elsa.Mediator.Contracts;
|
||||
|
|
@ -19,9 +18,8 @@ public class ReplaceWorkflowTriggersHandler : ICommandHandler<ReplaceWorkflowTri
|
|||
|
||||
public Task<Unit> HandleAsync(ReplaceWorkflowTriggers command, CancellationToken cancellationToken)
|
||||
{
|
||||
var ids = command.WorkflowTriggers.Select(x => x.Id).ToList();
|
||||
_store.DeleteWhere(x => ids.Contains(x.Id));
|
||||
_store.SaveMany(command.WorkflowTriggers);
|
||||
_store.DeleteMany(command.Removed);
|
||||
_store.SaveMany(command.Added);
|
||||
|
||||
return Task.FromResult(Unit.Instance);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,12 @@ using Elsa.Persistence.Requests;
|
|||
|
||||
namespace Elsa.Persistence.InMemory.Handlers.Requests;
|
||||
|
||||
public class FindWorkflowTriggersHandler : IRequestHandler<FindWorkflowTriggers, IEnumerable<WorkflowTrigger>>
|
||||
public class FindWorkflowTriggersByNameHandler : IRequestHandler<FindWorkflowTriggersByName, ICollection<WorkflowTrigger>>
|
||||
{
|
||||
private readonly InMemoryStore<WorkflowTrigger> _store;
|
||||
public FindWorkflowTriggersHandler(InMemoryStore<WorkflowTrigger> store) => _store = store;
|
||||
public FindWorkflowTriggersByNameHandler(InMemoryStore<WorkflowTrigger> store) => _store = store;
|
||||
|
||||
public Task<IEnumerable<WorkflowTrigger>> HandleAsync(FindWorkflowTriggers request, CancellationToken cancellationToken)
|
||||
public Task<ICollection<WorkflowTrigger>> HandleAsync(FindWorkflowTriggersByName request, CancellationToken cancellationToken)
|
||||
{
|
||||
var triggers = _store.Query(query =>
|
||||
{
|
||||
|
|
@ -26,6 +26,6 @@ public class FindWorkflowTriggersHandler : IRequestHandler<FindWorkflowTriggers,
|
|||
return query;
|
||||
});
|
||||
|
||||
return Task.FromResult(triggers);
|
||||
return Task.FromResult<ICollection<WorkflowTrigger>>(triggers.ToList());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Persistence.Entities;
|
||||
using Elsa.Persistence.InMemory.Services;
|
||||
using Elsa.Persistence.Requests;
|
||||
|
||||
namespace Elsa.Persistence.InMemory.Handlers.Requests;
|
||||
|
||||
public class FindWorkflowTriggersByWorkflowDefinitionHandler : IRequestHandler<FindWorkflowTriggersByWorkflowDefinition, ICollection<WorkflowTrigger>>
|
||||
{
|
||||
private readonly InMemoryStore<WorkflowTrigger> _store;
|
||||
public FindWorkflowTriggersByWorkflowDefinitionHandler(InMemoryStore<WorkflowTrigger> store) => _store = store;
|
||||
|
||||
public Task<ICollection<WorkflowTrigger>> HandleAsync(FindWorkflowTriggersByWorkflowDefinition request, CancellationToken cancellationToken)
|
||||
{
|
||||
var triggers = _store.Query(query => query.Where(x => x.WorkflowDefinitionId == request.WorkflowDefinitionId));
|
||||
|
||||
return Task.FromResult<ICollection<WorkflowTrigger>>(triggers.ToList());
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ public class InMemoryStore<TEntity> where TEntity : Entity
|
|||
|
||||
public void SaveMany(IEnumerable<TEntity> entities)
|
||||
{
|
||||
foreach (var entity in entities)
|
||||
foreach (var entity in entities)
|
||||
Save(entity);
|
||||
}
|
||||
|
||||
|
|
@ -48,6 +48,20 @@ public class InMemoryStore<TEntity> where TEntity : Entity
|
|||
return count;
|
||||
}
|
||||
|
||||
public int DeleteMany(IEnumerable<TEntity> entities)
|
||||
{
|
||||
var count = 0;
|
||||
var list = entities.ToList();
|
||||
|
||||
foreach (var entity in list)
|
||||
{
|
||||
count++;
|
||||
Entities.Remove(entity.Id);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public IEnumerable<TEntity> Query(Func<IQueryable<TEntity>, IQueryable<TEntity>> query)
|
||||
{
|
||||
var queryable = Entities.Values.AsQueryable();
|
||||
|
|
|
|||
12
src/runtime/Elsa.Runtime/Contracts/IBookmarkManager.cs
Normal file
12
src/runtime/Elsa.Runtime/Contracts/IBookmarkManager.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using Elsa.Persistence.Entities;
|
||||
|
||||
namespace Elsa.Runtime.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods to add and remove bookmarks.
|
||||
/// </summary>
|
||||
public interface IBookmarkManager
|
||||
{
|
||||
Task DeleteBookmarksAsync(IEnumerable<WorkflowBookmark> bookmarks, CancellationToken cancellationToken = default);
|
||||
Task SaveBookmarksAsync(IEnumerable<WorkflowBookmark> bookmarks, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
using Elsa.Models;
|
||||
using Elsa.Persistence.Entities;
|
||||
using Elsa.Runtime.Models;
|
||||
using Elsa.Runtime.Notifications;
|
||||
|
||||
namespace Elsa.Runtime.Contracts;
|
||||
|
||||
|
|
@ -8,10 +10,10 @@ public interface ITriggerIndexer
|
|||
/// <summary>
|
||||
/// Indexes triggers from all workflows.
|
||||
/// </summary>
|
||||
Task IndexTriggersAsync(CancellationToken cancellationToken = default);
|
||||
Task<ICollection<IndexedWorkflow>> IndexTriggersAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Indexes triggers of the specified workflow.
|
||||
/// </summary>
|
||||
Task<IEnumerable<WorkflowTrigger>> IndexTriggersAsync(Workflow workflow, CancellationToken cancellationToken = default);
|
||||
Task<IndexedWorkflow> IndexTriggersAsync(Workflow workflow, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="System.Linq.Async" Version="5.1.0" />
|
||||
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ public static class ServiceCollectionExtensions
|
|||
.AddSingleton<IStimulusInterpreter, StimulusInterpreter>()
|
||||
.AddSingleton<IWorkflowInstructionExecutor, WorkflowInstructionExecutor>()
|
||||
.AddSingleton<ITriggerIndexer, TriggerIndexer>()
|
||||
.AddSingleton<IBookmarkManager, BookmarkManager>()
|
||||
|
||||
// Stimulus handlers.
|
||||
.AddStimulusHandler<TriggerWorkflowsStimulusHandler>()
|
||||
|
|
@ -44,8 +45,8 @@ public static class ServiceCollectionExtensions
|
|||
.AddNotificationHandlersFrom(typeof(ServiceCollectionExtensions))
|
||||
|
||||
// Hosted Services.
|
||||
.AddHostedService<RegisterDescriptorsHostedService>()
|
||||
.AddHostedService<RegisterExpressionSyntaxDescriptorsHostedService>();
|
||||
.AddHostedService<RegisterDescriptors>()
|
||||
.AddHostedService<RegisterExpressionSyntaxDescriptors>();
|
||||
;
|
||||
}
|
||||
|
||||
|
|
@ -60,5 +61,5 @@ public static class ServiceCollectionExtensions
|
|||
public static IServiceCollection AddStimulusHandler<T>(this IServiceCollection services) where T : class, IStimulusHandler => services.AddSingleton<IStimulusHandler, T>();
|
||||
public static IServiceCollection AddInstructionInterpreter<T>(this IServiceCollection services) where T : class, IWorkflowInstructionInterpreter => services.AddSingleton<IWorkflowInstructionInterpreter, T>();
|
||||
public static IServiceCollection ConfigureWorkflowRuntime(this IServiceCollection services, Action<WorkflowRuntimeOptions> configure) => services.Configure(configure);
|
||||
public static IServiceCollection IndexWorkflowTriggers(this IServiceCollection services) => services.AddHostedService<IndexWorkflowTriggersHostedService>();
|
||||
public static IServiceCollection IndexWorkflowTriggers(this IServiceCollection services) => services.AddHostedService<IndexWorkflowTriggers>();
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using Elsa.Contracts;
|
||||
using Elsa.Pipelines.WorkflowExecution;
|
||||
using Elsa.Runtime.Middleware;
|
||||
|
||||
namespace Elsa.Runtime.Extensions;
|
||||
|
||||
public static class WorkflowExecutionBuilderExtensions
|
||||
{
|
||||
public static IWorkflowExecutionBuilder UsePersistence(this IWorkflowExecutionBuilder builder) => builder.UseMiddleware<PersistWorkflowInstanceMiddleware>();
|
||||
public static IWorkflowExecutionBuilder UseWorkflowExecutionEvents(this IWorkflowExecutionBuilder builder) => builder.UseMiddleware<PublishWorkflowExecutionEventsMiddleware>();
|
||||
public static IWorkflowExecutionBuilder UseWorkflowExecutionLogPersistence(this IWorkflowExecutionBuilder builder) => builder.UseMiddleware<PersistWorkflowExecutionLogMiddleware>();
|
||||
}
|
||||
|
|
@ -7,11 +7,11 @@ namespace Elsa.Runtime.HostedServices;
|
|||
/// <summary>
|
||||
/// Asynchronously indexes all workflows in the background.
|
||||
/// </summary>
|
||||
public class IndexWorkflowTriggersHostedService : BackgroundService
|
||||
public class IndexWorkflowTriggers : BackgroundService
|
||||
{
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
|
||||
public IndexWorkflowTriggersHostedService(IServiceScopeFactory serviceScopeFactory)
|
||||
public IndexWorkflowTriggers(IServiceScopeFactory serviceScopeFactory)
|
||||
{
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
}
|
||||
|
|
@ -3,12 +3,12 @@ using Microsoft.Extensions.Hosting;
|
|||
|
||||
namespace Elsa.Runtime.HostedServices;
|
||||
|
||||
public class RegisterDescriptorsHostedService : IHostedService
|
||||
public class RegisterDescriptors : IHostedService
|
||||
{
|
||||
private readonly IActivityRegistryPopulator _activityRegistryPopulator;
|
||||
private readonly ITriggerRegistryPopulator _triggerRegistryPopulator;
|
||||
|
||||
public RegisterDescriptorsHostedService(IActivityRegistryPopulator activityRegistryPopulator, ITriggerRegistryPopulator triggerRegistryPopulator)
|
||||
public RegisterDescriptors(IActivityRegistryPopulator activityRegistryPopulator, ITriggerRegistryPopulator triggerRegistryPopulator)
|
||||
{
|
||||
_activityRegistryPopulator = activityRegistryPopulator;
|
||||
_triggerRegistryPopulator = triggerRegistryPopulator;
|
||||
|
|
@ -3,10 +3,10 @@ using Microsoft.Extensions.Hosting;
|
|||
|
||||
namespace Elsa.Runtime.HostedServices;
|
||||
|
||||
public class RegisterExpressionSyntaxDescriptorsHostedService : IHostedService
|
||||
public class RegisterExpressionSyntaxDescriptors : IHostedService
|
||||
{
|
||||
private readonly IExpressionSyntaxRegistryPopulator _expressionSyntaxRegistryPopulator;
|
||||
public RegisterExpressionSyntaxDescriptorsHostedService(IExpressionSyntaxRegistryPopulator expressionSyntaxRegistryPopulator) => _expressionSyntaxRegistryPopulator = expressionSyntaxRegistryPopulator;
|
||||
public RegisterExpressionSyntaxDescriptors(IExpressionSyntaxRegistryPopulator expressionSyntaxRegistryPopulator) => _expressionSyntaxRegistryPopulator = expressionSyntaxRegistryPopulator;
|
||||
public async Task StartAsync(CancellationToken cancellationToken) => await _expressionSyntaxRegistryPopulator.PopulateRegistryAsync(cancellationToken);
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ public class ResumeWorkflowInstructionInterpreter : WorkflowInstructionInterpret
|
|||
{
|
||||
_logger
|
||||
.LogWarning(
|
||||
"Workflow bookmark {WorkflowBookmarkId} for workflow definition {WorkflowDefinitionId} points to workflow instance ID {WorkflowInstanceId}, but no such workflow instance was found", workflowBookmark.Id, workflowBookmark.WorkflowDefinitionId, workflowBookmark.WorkflowInstanceId);
|
||||
"Workflow bookmark {WorkflowBookmarkId} for workflow definition {WorkflowDefinitionId} references workflow instance ID {WorkflowInstanceId}, but no such workflow instance was found", workflowBookmark.Id, workflowBookmark.WorkflowDefinitionId, workflowBookmark.WorkflowInstanceId);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ public class ResumeWorkflowInstructionInterpreter : WorkflowInstructionInterpret
|
|||
|
||||
if (workflow == null)
|
||||
{
|
||||
_logger.LogWarning("Workflow bookmark {WorkflowBookmarkId} points to workflow definition ID {WorkflowDefinitionId}, but no such workflow definition was found", workflowBookmark.Id, workflowBookmark.WorkflowDefinitionId);
|
||||
_logger.LogWarning("Workflow bookmark {WorkflowBookmarkId} references workflow definition ID {WorkflowDefinitionId}, but no such workflow definition was found", workflowBookmark.Id, workflowBookmark.WorkflowDefinitionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,12 +6,7 @@ using Elsa.Persistence.Entities;
|
|||
using Elsa.Pipelines.WorkflowExecution;
|
||||
using Elsa.Pipelines.WorkflowExecution.Components;
|
||||
|
||||
namespace Elsa.Persistence.Middleware.WorkflowExecution;
|
||||
|
||||
public static class PersistWorkflowExecutionLogMiddlewareExtensions
|
||||
{
|
||||
public static IWorkflowExecutionBuilder UseWorkflowExecutionLogPersistence(this IWorkflowExecutionBuilder builder) => builder.UseMiddleware<PersistWorkflowExecutionLogMiddleware>();
|
||||
}
|
||||
namespace Elsa.Runtime.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Takes care of persisting a workflow instance after workflow execution.
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using Elsa.Contracts;
|
||||
using Elsa.Helpers;
|
||||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Models;
|
||||
using Elsa.Persistence.Commands;
|
||||
|
|
@ -6,13 +7,9 @@ using Elsa.Persistence.Entities;
|
|||
using Elsa.Persistence.Requests;
|
||||
using Elsa.Pipelines.WorkflowExecution;
|
||||
using Elsa.Pipelines.WorkflowExecution.Components;
|
||||
using Elsa.Runtime.Contracts;
|
||||
|
||||
namespace Elsa.Persistence.Middleware.WorkflowExecution;
|
||||
|
||||
public static class PersistWorkflowInstanceMiddlewareExtensions
|
||||
{
|
||||
public static IWorkflowExecutionBuilder UsePersistence(this IWorkflowExecutionBuilder builder) => builder.UseMiddleware<PersistWorkflowInstanceMiddleware>();
|
||||
}
|
||||
namespace Elsa.Runtime.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Takes care of persisting a workflow instance after workflow execution.
|
||||
|
|
@ -22,7 +19,7 @@ public class PersistWorkflowInstanceMiddleware : WorkflowExecutionMiddleware
|
|||
private readonly IRequestSender _requestSender;
|
||||
private readonly ICommandSender _commandSender;
|
||||
private readonly IWorkflowStateSerializer _workflowStateSerializer;
|
||||
private readonly IPayloadSerializer _payloadSerializer;
|
||||
private readonly IBookmarkManager _bookmarkManager;
|
||||
private readonly IIdentityGenerator _identityGenerator;
|
||||
private readonly ISystemClock _clock;
|
||||
|
||||
|
|
@ -30,15 +27,15 @@ public class PersistWorkflowInstanceMiddleware : WorkflowExecutionMiddleware
|
|||
WorkflowMiddlewareDelegate next,
|
||||
IRequestSender requestSender,
|
||||
ICommandSender commandSender,
|
||||
IBookmarkManager bookmarkManager,
|
||||
IWorkflowStateSerializer workflowStateSerializer,
|
||||
IPayloadSerializer payloadSerializer,
|
||||
IIdentityGenerator identityGenerator,
|
||||
ISystemClock clock) : base(next)
|
||||
{
|
||||
_requestSender = requestSender;
|
||||
_commandSender = commandSender;
|
||||
_bookmarkManager = bookmarkManager;
|
||||
_workflowStateSerializer = workflowStateSerializer;
|
||||
_payloadSerializer = payloadSerializer;
|
||||
_identityGenerator = identityGenerator;
|
||||
_clock = clock;
|
||||
}
|
||||
|
|
@ -66,19 +63,14 @@ public class PersistWorkflowInstanceMiddleware : WorkflowExecutionMiddleware
|
|||
// Get a copy of current bookmarks.
|
||||
var existingBookmarks = await _requestSender.RequestAsync(new FindWorkflowBookmarks(workflowInstance.Id), cancellationToken);
|
||||
var bookmarksSnapshot = existingBookmarks.Select(x => x.ToBookmark()).ToList();
|
||||
var bookmarks = bookmarksSnapshot.ToList();
|
||||
|
||||
// Exclude the current bookmark that initiated the creation of the workflow context, if any.
|
||||
var invokedBookmark = context.Bookmark;
|
||||
var removedBookmarkIds = new List<string>();
|
||||
|
||||
if (invokedBookmark != null)
|
||||
{
|
||||
bookmarksSnapshot.RemoveAll(x => x.Id == invokedBookmark.Id);
|
||||
removedBookmarkIds.Add(invokedBookmark.Id);
|
||||
}
|
||||
if (invokedBookmark != null) bookmarks.RemoveAll(x => x.Id == invokedBookmark.Id);
|
||||
|
||||
// Apply bookmarks to workflow context.
|
||||
context.RegisterBookmarks(bookmarksSnapshot);
|
||||
context.RegisterBookmarks(bookmarks);
|
||||
|
||||
// Persist workflow instance.
|
||||
await _commandSender.ExecuteAsync(new SaveWorkflowInstance(workflowInstance), cancellationToken);
|
||||
|
|
@ -93,25 +85,15 @@ public class PersistWorkflowInstanceMiddleware : WorkflowExecutionMiddleware
|
|||
// Persist updated workflow instance.
|
||||
await _commandSender.ExecuteAsync(new SaveWorkflowInstance(workflowInstance), cancellationToken);
|
||||
|
||||
// Remove bookmarks that were in the snapshot but no longer present in context.
|
||||
removedBookmarkIds.AddRange(bookmarksSnapshot.Except(context.Bookmarks).Select(x => x.Id));
|
||||
await _commandSender.ExecuteAsync(new DeleteWorkflowBookmarks(removedBookmarkIds), cancellationToken);
|
||||
// Get a diff between bookmarks that were in the snapshot but no longer present in context.
|
||||
var diff = Diff.For(bookmarksSnapshot, context.Bookmarks.ToList());
|
||||
|
||||
// Persist bookmarks, if any.
|
||||
var workflowBookmarks = context.Bookmarks.Select(x => new WorkflowBookmark
|
||||
{
|
||||
Id = x.Id,
|
||||
WorkflowDefinitionId = definitionId,
|
||||
WorkflowInstanceId = workflowInstance.Id,
|
||||
CorrelationId = workflowInstance.CorrelationId,
|
||||
Hash = x.Hash,
|
||||
Payload = x.Payload,
|
||||
Name = x.Name,
|
||||
ActivityId = x.ActivityId,
|
||||
ActivityInstanceId = x.ActivityInstanceId,
|
||||
CallbackMethodName = x.CallbackMethodName
|
||||
}).ToList();
|
||||
// Delete removed bookmarks.
|
||||
var removedBookmarks = diff.Removed.Select(x => WorkflowBookmark.FromBookmark(x, workflowInstance)).ToList();
|
||||
await _bookmarkManager.DeleteBookmarksAsync(removedBookmarks, cancellationToken);
|
||||
|
||||
await _commandSender.ExecuteAsync(new SaveWorkflowBookmarks(workflowBookmarks), cancellationToken);
|
||||
// Persist created bookmarks.
|
||||
var createdBookmarks = diff.Added.Select(x => WorkflowBookmark.FromBookmark(x, workflowInstance)).ToList();
|
||||
await _bookmarkManager.SaveBookmarksAsync(createdBookmarks, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,11 +7,6 @@ using Elsa.Runtime.Notifications;
|
|||
|
||||
namespace Elsa.Runtime.Middleware;
|
||||
|
||||
public static class PersistWorkflowExecutionLogMiddlewareExtensions
|
||||
{
|
||||
public static IWorkflowExecutionBuilder UseWorkflowExecutionEvents(this IWorkflowExecutionBuilder builder) => builder.UseMiddleware<PublishWorkflowExecutionEventsMiddleware>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes collected bookmarks.
|
||||
/// </summary>
|
||||
|
|
|
|||
6
src/runtime/Elsa.Runtime/Models/IndexedWorkflow.cs
Normal file
6
src/runtime/Elsa.Runtime/Models/IndexedWorkflow.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
using Elsa.Models;
|
||||
using Elsa.Persistence.Entities;
|
||||
|
||||
namespace Elsa.Runtime.Models;
|
||||
|
||||
public record IndexedWorkflow(Workflow Workflow, ICollection<WorkflowTrigger> AddedTriggers, ICollection<WorkflowTrigger> RemovedTriggers);
|
||||
|
|
@ -2,7 +2,7 @@ using Elsa.Runtime.Stimuli;
|
|||
|
||||
namespace Elsa.Runtime.Models;
|
||||
|
||||
public static class Stimuli
|
||||
public static class Stimulus
|
||||
{
|
||||
public static StandardStimulus Standard(string activityTypeName, string? hash = default, IDictionary<string, object?>? data = default) => new(activityTypeName, hash, data);
|
||||
}
|
||||
10
src/runtime/Elsa.Runtime/Models/WorkflowIndexingCompleted.cs
Normal file
10
src/runtime/Elsa.Runtime/Models/WorkflowIndexingCompleted.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Runtime.Notifications;
|
||||
|
||||
namespace Elsa.Runtime.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Published when the all workflows' triggers have been indexed.
|
||||
/// </summary>
|
||||
/// <param name="IndexedWorkflows">Contains the workflows that were indexed and their resulting triggers.</param>
|
||||
public record WorkflowIndexingCompleted(ICollection<IndexedWorkflow> IndexedWorkflows) : INotification;
|
||||
10
src/runtime/Elsa.Runtime/Models/WorkflowTriggersIndexed.cs
Normal file
10
src/runtime/Elsa.Runtime/Models/WorkflowTriggersIndexed.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Runtime.Notifications;
|
||||
|
||||
namespace Elsa.Runtime.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Published when the specified workflow's triggers have been indexed.
|
||||
/// </summary>
|
||||
/// <param name="IndexedWorkflow">Contains the workflow that was indexed and the resulting triggers.</param>
|
||||
public record WorkflowTriggersIndexed(IndexedWorkflow IndexedWorkflow) : INotification;
|
||||
|
|
@ -3,4 +3,4 @@ using Elsa.Persistence.Entities;
|
|||
|
||||
namespace Elsa.Runtime.Notifications;
|
||||
|
||||
public record TriggerIndexingFinished(ICollection<WorkflowTrigger> Triggers) : INotification;
|
||||
public record WorkflowBookmarksDeleted(List<WorkflowBookmark> Bookmarks) : INotification;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Persistence.Entities;
|
||||
|
||||
namespace Elsa.Runtime.Notifications;
|
||||
|
||||
public record WorkflowBookmarksSaved(List<WorkflowBookmark> Bookmarks) : INotification;
|
||||
34
src/runtime/Elsa.Runtime/Services/BookmarkManager.cs
Normal file
34
src/runtime/Elsa.Runtime/Services/BookmarkManager.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using Elsa.Mediator.Contracts;
|
||||
using Elsa.Persistence.Commands;
|
||||
using Elsa.Persistence.Entities;
|
||||
using Elsa.Runtime.Contracts;
|
||||
using Elsa.Runtime.Notifications;
|
||||
|
||||
namespace Elsa.Runtime.Services;
|
||||
|
||||
public class BookmarkManager : IBookmarkManager
|
||||
{
|
||||
private readonly ICommandSender _commandSender;
|
||||
private readonly IEventPublisher _eventPublisher;
|
||||
|
||||
public BookmarkManager(ICommandSender commandSender, IEventPublisher eventPublisher)
|
||||
{
|
||||
_commandSender = commandSender;
|
||||
_eventPublisher = eventPublisher;
|
||||
}
|
||||
|
||||
public async Task DeleteBookmarksAsync(IEnumerable<WorkflowBookmark> bookmarks, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var list = bookmarks.ToList();
|
||||
var ids = list.Select(x => x.Id).ToList();
|
||||
await _commandSender.ExecuteAsync(new DeleteWorkflowBookmarks(ids), cancellationToken);
|
||||
await _eventPublisher.PublishAsync(new WorkflowBookmarksDeleted(list), cancellationToken);
|
||||
}
|
||||
|
||||
public async Task SaveBookmarksAsync(IEnumerable<WorkflowBookmark> bookmarks, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var list = bookmarks.ToList();
|
||||
await _commandSender.ExecuteAsync(new SaveWorkflowBookmarks(list), cancellationToken);
|
||||
await _eventPublisher.PublishAsync(new WorkflowBookmarksSaved(list), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
|
|
@ -8,7 +9,9 @@ using Elsa.Mediator.Contracts;
|
|||
using Elsa.Models;
|
||||
using Elsa.Persistence.Commands;
|
||||
using Elsa.Persistence.Entities;
|
||||
using Elsa.Persistence.Requests;
|
||||
using Elsa.Runtime.Contracts;
|
||||
using Elsa.Runtime.Models;
|
||||
using Elsa.Runtime.Notifications;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -25,6 +28,7 @@ public class TriggerIndexer : ITriggerIndexer
|
|||
private readonly IWorkflowRegistry _workflowRegistry;
|
||||
private readonly IExpressionEvaluator _expressionEvaluator;
|
||||
private readonly IIdentityGenerator _identityGenerator;
|
||||
private readonly IRequestSender _requestSender;
|
||||
private readonly ICommandSender _commandSender;
|
||||
private readonly IEventPublisher _eventPublisher;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
|
@ -35,6 +39,7 @@ public class TriggerIndexer : ITriggerIndexer
|
|||
IWorkflowRegistry workflowRegistry,
|
||||
IExpressionEvaluator expressionEvaluator,
|
||||
IIdentityGenerator identityGenerator,
|
||||
IRequestSender requestSender,
|
||||
ICommandSender commandSender,
|
||||
IEventPublisher eventPublisher,
|
||||
IServiceProvider serviceProvider,
|
||||
|
|
@ -44,6 +49,7 @@ public class TriggerIndexer : ITriggerIndexer
|
|||
_workflowRegistry = workflowRegistry;
|
||||
_expressionEvaluator = expressionEvaluator;
|
||||
_identityGenerator = identityGenerator;
|
||||
_requestSender = requestSender;
|
||||
_commandSender = commandSender;
|
||||
_eventPublisher = eventPublisher;
|
||||
_serviceProvider = serviceProvider;
|
||||
|
|
@ -51,7 +57,7 @@ public class TriggerIndexer : ITriggerIndexer
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task IndexTriggersAsync(CancellationToken cancellationToken = default)
|
||||
public async Task<ICollection<IndexedWorkflow>> IndexTriggersAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var stopwatch = new Stopwatch();
|
||||
|
||||
|
|
@ -60,37 +66,49 @@ public class TriggerIndexer : ITriggerIndexer
|
|||
|
||||
// Only stream workflows from providers that are not "dynamic" (such as DatabaseWorkflowProvider).
|
||||
var workflows = _workflowRegistry.StreamAllAsync(WorkflowRegistry.SkipDynamicProviders, cancellationToken);
|
||||
//var collectedTriggers = new List<WorkflowTrigger>();
|
||||
|
||||
// Index each workflow.
|
||||
var indexedWorkflows = new Collection<IndexedWorkflow>();
|
||||
await foreach (var workflow in workflows.WithCancellation(cancellationToken))
|
||||
{
|
||||
//var triggers = await GetTriggersAsync(workflow, cancellationToken).ToListAsync(cancellationToken);
|
||||
await IndexTriggersAsync(workflow, cancellationToken);
|
||||
var indexedWorkflow = await IndexTriggersAsync(workflow, cancellationToken);
|
||||
indexedWorkflows.Add(indexedWorkflow);
|
||||
}
|
||||
|
||||
// // Replace triggers for the specified workflow.
|
||||
// await _commandSender.ExecuteAsync(new ReplaceWorkflowTriggers(collectedTriggers), cancellationToken);
|
||||
// Publish event.
|
||||
await _eventPublisher.PublishAsync(new WorkflowIndexingCompleted(indexedWorkflows), cancellationToken);
|
||||
|
||||
stopwatch.Stop();
|
||||
_logger.LogInformation("Finished indexing workflow triggers in {ElapsedTime}", stopwatch.Elapsed);
|
||||
|
||||
// // Publish event.
|
||||
// await _eventPublisher.PublishAsync(new TriggerIndexingFinished(collectedTriggers), cancellationToken);
|
||||
return indexedWorkflows;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<WorkflowTrigger>> IndexTriggersAsync(Workflow workflow, CancellationToken cancellationToken = default)
|
||||
public async Task<IndexedWorkflow> IndexTriggersAsync(Workflow workflow, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Collect new triggers.
|
||||
var triggers = await GetTriggersAsync(workflow, cancellationToken).ToListAsync(cancellationToken);
|
||||
// Get current triggers
|
||||
var currentTriggers = await GetCurrentTriggersAsync(workflow.Identity.DefinitionId, cancellationToken);
|
||||
|
||||
// Collect new triggers **if workflow is published**.
|
||||
var newTriggers = workflow.Publication.IsPublished
|
||||
? await GetTriggersAsync(workflow, cancellationToken).ToListAsync(cancellationToken)
|
||||
: new List<WorkflowTrigger>(0);
|
||||
|
||||
// Diff triggers.
|
||||
var diff = Diff.For(currentTriggers, newTriggers);
|
||||
|
||||
// Replace triggers for the specified workflow.
|
||||
await _commandSender.ExecuteAsync(new ReplaceWorkflowTriggers(workflow, triggers), cancellationToken);
|
||||
await _commandSender.ExecuteAsync(new ReplaceWorkflowTriggers(workflow, diff.Removed, diff.Added), cancellationToken);
|
||||
|
||||
var indexedWorkflow = new IndexedWorkflow(workflow, diff.Added, diff.Removed);
|
||||
|
||||
// Publish event.
|
||||
await _eventPublisher.PublishAsync(new TriggerIndexingFinished(triggers), cancellationToken);
|
||||
return triggers;
|
||||
await _eventPublisher.PublishAsync(new WorkflowTriggersIndexed(indexedWorkflow), cancellationToken);
|
||||
return indexedWorkflow;
|
||||
}
|
||||
|
||||
private async Task<ICollection<WorkflowTrigger>> GetCurrentTriggersAsync(string workflowDefinitionId, CancellationToken cancellationToken) =>
|
||||
await _requestSender.RequestAsync(new FindWorkflowTriggersByWorkflowDefinition(workflowDefinitionId), cancellationToken);
|
||||
|
||||
private async IAsyncEnumerable<WorkflowTrigger> GetTriggersAsync(Workflow workflow, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
var context = new WorkflowIndexingContext(workflow);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ namespace Elsa.Runtime.Services;
|
|||
|
||||
/// <summary>
|
||||
/// A workflow server represents a logical unit of services to invoke workflows.
|
||||
/// It's basically a thin wrapper around a service provider built from a given service collection using <see cref="WorkflowEngineBuilder"/>.
|
||||
/// It's essentially a thin wrapper around a service provider built from a given service collection using <see cref="WorkflowEngineBuilder"/>.
|
||||
/// May be useful for scenarios where you have multiple tenants in a system or if you simply want to use different workflow engine configurations within the same application.
|
||||
/// </summary>
|
||||
public class WorkflowServer : IWorkflowServer
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public class TriggerWorkflowsStimulusHandler : StimulusHandler<StandardStimulus>
|
|||
|
||||
protected override async ValueTask<IEnumerable<IWorkflowInstruction>> GetInstructionsAsync(StandardStimulus stimulus, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var workflowTriggers = (await _mediator.RequestAsync(new FindWorkflowTriggers(stimulus.ActivityTypeName, stimulus.Hash), cancellationToken)).ToList();
|
||||
var workflowTriggers = (await _mediator.RequestAsync(new FindWorkflowTriggersByName(stimulus.ActivityTypeName, stimulus.Hash), cancellationToken)).ToList();
|
||||
return workflowTriggers.Select(x => new TriggerWorkflowInstruction(x));
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ using Elsa.Modules.Scheduling.Extensions;
|
|||
using Elsa.Modules.Scheduling.Triggers;
|
||||
using Elsa.Persistence.EntityFrameworkCore.Extensions;
|
||||
using Elsa.Persistence.EntityFrameworkCore.Sqlite;
|
||||
using Elsa.Persistence.Middleware.WorkflowExecution;
|
||||
using Elsa.Pipelines.WorkflowExecution.Components;
|
||||
using Elsa.Runtime.Middleware;
|
||||
using Elsa.Runtime.ProtoActor.Extensions;
|
||||
|
|
@ -29,6 +28,7 @@ using Microsoft.AspNetCore.Builder;
|
|||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using WorkflowExecutionBuilderExtensions = Elsa.Runtime.Extensions.WorkflowExecutionBuilderExtensions;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
|
@ -49,11 +49,13 @@ services
|
|||
.AddAzureServiceBusServices(options => configuration.GetSection("AzureServiceBus").Bind(options))
|
||||
.ConfigureWorkflowRuntime(options =>
|
||||
{
|
||||
options.Workflows.Add("HelloWorldWorkflow", new HelloWorldWorkflow());
|
||||
options.Workflows.Add("HttpWorkflow", new HttpWorkflow());
|
||||
options.Workflows.Add("ForkedHttpWorkflow", new ForkedHttpWorkflow());
|
||||
options.Workflows.Add("AzureServiceBusWorkflow", new AzureServiceBusWorkflow());
|
||||
// Register workflows.
|
||||
options.Workflows.Add(nameof(HelloWorldWorkflow), new HelloWorldWorkflow());
|
||||
options.Workflows.Add(nameof(HttpWorkflow), new HttpWorkflow());
|
||||
options.Workflows.Add(nameof(ForkedHttpWorkflow), new ForkedHttpWorkflow());
|
||||
options.Workflows.Add(nameof(CompositeActivitiesWorkflow), new CompositeActivitiesWorkflow());
|
||||
options.Workflows.Add(nameof(SendMessageWorkflow), new SendMessageWorkflow());
|
||||
options.Workflows.Add(nameof(ReceiveMessageWorkflow), new ReceiveMessageWorkflow());
|
||||
});
|
||||
|
||||
// Testing only: allow client app to connect from anywhere.
|
||||
|
|
@ -70,7 +72,7 @@ services
|
|||
.AddActivity<Delay>()
|
||||
.AddActivity<ForEach>()
|
||||
.AddActivity<Switch>()
|
||||
.AddActivity<Send>()
|
||||
.AddActivity<SendMessage>()
|
||||
;
|
||||
|
||||
// Register available triggers.
|
||||
|
|
@ -95,10 +97,7 @@ wellKnownTypeRegistry.RegisterType<bool>("boolean");
|
|||
wellKnownTypeRegistry.RegisterType<string>("string");
|
||||
|
||||
// Configure workflow engine execution pipeline.
|
||||
serviceProvider.ConfigureDefaultWorkflowExecutionPipeline(pipeline => pipeline
|
||||
.UsePersistence()
|
||||
.UseWorkflowExecutionLogPersistence()
|
||||
.UseWorkflowExecutionEvents()
|
||||
serviceProvider.ConfigureDefaultWorkflowExecutionPipeline(pipeline => WorkflowExecutionBuilderExtensions.UseWorkflowExecutionEvents(WorkflowExecutionBuilderExtensions.UseWorkflowExecutionLogPersistence(WorkflowExecutionBuilderExtensions.UsePersistence(pipeline)))
|
||||
.UseActivityScheduler()
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
using Elsa.Activities.Console;
|
||||
using Elsa.Activities.Workflows;
|
||||
using Elsa.Contracts;
|
||||
using Elsa.Models;
|
||||
using Elsa.Modules.AzureServiceBus.Activities;
|
||||
using Elsa.Modules.AzureServiceBus.Models;
|
||||
using Elsa.Runtime.Contracts;
|
||||
|
||||
namespace Elsa.Samples.Web1.Workflows;
|
||||
|
||||
public class ReceiveMessageWorkflow : IWorkflow
|
||||
{
|
||||
public void Build(IWorkflowDefinitionBuilder workflow)
|
||||
{
|
||||
workflow.AddTrigger(new MessageReceived
|
||||
{
|
||||
QueueOrTopic = new Input<string>("inbox")
|
||||
});
|
||||
|
||||
workflow.WithRoot(new Sequence
|
||||
{
|
||||
Activities =
|
||||
{
|
||||
new WriteLine("Message received!")
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -5,11 +5,11 @@ using Elsa.Runtime.Contracts;
|
|||
|
||||
namespace Elsa.Samples.Web1.Workflows;
|
||||
|
||||
public class AzureServiceBusWorkflow : IWorkflow
|
||||
public class SendMessageWorkflow : IWorkflow
|
||||
{
|
||||
public void Build(IWorkflowDefinitionBuilder workflow)
|
||||
{
|
||||
workflow.WithRoot(new Send
|
||||
workflow.WithRoot(new SendMessage
|
||||
{
|
||||
QueueOrTopic = new Input<string>("inbox"),
|
||||
MessageBody = new Input<object>(new { Subject = "Greetings", Message = "Hello World!" }),
|
||||
|
|
@ -2,13 +2,14 @@ using Elsa.Api.Extensions;
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Modules.Http.Extensions;
|
||||
using Elsa.Persistence.InMemory.Extensions;
|
||||
using Elsa.Persistence.Middleware.WorkflowExecution;
|
||||
using Elsa.Pipelines.WorkflowExecution.Components;
|
||||
using Elsa.Runtime.Middleware;
|
||||
using Elsa.Runtime.ProtoActor.Extensions;
|
||||
using Elsa.Samples.Web2.Workflows;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using WorkflowExecutionBuilderExtensions = Elsa.Runtime.Extensions.WorkflowExecutionBuilderExtensions;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
|
|
@ -26,8 +27,7 @@ builder.Services
|
|||
var app = builder.Build();
|
||||
|
||||
// Configure workflow engine execution pipeline.
|
||||
app.Services.ConfigureDefaultWorkflowExecutionPipeline(pipeline => pipeline
|
||||
.UsePersistence()
|
||||
app.Services.ConfigureDefaultWorkflowExecutionPipeline(pipeline => WorkflowExecutionBuilderExtensions.UsePersistence(pipeline)
|
||||
.UseActivityScheduler()
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ using Elsa.Extensions;
|
|||
using Elsa.Mediator.Extensions;
|
||||
using Elsa.Models;
|
||||
using Elsa.Persistence.InMemory.Extensions;
|
||||
using Elsa.Persistence.Middleware.WorkflowExecution;
|
||||
using Elsa.Pipelines.ActivityExecution.Components;
|
||||
using Elsa.Pipelines.WorkflowExecution.Components;
|
||||
using Elsa.Runtime.Extensions;
|
||||
using Elsa.Runtime.Middleware;
|
||||
using Elsa.Samples.Console1.Workflows;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
|
|||
Loading…
Reference in a new issue