Optimize Workflow Execution and Messaging (#5243)

* Add conditional index triggers in workflow populator

The trigger indexing in the workflow populator is now conditional. A boolean parameter has been added to the PopulateStoreAsync and AddAsync methods to determine whether to index triggers or not. Additionally, some code cleanups and refactoring have been made for efficient and cleaner code.

* Update method call in DefaultWorkflowRegistry

The method `AddAsync` in `DefaultWorkflowRegistry` has been updated to include a new first parameter set to true. This change aligns with recent modifications to the `AddAsync` method signature, ensuring proper function execution.

* Add new branch triggers to GitHub workflow

The updated GitHub workflow now includes triggers for branches with 'feat/*', 'enh/*', 'perf/*', 'hotfix/*', and 'chore/*' prefixes. This is to ensure that the workflow runs not only for the main, feature, issue, bug, enhancement, patch, and fix branches, but also on all new branches, improving coverage and visibility on all changes.

* Add FindByIdAsync method to WorkflowInstanceManager

This commit introduces a new method, FindByIdAsync, to the WorkflowInstanceManager service. This method fetches a WorkflowInstance using its Id. Also, an interface declaration for the new method is added to IWorkflowInstanceManager.

* Refactor workflow definitions and add indexTriggers parameter

The code for creating workflow definition filters has been refactored for brevity. Additionally, two sets of overloaded methods named `PopulateStoreAsync` and `AddAsync` were added to "IWorkflowDefinitionStorePopulator" and implemented in "DefaultWorkflowDefinitionStorePopulator". These methods allow specifying whether triggers should be indexed.

* Refactor WorkflowDefinitionActivity code

The refactoring is focused on an improved way of finding and passing ActivityDescriptor within WorkflowDefinitionActivity class. Previously, the service provider was passed to the DeclareInputAsVariables and DeclareOutputAsVariables methods, leading to a less readable and harder to maintain code. Now, we pass the ActivityDescriptor directly, making the code easier to understand and modify.

* Update PolymorphicObjectConverter exception handling

Fixes have been applied to the PolymorphicObjectConverter by adding the handling of TargetException. Additionally, the System.Reflection namespace has been included, and the addSetMethod invocation for the HashSet has been streamlined for better readability and performance.

* Remove unnecessary whitespace in PersistWorkflowExecutionLogMiddleware

This change simply removes an unneeded line of whitespace in the corresponding Middleware file. This change is consistent with the goal of maintaining clean and easy-to-read code.

* Refactor MassTransitWorkflowDispatcher and add new methods

Systematic refactor of the MassTransitWorkflowDispatcher class which initially focused on restructuring the DispatchAsync methods. New methods have been added that deal specifically with triggering and bookmarking workflows thus enhancing the readability of the code while also improving its autonomous function. The logging for non-found workflows has been improved as well.

* Update event handler names in Workflow cache eviction

Evicting the cache prior to triggers being indexed fixes a bug where publishing workflow changes would not result in new triggers being found.

* Update Async calls and mark obsolete messages

The commit adjusts calls to AddAsync in DefaultWorkflowRegistry and DispatchAsync in DefaultWorkflowInbox to improve readability. Also, it marks DispatchResumeWorkflows and DispatchTriggerWorkflows in the Elsa.MassTransit.Messages namespace as obsolete, indicating their pending removal in future releases.

* Refactor workflow dispatch code to a separate method

The changes remove duplication and improve readability by extracting the code responsible for dispatching a workflow into a separate method called DispatchWorkflowAsync. This method creates a workflow instance, gets the send endpoint, and then sends the message.

* Refactor exception handling in PolymorphicObjectConverter

This commit simplifies the two separate catch blocks for NotSupportedException and TargetException into a single block using the new 'or' pattern in C#. It also makes minor adjustments to improve the clarity and readability of the code relating to the 'addSetMethod' invocation.

* Update src/modules/Elsa.MassTransit/Services/MassTransitWorkflowDispatcher.cs

Co-authored-by: raymonddenhaan <155616759+raymonddenhaan@users.noreply.github.com>

* Fix an attempt to dispatch bookmark ID instead of workflow instance ID

The MassTransitWorkflowDispatcher.cs file is updated to improve readability and clarity. This includes changing the way bookmark and trigger filter objects are initialized, by breaking down the single-line initialization into multiple lines. Additionally, some logic has been updated in the DispatchBookmarksAsync function for better handling of workflow instance properties and input merging.

* Add logging to SendHttpRequestBase

The SendHttpRequestBase activity in the Elsa.Http module is updated to utilize the ILogger service. This extension enables the capture of HttpRequestException and TaskCanceledException events and logs their warnings, providing insight into potential issues during HTTP request sending.

---------

Co-authored-by: raymonddenhaan <155616759+raymonddenhaan@users.noreply.github.com>
This commit is contained in:
Sipke Schoorstra 2024-04-19 01:06:52 +02:00 committed by GitHub
parent cacd8b238e
commit 568cefa629
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 215 additions and 80 deletions

View file

@ -5,11 +5,16 @@ on:
branches:
- 'main'
- 'feature/*'
- 'feat/*'
- 'issue/*'
- 'bug/*'
- 'enhancement/*'
- 'enh/*'
- 'patch/*'
- 'fix/*'
- 'perf/*'
- 'hotfix/*'
- 'chore/*'
release:
types: [ prereleased, published ]
env:

View file

@ -6,6 +6,7 @@ using Elsa.Workflows;
using Elsa.Workflows.Attributes;
using Elsa.Workflows.UIHints;
using Elsa.Workflows.Models;
using Microsoft.Extensions.Logging;
using HttpHeaders = Elsa.Http.Models.HttpHeaders;
namespace Elsa.Http;
@ -119,6 +120,7 @@ public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
private async Task TrySendAsync(ActivityExecutionContext context)
{
var request = PrepareRequest(context);
var logger = (ILogger)context.GetRequiredService(typeof(ILogger<>).MakeGenericType(GetType()));
var httpClientFactory = context.GetRequiredService<IHttpClientFactory>();
var httpClient = httpClientFactory.CreateClient(nameof(SendHttpRequestBase));
var cancellationToken = context.CancellationToken;
@ -139,12 +141,14 @@ public abstract class SendHttpRequestBase : Activity<HttpResponseMessage>
}
catch (HttpRequestException e)
{
logger.LogWarning(e, "An error occurred while sending an HTTP request");
context.AddExecutionLogEntry("Error", e.Message, payload: new { StackTrace = e.StackTrace });
context.JournalData.Add("Error", e.Message);
await HandleRequestExceptionAsync(context, e);
}
catch (TaskCanceledException e)
{
logger.LogWarning(e, "An error occurred while sending an HTTP request");
context.AddExecutionLogEntry("Error", e.Message, payload: new { StackTrace = e.StackTrace });
context.JournalData.Add("Cancelled", true);
await HandleTaskCanceledExceptionAsync(context, e);

View file

@ -3,6 +3,7 @@ using Elsa.Workflows.Serialization.Converters;
namespace Elsa.MassTransit.Messages;
[Obsolete("This message is no longer used and will be removed in a future version.")]
public class DispatchResumeWorkflows(string activityTypeName, object bookmarkPayload)
{
public string ActivityTypeName { get; init; } = activityTypeName;

View file

@ -3,6 +3,7 @@ using Elsa.Workflows.Serialization.Converters;
namespace Elsa.MassTransit.Messages;
[Obsolete("This is no longer used and will be removed in a future version.")]
public class DispatchTriggerWorkflows(string activityTypeName, object bookmarkPayload)
{
public string ActivityTypeName { get; init; } = activityTypeName;

View file

@ -1,12 +1,17 @@
using Elsa.Extensions;
using Elsa.MassTransit.Contracts;
using Elsa.MassTransit.Messages;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Management.Contracts;
using Elsa.Workflows.Management.Requests;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Filters;
using Elsa.Workflows.Runtime.Models;
using Elsa.Workflows.Runtime.Requests;
using Elsa.Workflows.Runtime.Responses;
using MassTransit;
using Microsoft.Extensions.Logging;
namespace Elsa.MassTransit.Services;
@ -17,18 +22,16 @@ public class MassTransitWorkflowDispatcher(
IBus bus,
IEndpointChannelFormatter endpointChannelFormatter,
IWorkflowDefinitionService workflowDefinitionService,
IWorkflowInstanceManager workflowInstanceManager)
IWorkflowInstanceManager workflowInstanceManager,
IBookmarkHasher bookmarkHasher,
ITriggerStore triggerStore,
IBookmarkStore bookmarkStore,
ILogger<MassTransitWorkflowDispatcher> logger)
: IWorkflowDispatcher
{
/// <inheritdoc />
public async Task<DispatchWorkflowResponse> DispatchAsync(DispatchWorkflowDefinitionRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default)
{
// When a request is received to execute a workflow, the initial step taken by our system is the creation of the workflow instance.
// The input parameters for the particular instance are immediately persisted in the database. This step is crucial for a couple of reasons:
// 1. Size constraint: It helps us prevent scenarios where the message size exceeds limits. Large messages cause the system to fail at the sending stage.
// 2. Performance: A smaller message size means less information needs to be processed and transferred, optimizing speed and efficiency.
// To create the instance, we need to find the workflow definition first.
var workflow = await workflowDefinitionService.FindWorkflowAsync(request.DefinitionId, request.VersionOptions, cancellationToken);
if (workflow == null)
@ -44,13 +47,7 @@ public class MassTransitWorkflowDispatcher(
CorrelationId = request.CorrelationId
};
// The workflow instance is created and persisted in the database.
var workflowInstance = await workflowInstanceManager.CreateWorkflowInstanceAsync(createWorkflowInstanceRequest, cancellationToken);
// The workflow instance is then dispatched for execution.
var sendEndpoint = await GetSendEndpointAsync(options);
var message = DispatchWorkflowDefinition.DispatchExistingWorkflowInstance(workflowInstance.Id, request.TriggerActivityId);
await sendEndpoint.Send(message, cancellationToken);
await DispatchWorkflowAsync(createWorkflowInstanceRequest, request.TriggerActivityId, options, cancellationToken);
return DispatchWorkflowResponse.Success();
}
@ -74,31 +71,120 @@ public class MassTransitWorkflowDispatcher(
/// <inheritdoc />
public async Task<DispatchWorkflowResponse> DispatchAsync(DispatchTriggerWorkflowsRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default)
{
var sendEndpoint = await GetSendEndpointAsync(options);
await sendEndpoint.Send(new DispatchTriggerWorkflows(request.ActivityTypeName, request.BookmarkPayload)
{
CorrelationId = request.CorrelationId,
WorkflowInstanceId = request.WorkflowInstanceId,
ActivityInstanceId = request.ActivityInstanceId,
Input = request.Input
}, cancellationToken);
await DispatchTriggersAsync(request, options, cancellationToken);
await DispatchBookmarksAsync(request, options, cancellationToken);
return DispatchWorkflowResponse.Success();
}
/// <inheritdoc />
public async Task<DispatchWorkflowResponse> DispatchAsync(DispatchResumeWorkflowsRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default)
{
var sendEndpoint = await GetSendEndpointAsync(options);
await sendEndpoint.Send(new DispatchResumeWorkflows(request.ActivityTypeName, request.BookmarkPayload)
var hash = bookmarkHasher.Hash(request.ActivityTypeName, request.BookmarkPayload, request.ActivityInstanceId);
var correlationId = request.CorrelationId;
var workflowInstanceId = request.WorkflowInstanceId;
var activityInstanceId = request.ActivityInstanceId;
var filter = new BookmarkFilter
{
CorrelationId = request.CorrelationId,
WorkflowInstanceId = request.WorkflowInstanceId,
ActivityInstanceId = request.ActivityInstanceId,
Input = request.Input
}, cancellationToken);
Hash = hash,
CorrelationId = correlationId,
WorkflowInstanceId = workflowInstanceId,
ActivityInstanceId = activityInstanceId
};
var bookmarks = await bookmarkStore.FindManyAsync(filter, cancellationToken);
await DispatchBookmarksAsync(bookmarks, request.Input, null, options, cancellationToken);
return DispatchWorkflowResponse.Success();
}
private async Task DispatchTriggersAsync(DispatchTriggerWorkflowsRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default)
{
var triggerHash = bookmarkHasher.Hash(request.ActivityTypeName, request.BookmarkPayload);
var triggerFilter = new TriggerFilter
{
Hash = triggerHash
};
var triggers = (await triggerStore.FindManyAsync(triggerFilter, cancellationToken)).ToList();
foreach (var trigger in triggers)
{
var workflow = await workflowDefinitionService.FindWorkflowAsync(trigger.WorkflowDefinitionVersionId, cancellationToken);
if (workflow == null)
{
logger.LogWarning("Workflow definition with ID '{WorkflowDefinitionId}' not found", trigger.WorkflowDefinitionVersionId);
continue;
}
var createWorkflowInstanceRequest = new CreateWorkflowInstanceRequest
{
Workflow = workflow,
WorkflowInstanceId = request.WorkflowInstanceId,
Input = request.Input,
Properties = request.Properties,
CorrelationId = request.CorrelationId
};
await DispatchWorkflowAsync(createWorkflowInstanceRequest, trigger.ActivityId, options, cancellationToken);
}
}
private async Task DispatchWorkflowAsync(CreateWorkflowInstanceRequest createWorkflowInstanceRequest, string? triggerActivityId, DispatchWorkflowOptions? options, CancellationToken cancellationToken)
{
var workflowInstance = await workflowInstanceManager.CreateWorkflowInstanceAsync(createWorkflowInstanceRequest, cancellationToken);
var sendEndpoint = await GetSendEndpointAsync(options);
var message = DispatchWorkflowDefinition.DispatchExistingWorkflowInstance(workflowInstance.Id, triggerActivityId);
await sendEndpoint.Send(message, cancellationToken);
}
private async Task DispatchBookmarksAsync(DispatchTriggerWorkflowsRequest request, DispatchWorkflowOptions? options = default, CancellationToken cancellationToken = default)
{
var correlationId = request.CorrelationId;
var workflowInstanceId = request.WorkflowInstanceId;
var activityInstanceId = request.ActivityInstanceId;
var bookmarkHash = bookmarkHasher.Hash(request.ActivityTypeName, request.BookmarkPayload, activityInstanceId);
var filter = new BookmarkFilter
{
Hash = bookmarkHash,
CorrelationId = correlationId,
WorkflowInstanceId = workflowInstanceId,
ActivityInstanceId = activityInstanceId
};
var bookmarks = (await bookmarkStore.FindManyAsync(filter, cancellationToken)).ToList();
await DispatchBookmarksAsync(bookmarks, request.Input, request.Properties, options, cancellationToken);
}
private async Task DispatchBookmarksAsync(IEnumerable<StoredBookmark> bookmarks, IDictionary<string, object>? input, IDictionary<string, object>? properties, DispatchWorkflowOptions? options, CancellationToken cancellationToken)
{
foreach (var bookmark in bookmarks)
{
var workflowInstanceId = bookmark.WorkflowInstanceId;
if (input != null || properties != null)
{
var workflowInstance = await workflowInstanceManager.FindByIdAsync(workflowInstanceId, cancellationToken);
if (workflowInstance == null)
{
logger.LogWarning("Workflow instance with ID '{WorkflowInstanceId}' not found", workflowInstanceId);
continue;
}
if (input != null) workflowInstance.WorkflowState.Input.Merge(input);
if (properties != null) workflowInstance.WorkflowState.Properties.Merge(properties);
await workflowInstanceManager.SaveAsync(workflowInstance, cancellationToken);
}
var dispatchInstanceRequest = new DispatchWorkflowInstanceRequest(workflowInstanceId)
{
BookmarkId = bookmark.BookmarkId,
CorrelationId = bookmark.CorrelationId
};
await DispatchAsync(dispatchInstanceRequest, options, cancellationToken);
}
}
private async Task<ISendEndpoint> GetSendEndpointAsync(DispatchWorkflowOptions? options = default)
{
var endpointName = endpointChannelFormatter.FormatEndpointName(options?.Channel);

View file

@ -1,5 +1,7 @@
using System.Collections;
using System.Dynamic;
using System.Reflection;
using System.Runtime;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
@ -47,7 +49,7 @@ public class PolymorphicObjectConverter : JsonConverter<object>
{
return JsonSerializer.Deserialize(ref reader, targetType, newOptions)!;
}
catch (NotSupportedException e)
catch (Exception e) when (e is NotSupportedException or TargetException)
{
return default!;
}

View file

@ -85,8 +85,9 @@ public class WorkflowDefinitionActivity : Composite, IInitializable
private void CopyInputOutputToVariables(ActivityExecutionContext context)
{
var serviceProvider = context.GetRequiredService<IServiceProvider>();
var activityDescriptor = FindActivityDescriptor(serviceProvider);
DeclareInputAsVariables(serviceProvider, (descriptor, variable) =>
DeclareInputAsVariables(activityDescriptor, (descriptor, variable) =>
{
var inputName = descriptor.Name;
var input = SyntheticProperties.TryGetValue(inputName, out var inputValue) ? (Input?)inputValue : default;
@ -96,14 +97,11 @@ public class WorkflowDefinitionActivity : Composite, IInitializable
variable.Set(context, evaluatedExpression);
});
DeclareOutputAsVariables(serviceProvider, (descriptor, variable) => context.ExpressionExecutionContext.Memory.Declare(variable));
DeclareOutputAsVariables(activityDescriptor, (descriptor, variable) => context.ExpressionExecutionContext.Memory.Declare(variable));
}
private void DeclareInputAsVariables(IServiceProvider serviceProvider, Action<InputDescriptor, Variable> configureVariable)
private void DeclareInputAsVariables(ActivityDescriptor activityDescriptor, Action<InputDescriptor, Variable> configureVariable)
{
var activityRegistry = serviceProvider.GetRequiredService<IActivityRegistry>();
var activityDescriptor = activityRegistry.Find(Type, Version)!;
foreach (var inputDescriptor in activityDescriptor.Inputs)
{
var inputName = inputDescriptor.Name;
@ -119,11 +117,8 @@ public class WorkflowDefinitionActivity : Composite, IInitializable
}
}
private void DeclareOutputAsVariables(IServiceProvider serviceProvider, Action<OutputDescriptor, Variable> configureVariable)
private void DeclareOutputAsVariables(ActivityDescriptor activityDescriptor, Action<OutputDescriptor, Variable> configureVariable)
{
var activityRegistry = serviceProvider.GetRequiredService<IActivityRegistry>();
var activityDescriptor = activityRegistry.Find(Type, Version)!;
foreach (var outputDescriptor in activityDescriptor.Outputs)
{
var outputName = outputDescriptor.Name;
@ -156,6 +151,12 @@ public class WorkflowDefinitionActivity : Composite, IInitializable
return workflow;
}
private ActivityDescriptor FindActivityDescriptor(IServiceProvider serviceProvider)
{
var activityRegistry = serviceProvider.GetRequiredService<IActivityRegistry>();
return activityRegistry.Find(Type, Version) ?? activityRegistry.Find(Type) ?? throw new Exception($"Could not find activity descriptor for {Type}.");
}
async ValueTask IInitializable.InitializeAsync(InitializationContext context)
{
@ -166,9 +167,11 @@ public class WorkflowDefinitionActivity : Composite, IInitializable
if (workflow == null)
throw new Exception($"Could not find workflow definition with ID {WorkflowDefinitionId}.");
var activityDescriptor = FindActivityDescriptor(serviceProvider);
// Declare input and output variables.
DeclareInputAsVariables(serviceProvider, (_, variable) => Variables.Declare(variable));
DeclareOutputAsVariables(serviceProvider, (_, variable) => Variables.Declare(variable));
DeclareInputAsVariables(activityDescriptor, (_, variable) => Variables.Declare(variable));
DeclareOutputAsVariables(activityDescriptor, (_, variable) => Variables.Declare(variable));
// Set the root activity.
Root = workflow;

View file

@ -10,6 +10,11 @@ namespace Elsa.Workflows.Management.Contracts;
/// </summary>
public interface IWorkflowInstanceManager
{
/// <summary>
/// Retrieves the workflow instance with the specified ID.
/// </summary>
Task<WorkflowInstance?> FindByIdAsync(string id, CancellationToken cancellationToken = default);
/// <summary>
/// Saves the specified workflow instance.
/// </summary>

View file

@ -13,31 +13,31 @@ namespace Elsa.Workflows.Management.Handlers;
/// </remarks>
[UsedImplicitly]
internal class EvictWorkflowDefinitionServiceCache(IWorkflowDefinitionCacheManager workflowDefinitionCacheManager) :
INotificationHandler<WorkflowDefinitionPublished>,
INotificationHandler<WorkflowDefinitionRetracted>,
INotificationHandler<WorkflowDefinitionDeleted>,
INotificationHandler<WorkflowDefinitionsDeleted>
INotificationHandler<WorkflowDefinitionPublishing>,
INotificationHandler<WorkflowDefinitionRetracting>,
INotificationHandler<WorkflowDefinitionDeleting>,
INotificationHandler<WorkflowDefinitionsDeleting>
{
/// <inheritdoc />
public async Task HandleAsync(WorkflowDefinitionPublished notification, CancellationToken cancellationToken)
public async Task HandleAsync(WorkflowDefinitionPublishing notification, CancellationToken cancellationToken)
{
await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken);
}
/// <inheritdoc />
public async Task HandleAsync(WorkflowDefinitionRetracted notification, CancellationToken cancellationToken)
public async Task HandleAsync(WorkflowDefinitionRetracting notification, CancellationToken cancellationToken)
{
await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.WorkflowDefinition.DefinitionId, cancellationToken);
}
/// <inheritdoc />
public async Task HandleAsync(WorkflowDefinitionDeleted notification, CancellationToken cancellationToken)
public async Task HandleAsync(WorkflowDefinitionDeleting notification, CancellationToken cancellationToken)
{
await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(notification.DefinitionId, cancellationToken);
}
/// <inheritdoc />
public async Task HandleAsync(WorkflowDefinitionsDeleted notification, CancellationToken cancellationToken)
public async Task HandleAsync(WorkflowDefinitionsDeleting notification, CancellationToken cancellationToken)
{
foreach (var definitionId in notification.DefinitionIds)
await workflowDefinitionCacheManager.EvictWorkflowDefinitionAsync(definitionId, cancellationToken);

View file

@ -51,21 +51,14 @@ public class WorkflowDefinitionService : IWorkflowDefinitionService
/// <inheritdoc />
public async Task<WorkflowDefinition?> FindWorkflowDefinitionAsync(string definitionId, VersionOptions versionOptions, CancellationToken cancellationToken = default)
{
var filter = new WorkflowDefinitionFilter
{
DefinitionId = definitionId,
VersionOptions = versionOptions
};
var filter = new WorkflowDefinitionFilter { DefinitionId = definitionId, VersionOptions = versionOptions };
return await _workflowDefinitionStore.FindAsync(filter, cancellationToken);
}
/// <inheritdoc />
public async Task<WorkflowDefinition?> FindWorkflowDefinitionAsync(string definitionVersionId, CancellationToken cancellationToken = default)
{
var filter = new WorkflowDefinitionFilter
{
Id = definitionVersionId
};
var filter = new WorkflowDefinitionFilter { Id = definitionVersionId };
return await _workflowDefinitionStore.FindAsync(filter, cancellationToken);
}

View file

@ -1,4 +1,4 @@
using Elsa.Common.Contracts;
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Contracts;
using Elsa.Workflows.Management.Contracts;
@ -8,6 +8,7 @@ using Elsa.Workflows.Management.Mappers;
using Elsa.Workflows.Management.Notifications;
using Elsa.Workflows.Management.Requests;
using Elsa.Workflows.State;
using Exception = System.Exception;
namespace Elsa.Workflows.Management.Services;
@ -21,6 +22,12 @@ public class WorkflowInstanceManager(
IWorkflowStateSerializer workflowStateSerializer)
: IWorkflowInstanceManager
{
/// <inheritdoc />
public async Task<WorkflowInstance?> FindByIdAsync(string id, CancellationToken cancellationToken = default)
{
return await store.FindAsync(id, cancellationToken);
}
/// <inheritdoc />
public async Task SaveAsync(WorkflowInstance workflowInstance, CancellationToken cancellationToken = default)
{

View file

@ -13,6 +13,13 @@ public interface IWorkflowDefinitionStorePopulator
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
Task PopulateStoreAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Populates the <see cref="IWorkflowDefinitionStore"/> with workflow definitions provided from <see cref="IWorkflowProvider"/> implementations.
/// </summary>
/// <param name="indexTriggers">Whether to index triggers.</param>
/// <param name="cancellationToken">The cancellation token.</param>
Task PopulateStoreAsync(bool indexTriggers, CancellationToken cancellationToken = default);
/// <summary>
/// Adds a workflow definition to the store.
@ -20,4 +27,12 @@ public interface IWorkflowDefinitionStorePopulator
/// <param name="materializedWorkflow">A materialized workflow.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
Task AddAsync(MaterializedWorkflow materializedWorkflow, CancellationToken cancellationToken = default);
/// <summary>
/// Adds a workflow definition to the store.
/// </summary>
/// <param name="materializedWorkflow">A materialized workflow.</param>
/// /// <param name="indexTriggers">Whether to index triggers.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
Task AddAsync(MaterializedWorkflow materializedWorkflow, bool indexTriggers, CancellationToken cancellationToken = default);
}

View file

@ -59,7 +59,7 @@ public class PersistWorkflowExecutionLogMiddleware : WorkflowExecutionMiddleware
}).ToList();
await _workflowExecutionLogStore.AddManyAsync(entries, context.CancellationTokens.SystemCancellationToken);
// Publish notification.
await _notificationSender.SendAsync(new WorkflowExecutionLogUpdated(context), context.CancellationTokens.SystemCancellationToken);
}

View file

@ -27,7 +27,7 @@ public class DefaultRegistriesPopulator : IRegistriesPopulator
await _activityRegistryPopulator.PopulateRegistryAsync(cancellationToken);
// Stage 2: Populate the workflow definition store.
await _workflowDefinitionStorePopulator.PopulateStoreAsync(cancellationToken);
await _workflowDefinitionStorePopulator.PopulateStoreAsync(false, cancellationToken);
// Stage 3: Re-populate the activity registry.
// After the workflow definition store has been populated, we need to re-populate the activity registry to make sure that the activity descriptors are up-to-date.
@ -35,6 +35,6 @@ public class DefaultRegistriesPopulator : IRegistriesPopulator
// Stage 4. Re-update the workflow definition store with the current set of activities.
// Finally, we need to re-populate the workflow definition store to make sure that the workflow definitions are up-to-date.
await _workflowDefinitionStorePopulator.PopulateStoreAsync(cancellationToken);
await _workflowDefinitionStorePopulator.PopulateStoreAsync(true, cancellationToken);
}
}

View file

@ -49,23 +49,37 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP
}
/// <inheritdoc />
public async Task PopulateStoreAsync(CancellationToken cancellationToken = default)
public Task PopulateStoreAsync(CancellationToken cancellationToken = default)
{
return PopulateStoreAsync(true, cancellationToken);
}
/// <inheritdoc />
public async Task PopulateStoreAsync(bool indexTriggers, CancellationToken cancellationToken = default)
{
var providers = _workflowDefinitionProviders();
foreach (var provider in providers)
{
var results = await provider.GetWorkflowsAsync(cancellationToken).AsTask().ToList();
foreach (var result in results) await AddAsync(result, cancellationToken);
foreach (var result in results) await AddAsync(result, indexTriggers, cancellationToken);
}
}
/// <inheritdoc />
public async Task AddAsync(MaterializedWorkflow materializedWorkflow, CancellationToken cancellationToken = default)
public Task AddAsync(MaterializedWorkflow materializedWorkflow, CancellationToken cancellationToken = default)
{
return AddAsync(materializedWorkflow, true, cancellationToken);
}
/// <inheritdoc />
public async Task AddAsync(MaterializedWorkflow materializedWorkflow, bool indexTriggers, CancellationToken cancellationToken = default)
{
await AssignIdentities(materializedWorkflow.Workflow, cancellationToken);
await AddOrUpdateAsync(materializedWorkflow, cancellationToken);
await IndexTriggersAsync(materializedWorkflow, cancellationToken);
if (indexTriggers)
await IndexTriggersAsync(materializedWorkflow, cancellationToken);
}
private async Task AssignIdentities(Workflow workflow, CancellationToken cancellationToken)
@ -145,33 +159,32 @@ public class DefaultWorkflowDefinitionStorePopulator : IWorkflowDefinitionStoreP
workflowDefinition.ProviderName = materializedWorkflow.ProviderName;
workflowDefinition.MaterializerContext = materializerContextJson;
workflowDefinition.MaterializerName = materializedWorkflow.MaterializerName;
if (existingDefinitionVersion is null
&& workflowDefinitionsToSave.Any(w => w.Id == workflowDefinition.Id))
if (existingDefinitionVersion is null && workflowDefinitionsToSave.Any(w => w.Id == workflowDefinition.Id))
{
_logger.LogError("Trying to create a new workflow with existing id {workflowId}", workflowDefinition.Id);
_logger.LogInformation("Workflow with ID {WorkflowId} already exists", workflowDefinition.Id);
return;
}
workflowDefinitionsToSave.Add(workflowDefinition);
var duplicates = workflowDefinitionsToSave.GroupBy(wd => wd.Id)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();
if (duplicates.Any())
{
throw new Exception($"Unable to update WorkflowDefinition with ids {string.Join(',', duplicates)} multiple times.");
}
await _workflowDefinitionStore.SaveManyAsync(workflowDefinitionsToSave, cancellationToken);
return;
async Task UpdateIsLatest()
{
// Always try to update the IsLatest property based on the VersionNumber
// Reset current latest definitions.
var filter = new WorkflowDefinitionFilter
{

View file

@ -127,15 +127,15 @@ public class DefaultWorkflowInbox : IWorkflowInbox
return new DeliverWorkflowInboxMessageResult(results.TriggeredWorkflows);
}
await _workflowDispatcher.DispatchAsync(new DispatchTriggerWorkflowsRequest(activityTypeName, bookmarkPayload)
var dispatchRequest = new DispatchTriggerWorkflowsRequest(activityTypeName, bookmarkPayload)
{
CorrelationId = correlationId,
WorkflowInstanceId = workflowInstanceId,
ActivityInstanceId = activityInstanceId,
Input = input
}, cancellationToken: cancellationToken);
};
await _workflowDispatcher.DispatchAsync(dispatchRequest, cancellationToken);
return new DeliverWorkflowInboxMessageResult(new List<WorkflowExecutionResult>());
}