elsa-core/src/modules/Elsa.Workflows.Runtime/Services/DefaultBackgroundActivityInvoker.cs
2023-04-13 20:05:14 +02:00

123 lines
6 KiB
C#

using Elsa.Common.Models;
using Elsa.Workflows.Core.Contracts;
using Elsa.Workflows.Core.Models;
using Elsa.Workflows.Core.Services;
using Elsa.Workflows.Runtime.Contracts;
using Elsa.Workflows.Runtime.Middleware.Activities;
using Elsa.Workflows.Runtime.Models;
using Elsa.Workflows.Runtime.Models.Requests;
namespace Elsa.Workflows.Runtime.Services;
/// <summary>
/// An activity invoker that invokes activities detached from the workflow. This is useful for invoking activities from a background worker.
/// </summary>
public class DefaultBackgroundActivityInvoker : IBackgroundActivityInvoker
{
private readonly IWorkflowRuntime _workflowRuntime;
private readonly IWorkflowDispatcher _workflowDispatcher;
private readonly IWorkflowDefinitionService _workflowDefinitionService;
private readonly IWorkflowExecutionContextFactory _workflowExecutionContextFactory;
private readonly IVariablePersistenceManager _variablePersistenceManager;
private readonly IActivityInvoker _activityInvoker;
private readonly IServiceProvider _serviceProvider;
/// <summary>
/// Initializes a new instance of the <see cref="DefaultBackgroundActivityInvoker"/> class.
/// </summary>
public DefaultBackgroundActivityInvoker(
IWorkflowRuntime workflowRuntime,
IWorkflowDispatcher workflowDispatcher,
IWorkflowDefinitionService workflowDefinitionService,
IWorkflowExecutionContextFactory workflowExecutionContextFactory,
IVariablePersistenceManager variablePersistenceManager,
IActivityInvoker activityInvoker,
IServiceProvider serviceProvider)
{
_workflowRuntime = workflowRuntime;
_workflowDispatcher = workflowDispatcher;
_workflowDefinitionService = workflowDefinitionService;
_workflowExecutionContextFactory = workflowExecutionContextFactory;
_variablePersistenceManager = variablePersistenceManager;
_activityInvoker = activityInvoker;
_serviceProvider = serviceProvider;
}
/// <inheritdoc />
public async Task ExecuteAsync(ScheduledBackgroundActivity scheduledBackgroundActivity, CancellationToken cancellationToken = default)
{
var workflowInstanceId = scheduledBackgroundActivity.WorkflowInstanceId;
var workflowState = await _workflowRuntime.ExportWorkflowStateAsync(workflowInstanceId, cancellationToken);
if (workflowState == null)
throw new Exception("Workflow state not found");
var workflowDefinition = await _workflowDefinitionService.FindAsync(workflowState.DefinitionId, VersionOptions.SpecificVersion(workflowState.DefinitionVersion), cancellationToken);
if (workflowDefinition == null)
throw new Exception("Workflow definition not found");
var workflow = await _workflowDefinitionService.MaterializeWorkflowAsync(workflowDefinition, cancellationToken);
var workflowExecutionContext = await _workflowExecutionContextFactory.CreateAsync(_serviceProvider, workflow, workflowState.Id, workflowState, cancellationToken: cancellationToken);
var activityNodeId = scheduledBackgroundActivity.ActivityNodeId;
var activityExecutionContext = workflowExecutionContext.ActivityExecutionContexts.First(x => x.NodeId == activityNodeId);
// Load persistent variables for the activity to use.
await _variablePersistenceManager.LoadVariablesAsync(workflowExecutionContext);
// Mark the activity as being invoked from a background worker.
activityExecutionContext.TransientProperties[BackgroundActivityInvokerMiddleware.IsBackgroundExecution] = true;
// Invoke the activity.
await _activityInvoker.InvokeAsync(activityExecutionContext);
// Capture any activity output produced by the activity (but only if the associated memory block is stored in the workflow itself).
var outputDescriptors = activityExecutionContext.ActivityDescriptor.Outputs;
var outputValues = new Dictionary<string, object>();
foreach (var outputDescriptor in outputDescriptors)
{
var output = (Output?)outputDescriptor.ValueGetter(activityExecutionContext.Activity);
if(output == null)
continue;
var memoryBlockReference = output.MemoryBlockReference();
if(!activityExecutionContext.ExpressionExecutionContext.TryGetBlock(memoryBlockReference, out var memoryBlock))
continue;
var variableMetadata = memoryBlock.Metadata as VariableBlockMetadata;
var driver = variableMetadata?.StorageDriverType;
// We only capture output written to the workflow itself. Other drivers like blob storage, etc. will be ignored since the foreground context will be loading those.
if(driver != typeof(WorkflowStorageDriver))
continue;
var outputValue = activityExecutionContext.Get(memoryBlockReference);
if (outputValue != null)
outputValues[outputDescriptor.Name] = outputValue;
}
// Persist any variables that were written to by the activity.
await _variablePersistenceManager.SaveVariablesAsync(workflowExecutionContext);
// Resume the workflow, passing along the activity output.
// TODO: This approach will fail if the output is non-serializable. We need to find a way to pass the output to the workflow without serializing it.
var bookmarkId = scheduledBackgroundActivity.BookmarkId;
var inputKey = BackgroundActivityInvokerMiddleware.GetBackgroundActivityOutputKey(activityNodeId);
var dispatchRequest = new DispatchWorkflowInstanceRequest
{
InstanceId = workflowInstanceId,
BookmarkId = bookmarkId,
Input = new Dictionary<string, object>
{
[inputKey] = outputValues
}
};
await _workflowDispatcher.DispatchAsync(dispatchRequest, cancellationToken);
}
}