Merge pull request #6326 from elsa-workflows/perf/activity-resumption
Refactor workflow execution handling to improve performance
This commit is contained in:
commit
09c31d9e45
2
.github/workflows/packages.yml
vendored
2
.github/workflows/packages.yml
vendored
|
|
@ -9,7 +9,7 @@ on:
|
|||
release:
|
||||
types: [ prereleased, published ]
|
||||
env:
|
||||
base_version: '3.3.0'
|
||||
base_version: '3.3.1'
|
||||
feedz_feed_source: 'https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json'
|
||||
nuget_feed_source: 'https://api.nuget.org/v3/index.json'
|
||||
|
||||
|
|
|
|||
|
|
@ -104,19 +104,18 @@ public class BulkDispatchWorkflows : Activity
|
|||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var waitForCompletion = WaitForCompletion.GetOrDefault(context);
|
||||
var items = context.GetItemSource<object>(Items);
|
||||
var dispatchedInstancesCount = 0;
|
||||
var items = await context.GetItemSource<object>(Items).ToListAsync(context.CancellationToken);
|
||||
var count = items.Count;
|
||||
|
||||
await foreach (var item in items)
|
||||
{
|
||||
await DispatchChildWorkflowAsync(context, item);
|
||||
dispatchedInstancesCount++;
|
||||
}
|
||||
// Dispatch the child workflows.
|
||||
foreach (var item in items)
|
||||
await DispatchChildWorkflowAsync(context, item, waitForCompletion);
|
||||
|
||||
context.SetProperty(DispatchedInstancesCountKey, dispatchedInstancesCount);
|
||||
// Store the number of dispatched instances for tracking.
|
||||
context.SetProperty(DispatchedInstancesCountKey, count);
|
||||
|
||||
// If we need to wait for the child workflows to complete (if any), create a bookmark.
|
||||
if (waitForCompletion && dispatchedInstancesCount > 0)
|
||||
if (waitForCompletion && count > 0)
|
||||
{
|
||||
var workflowInstanceId = context.WorkflowExecutionContext.Id;
|
||||
var bookmarkOptions = new CreateBookmarkArgs
|
||||
|
|
@ -125,11 +124,12 @@ public class BulkDispatchWorkflows : Activity
|
|||
Stimulus = new BulkDispatchWorkflowsStimulus(workflowInstanceId)
|
||||
{
|
||||
ParentInstanceId = context.WorkflowExecutionContext.Id,
|
||||
ScheduledInstanceIdsCount = dispatchedInstancesCount
|
||||
ScheduledInstanceIdsCount = count
|
||||
},
|
||||
IncludeActivityInstanceId = false,
|
||||
AutoBurn = false,
|
||||
};
|
||||
|
||||
context.CreateBookmark(bookmarkOptions);
|
||||
}
|
||||
else
|
||||
|
|
@ -139,9 +139,15 @@ public class BulkDispatchWorkflows : Activity
|
|||
}
|
||||
}
|
||||
|
||||
private async ValueTask<string> DispatchChildWorkflowAsync(ActivityExecutionContext context, object item)
|
||||
private async ValueTask<string> DispatchChildWorkflowAsync(ActivityExecutionContext context, object item, bool waitForCompletion)
|
||||
{
|
||||
var workflowDefinitionId = WorkflowDefinitionId.Get(context);
|
||||
var workflowDefinitionService = context.GetRequiredService<IWorkflowDefinitionService>();
|
||||
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, VersionOptions.Published);
|
||||
|
||||
if (workflowGraph == null)
|
||||
throw new($"No published version of workflow definition with ID {workflowDefinitionId} found.");
|
||||
|
||||
var parentInstanceId = context.WorkflowExecutionContext.Id;
|
||||
var input = Input.GetOrDefault(context) ?? new Dictionary<string, object>();
|
||||
var channelName = ChannelName.GetOrDefault(context);
|
||||
|
|
@ -151,6 +157,9 @@ public class BulkDispatchWorkflows : Activity
|
|||
["ParentInstanceId"] = parentInstanceId
|
||||
};
|
||||
|
||||
if (waitForCompletion)
|
||||
properties["WaitForCompletion"] = true;
|
||||
|
||||
var itemDictionary = new Dictionary<string, object>
|
||||
{
|
||||
[defaultInputItemKey] = item
|
||||
|
|
@ -168,12 +177,6 @@ public class BulkDispatchWorkflows : Activity
|
|||
var workflowDispatcher = context.GetRequiredService<IWorkflowDispatcher>();
|
||||
var identityGenerator = context.GetRequiredService<IIdentityGenerator>();
|
||||
var evaluator = context.GetRequiredService<IExpressionEvaluator>();
|
||||
var workflowDefinitionService = context.GetRequiredService<IWorkflowDefinitionService>();
|
||||
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, VersionOptions.Published);
|
||||
|
||||
if (workflowGraph == null)
|
||||
throw new Exception($"No published version of workflow definition with ID {workflowDefinitionId} found.");
|
||||
|
||||
var correlationId = CorrelationIdFunction != null ? await evaluator.EvaluateAsync<string>(CorrelationIdFunction!, context.ExpressionExecutionContext, evaluatorOptions) : null;
|
||||
var instanceId = identityGenerator.GenerateId();
|
||||
var request = new DispatchWorkflowDefinitionRequest(workflowGraph.Workflow.Identity.Id)
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ public class DispatchWorkflow : Activity<object>
|
|||
var waitForCompletion = WaitForCompletion.GetOrDefault(context);
|
||||
|
||||
// Dispatch the child workflow.
|
||||
var instanceId = await DispatchChildWorkflowAsync(context);
|
||||
var instanceId = await DispatchChildWorkflowAsync(context, waitForCompletion);
|
||||
|
||||
// If we need to wait for the child workflow to complete, create a bookmark.
|
||||
if (waitForCompletion)
|
||||
|
|
@ -92,28 +92,38 @@ public class DispatchWorkflow : Activity<object>
|
|||
}
|
||||
}
|
||||
|
||||
private async ValueTask<string> DispatchChildWorkflowAsync(ActivityExecutionContext context)
|
||||
private async ValueTask<string> DispatchChildWorkflowAsync(ActivityExecutionContext context, bool waitForCompletion)
|
||||
{
|
||||
var workflowDefinitionId = WorkflowDefinitionId.Get(context);
|
||||
var workflowDefinitionService = context.GetRequiredService<IWorkflowDefinitionService>();
|
||||
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, VersionOptions.Published, context.CancellationToken);
|
||||
|
||||
if (workflowGraph == null)
|
||||
throw new($"No published version of workflow definition with ID {workflowDefinitionId} found.");
|
||||
|
||||
var input = Input.GetOrDefault(context) ?? new Dictionary<string, object>();
|
||||
var channelName = ChannelName.GetOrDefault(context);
|
||||
var parentInstanceId = context.WorkflowExecutionContext.Id;
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["ParentInstanceId"] = parentInstanceId
|
||||
};
|
||||
|
||||
input["ParentInstanceId"] = context.WorkflowExecutionContext.Id;
|
||||
// If we need to wait for the child workflow to complete, set the property. This will be used by the ResumeDispatchWorkflowActivity handler.
|
||||
if (waitForCompletion)
|
||||
properties["WaitForCompletion"] = true;
|
||||
|
||||
input["ParentInstanceId"] = parentInstanceId;
|
||||
|
||||
var correlationId = CorrelationId.GetOrDefault(context);
|
||||
var workflowDispatcher = context.GetRequiredService<IWorkflowDispatcher>();
|
||||
var identityGenerator = context.GetRequiredService<IIdentityGenerator>();
|
||||
var workflowDefinitionService = context.GetRequiredService<IWorkflowDefinitionService>();
|
||||
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, VersionOptions.Published, context.CancellationToken);
|
||||
|
||||
if (workflowGraph == null)
|
||||
throw new Exception($"No published version of workflow definition with ID {workflowDefinitionId} found.");
|
||||
|
||||
var instanceId = identityGenerator.GenerateId();
|
||||
var request = new DispatchWorkflowDefinitionRequest(workflowGraph.Workflow.Identity.Id)
|
||||
{
|
||||
ParentWorkflowInstanceId = context.WorkflowExecutionContext.Id,
|
||||
ParentWorkflowInstanceId = parentInstanceId,
|
||||
Input = input,
|
||||
Properties = properties,
|
||||
CorrelationId = correlationId,
|
||||
InstanceId = instanceId,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ namespace Elsa.Workflows.Runtime.Activities;
|
|||
public class ExecuteWorkflow : Activity<ExecuteWorkflowResult>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public ExecuteWorkflow([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
|
||||
public ExecuteWorkflow([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ public class ExecuteWorkflow : Activity<ExecuteWorkflowResult>
|
|||
Description = "The definition ID of the workflow to execute.",
|
||||
UIHint = InputUIHints.WorkflowDefinitionPicker
|
||||
)]
|
||||
public Input<string> WorkflowDefinitionId { get; set; } = default!;
|
||||
public Input<string> WorkflowDefinitionId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The correlation ID to associate the workflow with.
|
||||
|
|
@ -40,33 +40,33 @@ public class ExecuteWorkflow : Activity<ExecuteWorkflowResult>
|
|||
DisplayName = "Correlation ID",
|
||||
Description = "The correlation ID to associate the workflow with."
|
||||
)]
|
||||
public Input<string?> CorrelationId { get; set; } = default!;
|
||||
public Input<string?> CorrelationId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The input to send to the workflow.
|
||||
/// </summary>
|
||||
[Input(Description = "The input to send to the workflow.")]
|
||||
public Input<IDictionary<string, object>?> Input { get; set; } = default!;
|
||||
|
||||
public Input<IDictionary<string, object>?> Input { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// True to wait for the child workflow to complete before completing this activity. If not set, the child workflow will be executed until it either completes or goes idle before this activity completes.
|
||||
/// </summary>
|
||||
[Input(Description = "Wait for the child workflow to complete before completing this activity.")]
|
||||
public Input<bool> WaitForCompletion { get; set; } = default!;
|
||||
public Input<bool> WaitForCompletion { get; set; } = null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var result = await ExecuteWorkflowAsync(context);
|
||||
var waitForCompletion = WaitForCompletion.Get(context);
|
||||
|
||||
if(!waitForCompletion || result.Status == WorkflowStatus.Finished)
|
||||
var result = await ExecuteWorkflowAsync(context, waitForCompletion);
|
||||
|
||||
if (!waitForCompletion || result.Status == WorkflowStatus.Finished)
|
||||
{
|
||||
context.SetResult(result);
|
||||
await context.CompleteActivityAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Since the child workflow is still running, we need to wait for it to complete using a bookmark.
|
||||
var bookmarkOptions = new CreateBookmarkArgs
|
||||
{
|
||||
|
|
@ -77,23 +77,36 @@ public class ExecuteWorkflow : Activity<ExecuteWorkflowResult>
|
|||
context.CreateBookmark(bookmarkOptions);
|
||||
}
|
||||
|
||||
private async ValueTask<ExecuteWorkflowResult> ExecuteWorkflowAsync(ActivityExecutionContext context)
|
||||
private async ValueTask<ExecuteWorkflowResult> ExecuteWorkflowAsync(ActivityExecutionContext context, bool waitForCompletion)
|
||||
{
|
||||
var workflowDefinitionId = WorkflowDefinitionId.Get(context);
|
||||
var input = Input.GetOrDefault(context) ?? new Dictionary<string, object>();
|
||||
var correlationId = CorrelationId.GetOrDefault(context);
|
||||
var workflowInvoker = context.GetRequiredService<IWorkflowInvoker>();
|
||||
var identityGenerator = context.GetRequiredService<IIdentityGenerator>();
|
||||
var workflowDefinitionService = context.GetRequiredService<IWorkflowDefinitionService>();
|
||||
var workflowGraph = await workflowDefinitionService.FindWorkflowGraphAsync(workflowDefinitionId, VersionOptions.Published, context.CancellationToken);
|
||||
|
||||
if (workflowGraph == null)
|
||||
throw new Exception($"No published version of workflow definition with ID {workflowDefinitionId} found.");
|
||||
throw new($"No published version of workflow definition with ID {workflowDefinitionId} found.");
|
||||
|
||||
var parentInstanceId = context.WorkflowExecutionContext.Id;
|
||||
var input = Input.GetOrDefault(context) ?? new Dictionary<string, object>();
|
||||
var correlationId = CorrelationId.GetOrDefault(context);
|
||||
var workflowInvoker = context.GetRequiredService<IWorkflowInvoker>();
|
||||
var identityGenerator = context.GetRequiredService<IIdentityGenerator>();
|
||||
var properties = new Dictionary<string, object>
|
||||
{
|
||||
["ParentInstanceId"] = parentInstanceId
|
||||
};
|
||||
|
||||
// If we need to wait for the child workflow to complete, set the property. This will be used by the ResumeExecuteWorkflowActivity to resume the parent workflow.
|
||||
if (waitForCompletion)
|
||||
properties["WaitForCompletion"] = true;
|
||||
|
||||
input["ParentInstanceId"] = parentInstanceId;
|
||||
|
||||
var options = new RunWorkflowOptions
|
||||
{
|
||||
ParentWorkflowInstanceId = context.WorkflowExecutionContext.Id,
|
||||
ParentWorkflowInstanceId = parentInstanceId,
|
||||
Input = input,
|
||||
Properties = properties,
|
||||
CorrelationId = correlationId,
|
||||
WorkflowInstanceId = identityGenerator.GenerateId()
|
||||
};
|
||||
|
|
@ -109,7 +122,7 @@ public class ExecuteWorkflow : Activity<ExecuteWorkflowResult>
|
|||
|
||||
return info;
|
||||
}
|
||||
|
||||
|
||||
private async ValueTask OnChildWorkflowCompletedAsync(ActivityExecutionContext context)
|
||||
{
|
||||
var input = context.WorkflowInput;
|
||||
|
|
|
|||
|
|
@ -21,11 +21,12 @@ internal class ResumeBulkDispatchWorkflowActivity(IBookmarkQueue bookmarkQueue,
|
|||
if (workflowState.Status != WorkflowStatus.Finished)
|
||||
return;
|
||||
|
||||
var parentInstanceId = workflowState.Properties.TryGetValue("ParentInstanceId", out var parentInstanceIdValue) ? parentInstanceIdValue.ToString() : default;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(parentInstanceId))
|
||||
var waitForCompletion = workflowState.Properties.TryGetValue("WaitForCompletion", out var waitForCompletionValue) && (bool)waitForCompletionValue;
|
||||
|
||||
if (!waitForCompletion)
|
||||
return;
|
||||
|
||||
|
||||
var parentInstanceId = (string)workflowState.Properties["ParentInstanceId"];
|
||||
var activityTypeName = ActivityTypeNameHelper.GenerateTypeName<BulkDispatchWorkflows>();
|
||||
var stimulus = new BulkDispatchWorkflowsStimulus(parentInstanceId);
|
||||
var stimulusHash = stimulusHasher.Hash(activityTypeName, stimulus);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using Elsa.Mediator.Contracts;
|
|||
using Elsa.Workflows.Helpers;
|
||||
using Elsa.Workflows.Notifications;
|
||||
using Elsa.Workflows.Runtime.Activities;
|
||||
using Elsa.Workflows.Runtime.Options;
|
||||
using Elsa.Workflows.Runtime.Stimuli;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
@ -16,33 +15,35 @@ namespace Elsa.Workflows.Runtime.Handlers;
|
|||
internal class ResumeDispatchWorkflowActivity(IBookmarkQueue bookmarkQueue, IStimulusHasher stimulusHasher, ILogger<ResumeDispatchWorkflowActivity> logger) : INotificationHandler<WorkflowExecuted>
|
||||
{
|
||||
private static readonly string ActivityTypeName = ActivityTypeNameHelper.GenerateTypeName<DispatchWorkflow>();
|
||||
|
||||
|
||||
public async Task HandleAsync(WorkflowExecuted notification, CancellationToken cancellationToken)
|
||||
{
|
||||
var workflowState = notification.WorkflowState;
|
||||
|
||||
logger.LogDebug("Handling workflow executed notification for workflow {WorkflowInstanceId}", notification.WorkflowState.Id);
|
||||
|
||||
if (workflowState.Status != WorkflowStatus.Finished)
|
||||
{
|
||||
logger.LogDebug("Workflow {WorkflowInstanceId} is not in a finished state. Skipping resumption of any blocking DispatchWorkflow activities", notification.WorkflowState.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var props = workflowState.Properties;
|
||||
var waitForCompletion = props.TryGetValue("WaitForCompletion", out var waitForCompletionValue) && (bool)waitForCompletionValue;
|
||||
|
||||
if (!waitForCompletion)
|
||||
return;
|
||||
|
||||
var parentInstanceId = (string)props["ParentInstanceId"];
|
||||
var stimulus = new DispatchWorkflowStimulus(notification.WorkflowState.Id);
|
||||
var input = workflowState.Output;
|
||||
|
||||
var bookmarkQueueItem = new NewBookmarkQueueItem
|
||||
{
|
||||
WorkflowInstanceId = parentInstanceId,
|
||||
ActivityTypeName = ActivityTypeName,
|
||||
StimulusHash = stimulusHasher.Hash(ActivityTypeName, stimulus),
|
||||
Options = new ResumeBookmarkOptions
|
||||
Options = new()
|
||||
{
|
||||
Input = input
|
||||
}
|
||||
};
|
||||
|
||||
logger.LogDebug("Resuming any blocking DispatchWorkflow activities for workflow {WorkflowInstanceId} using stimulus hash {StimulusHash}", notification.WorkflowState.Id, bookmarkQueueItem.StimulusHash);
|
||||
|
||||
await bookmarkQueue.EnqueueAsync(bookmarkQueueItem, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ using Elsa.Mediator.Contracts;
|
|||
using Elsa.Workflows.Helpers;
|
||||
using Elsa.Workflows.Notifications;
|
||||
using Elsa.Workflows.Runtime.Activities;
|
||||
using Elsa.Workflows.Runtime.Options;
|
||||
using Elsa.Workflows.Runtime.Stimuli;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
|
|
@ -11,7 +10,7 @@ namespace Elsa.Workflows.Runtime.Handlers;
|
|||
/// <summary>
|
||||
/// Resumes any blocking <see cref="ExecuteWorkflow"/> activities when its child workflow completes.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
[UsedImplicitly]
|
||||
internal class ResumeExecuteWorkflowActivity(IBookmarkQueue bookmarkQueue, IStimulusHasher stimulusHasher) : INotificationHandler<WorkflowExecuted>
|
||||
{
|
||||
private static readonly string ActivityTypeName = ActivityTypeNameHelper.GenerateTypeName<ExecuteWorkflow>();
|
||||
|
|
@ -23,14 +22,22 @@ internal class ResumeExecuteWorkflowActivity(IBookmarkQueue bookmarkQueue, IStim
|
|||
if (workflowState.Status != WorkflowStatus.Finished)
|
||||
return;
|
||||
|
||||
var props = workflowState.Properties;
|
||||
var waitForCompletion = props.TryGetValue("WaitForCompletion", out var waitForCompletionValue) && (bool)waitForCompletionValue;
|
||||
|
||||
if (!waitForCompletion)
|
||||
return;
|
||||
|
||||
var parentInstanceId = (string)props["ParentInstanceId"];
|
||||
var stimulus = new ExecuteWorkflowStimulus(notification.WorkflowState.Id);
|
||||
var input = workflowState.Output;
|
||||
|
||||
var bookmarkQueueItem = new NewBookmarkQueueItem
|
||||
{
|
||||
WorkflowInstanceId = parentInstanceId,
|
||||
ActivityTypeName = ActivityTypeName,
|
||||
StimulusHash = stimulusHasher.Hash(ActivityTypeName, stimulus),
|
||||
Options = new ResumeBookmarkOptions
|
||||
Options = new()
|
||||
{
|
||||
Input = input
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue