diff --git a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs index c91f94870..be3520236 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/WorkflowExecutionContext.cs @@ -16,18 +16,14 @@ using Microsoft.Extensions.DependencyInjection; namespace Elsa.Workflows; -/// /// A delegate entry that is used by activities to be notified when the activities they scheduled are completed. -/// /// The activity scheduling the activity. /// The child being scheduled. /// The delegate to invoke when the scheduled activity completes. /// An optional tag. public record ActivityCompletionCallbackEntry(ActivityExecutionContext Owner, ActivityNode Child, ActivityCompletionCallback? CompletionCallback, object? Tag = default); -/// /// Provides context to the currently executing workflow. -/// [PublicAPI] public partial class WorkflowExecutionContext : IExecutionContext { @@ -40,9 +36,7 @@ public partial class WorkflowExecutionContext : IExecutionContext private readonly IHasher _hasher; private readonly Action? _statusUpdatedCallback; - /// /// Initializes a new instance of . - /// private WorkflowExecutionContext( IServiceProvider serviceProvider, WorkflowGraph workflowGraph, @@ -87,9 +81,7 @@ public partial class WorkflowExecutionContext : IExecutionContext _cancellationRegistrations.Add(sysSource.Token.Register(CancelWorkflow)); } - /// /// Creates a new for the specified workflow. - /// public static async Task CreateAsync( IServiceProvider serviceProvider, WorkflowGraph workflowGraph, @@ -122,9 +114,7 @@ public partial class WorkflowExecutionContext : IExecutionContext ); } - /// /// Creates a new for the specified workflow. - /// public static async Task CreateAsync( IServiceProvider serviceProvider, WorkflowGraph workflowGraph, @@ -159,9 +149,7 @@ public partial class WorkflowExecutionContext : IExecutionContext return workflowExecutionContext; } - /// /// Creates a new for the specified workflow. - /// public static async Task CreateAsync( IServiceProvider serviceProvider, WorkflowGraph workflowGraph, @@ -202,9 +190,7 @@ public partial class WorkflowExecutionContext : IExecutionContext return workflowExecutionContext; } - /// /// Assigns the specified workflow to this workflow execution context. - /// /// The workflow graph to assign. public async Task SetWorkflowGraphAsync(WorkflowGraph workflowGraph) { @@ -220,247 +206,157 @@ public partial class WorkflowExecutionContext : IExecutionContext activityExecutionContext.Activity = workflowGraph.NodeIdLookup[activityExecutionContext.Activity.NodeId].Activity; } - /// /// Gets the . - /// public IServiceProvider ServiceProvider { get; } - /// /// Gets the . - /// public IActivityRegistry ActivityRegistry { get; } - /// /// Gets the . - /// public IActivityRegistryLookupService ActivityRegistryLookup { get; } - /// /// Gets the workflow graph. - /// public WorkflowGraph WorkflowGraph { get; private set; } - /// /// The associated with the execution context. - /// public Workflow Workflow => WorkflowGraph.Workflow; - /// /// A graph of the workflow structure. - /// public ActivityNode Graph => WorkflowGraph.Root; - /// - /// The current status of the workflow. - /// + /// The current status of the workflow. public WorkflowStatus Status => GetMainStatus(SubStatus); - /// /// The current sub status of the workflow. - /// public WorkflowSubStatus SubStatus { get; internal set; } - /// /// The root associated with the execution context. - /// public MemoryRegister MemoryRegister { get; private set; } = default!; - /// /// A unique ID of the execution context. - /// public string Id { get; set; } /// public IActivity Activity => Workflow; - /// /// An application-specific identifier associated with the execution context. - /// public string? CorrelationId { get; set; } - /// /// The ID of the workflow instance that triggered this instance. - /// public string? ParentWorkflowInstanceId { get; set; } - /// /// The date and time the workflow execution context was created. - /// public DateTimeOffset CreatedAt { get; set; } - /// /// The date and time the workflow execution context was last updated. - /// public DateTimeOffset UpdatedAt { get; set; } - /// /// The date and time the workflow execution context has finished. - /// public DateTimeOffset? FinishedAt { get; set; } - /// /// Gets the clock used to determine the current time. - /// public ISystemClock SystemClock { get; } - /// /// A flattened list of s from the . - /// public IReadOnlyCollection Nodes => WorkflowGraph.Nodes.ToList(); - /// /// A map between activity IDs and s in the workflow graph. - /// public IDictionary NodeIdLookup => WorkflowGraph.NodeIdLookup; - /// /// A map between hashed activity node IDs and s in the workflow graph. - /// public IDictionary NodeHashLookup => WorkflowGraph.NodeHashLookup; - /// /// A map between s and s in the workflow graph. - /// public IDictionary NodeActivityLookup => WorkflowGraph.NodeActivityLookup; - /// /// The for the execution context. - /// public IActivityScheduler Scheduler { get; } - /// /// Gets the . - /// public IIdentityGenerator IdentityGenerator { get; } - /// /// A collection of collected bookmarks during workflow execution. - /// public ICollection Bookmarks { get; set; } = new List(); - /// /// A dictionary of inputs provided at the start of the current workflow execution. - /// public IDictionary Input { get; set; } - /// /// A dictionary of outputs provided by the current workflow execution. - /// public IDictionary Output { get; set; } = new Dictionary(); /// public IDictionary Properties { get; set; } - /// /// A dictionary that can be used by application code and middleware to store information and even services. Values do not need to be serializable. /// All data will be gone once workflow execution completes. - /// public IDictionary TransientProperties { get; set; } = new Dictionary(); - /// /// A collection of incidents that may have occurred during execution. - /// public ICollection Incidents { get; set; } - /// /// The current delegate to invoke when executing the next activity. - /// public ExecuteActivityDelegate? ExecuteDelegate { get; set; } - /// /// Provides context about the bookmark that was used to resume workflow execution, if any. - /// public ResumedBookmarkContext? ResumedBookmarkContext { get; set; } - /// /// The ID of the activity associated with the trigger that caused this workflow execution, if any. - /// public string? TriggerActivityId { get; set; } - /// /// A set of cancellation tokens that can be used to cancel the workflow execution without cancelling system-level operations. - /// public CancellationTokens CancellationTokens { get; } - /// /// A list of callbacks that are invoked when the associated child activity completes. - /// public ICollection CompletionCallbacks => new ReadOnlyCollection(_completionCallbackEntries); - /// /// A list of s that are currently active. - /// public IReadOnlyCollection ActivityExecutionContexts { get => _activityExecutionContexts.ToList(); internal set => _activityExecutionContexts = value.ToList(); } - /// /// The last execution log sequence number. This number is incremented every time a new entry is added to the execution log and is persisted alongside the workflow instance and restored when the workflow is resumed. - /// public long ExecutionLogSequence { get; set; } - /// /// A collection of execution log entries. This collection is flushed when the workflow execution context ends. - /// public ICollection ExecutionLog { get; } = new List(); - /// /// The expression execution context for the current workflow execution. - /// public ExpressionExecutionContext? ExpressionExecutionContext { get; private set; } /// public IEnumerable Variables => Workflow.Variables; - /// /// Resolves the specified service type from the service provider. - /// public T GetRequiredService() where T : notnull => ServiceProvider.GetRequiredService(); - /// /// Resolves the specified service type from the service provider. - /// public object GetRequiredService(Type serviceType) => ServiceProvider.GetRequiredService(serviceType); - /// /// Resolves the specified service type from the service provider, or creates a new instance if the service type was not found in the service container. - /// public T GetOrCreateService() where T : notnull => ActivatorUtilities.GetServiceOrCreateInstance(ServiceProvider); - /// /// Resolves the specified service type from the service provider, or creates a new instance if the service type was not found in the service container. - /// public object GetOrCreateService(Type serviceType) => ActivatorUtilities.GetServiceOrCreateInstance(ServiceProvider, serviceType); - /// /// Resolves the specified service type from the service provider. - /// public T? GetService() where T : notnull => ServiceProvider.GetService(); - /// /// Resolves the specified service type from the service provider. - /// public object? GetService(Type serviceType) => ServiceProvider.GetService(serviceType); - /// /// Resolves multiple implementations of the specified service type from the service provider. - /// public IEnumerable GetServices() where T : notnull => ServiceProvider.GetServices(); - /// /// Registers a completion callback for the specified activity. - /// internal void AddCompletionCallback(ActivityExecutionContext owner, ActivityNode child, ActivityCompletionCallback? completionCallback = default, object? tag = default) { var entry = new ActivityCompletionCallbackEntry(owner, child, completionCallback, tag); _completionCallbackEntries.Add(entry); } - /// /// Unregisters the completion callback for the specified owner and child activity. - /// internal ActivityCompletionCallbackEntry? PopCompletionCallback(ActivityExecutionContext owner, ActivityNode child) { var entry = _completionCallbackEntries.FirstOrDefault(x => x.Owner == owner && x.Child == child); @@ -480,61 +376,41 @@ public partial class WorkflowExecutionContext : IExecutionContext _completionCallbackEntries.Remove(entry); } - /// /// Returns the with the specified activity ID from the workflow graph. - /// public ActivityNode? FindNodeById(string nodeId) => NodeIdLookup.TryGetValue(nodeId, out var node) ? node : default; - /// /// Returns the with the specified hash of the activity node ID from the workflow graph. - /// /// The hash of the activity node ID. /// The with the specified hash of the activity node ID. public ActivityNode? FindNodeByHash(string hash) => NodeHashLookup.TryGetValue(hash, out var node) ? node : default; - /// /// Returns the containing the specified activity from the workflow graph. - /// public ActivityNode? FindNodeByActivity(IActivity activity) { return NodeActivityLookup.TryGetValue(activity, out var node) ? node : default; } - /// /// Returns the associated with the specified activity ID. - /// public ActivityNode? FindNodeByActivityId(string activityId) => Nodes.FirstOrDefault(x => x.Activity.Id == activityId); - /// /// Returns the with the specified ID from the workflow graph. - /// public IActivity? FindActivityByNodeId(string nodeId) => FindNodeById(nodeId)?.Activity; - /// /// Returns the with the specified ID from the workflow graph. - /// public IActivity? FindActivityById(string activityId) => FindNodeById(NodeIdLookup.SingleOrDefault(n => n.Key.EndsWith(activityId)).Value.NodeId)?.Activity; - /// /// Returns the with the specified hash of the activity node ID from the workflow graph. - /// /// The hash of the activity node ID. /// The with the specified hash of the activity node ID. public IActivity? FindActivityByHash(string hash) => FindNodeByHash(hash)?.Activity; - /// /// Returns a custom property with the specified key from the dictionary. - /// public T? GetProperty(string key) => Properties.TryGetValue(key, out var value) ? value.ConvertTo() : default; - /// /// Sets a custom property with the specified key on the dictionary. - /// public void SetProperty(string key, T value) => Properties[key] = value!; - /// /// Updates a custom property with the specified key on the dictionary. - /// public T UpdateProperty(string key, Func updater) { var value = GetProperty(key); @@ -543,16 +419,14 @@ public partial class WorkflowExecutionContext : IExecutionContext return value; } - /// /// Returns true if the dictionary contains the specified key. - /// public bool HasProperty(string name) => Properties.ContainsKey(name); - internal bool CanTransitionTo(WorkflowSubStatus targetSubStatus) => ValidateStatusTransition(targetSubStatus); + internal bool CanTransitionTo(WorkflowSubStatus targetSubStatus) => ValidateStatusTransition(); internal void TransitionTo(WorkflowSubStatus subStatus) { - if (!ValidateStatusTransition(SubStatus)) + if (!ValidateStatusTransition()) throw new Exception($"Cannot transition from {SubStatus} to {subStatus}"); SubStatus = subStatus; @@ -561,7 +435,7 @@ public partial class WorkflowExecutionContext : IExecutionContext if (Status == WorkflowStatus.Finished) FinishedAt = UpdatedAt; - //For now only trigger on Cancelled, since the other statuses are handling via the host/runner + //For now only trigger on Cancelled, since the other statuses are handled via the host/runner if (SubStatus == WorkflowSubStatus.Cancelled && _statusUpdatedCallback is not null) _statusUpdatedCallback(this); @@ -576,9 +450,7 @@ public partial class WorkflowExecutionContext : IExecutionContext } } - /// /// Creates a new for the specified activity. - /// public async Task CreateActivityExecutionContextAsync(IActivity activity, ActivityInvocationOptions? options = default) { var activityDescriptor = await ActivityRegistryLookup.FindAsync(activity) ?? throw new ActivityNotFoundException(activity.Type); @@ -616,35 +488,23 @@ public partial class WorkflowExecutionContext : IExecutionContext return activityExecutionContext; } - /// /// Returns a register of recorded activity output. - /// public ActivityOutputRegister GetActivityOutputRegister() => TransientProperties.GetOrAdd(ActivityOutputRegistryKey, () => new ActivityOutputRegister()); - /// /// Returns the last activity result. - /// public object? GetLastActivityResult() => TransientProperties.TryGetValue(LastActivityResultKey, out var value) ? value : default; - /// /// Adds the specified to the workflow execution context. - /// public void AddActivityExecutionContext(ActivityExecutionContext context) => _activityExecutionContexts.Add(context); - /// /// Removes the specified from the workflow execution context. - /// public void RemoveActivityExecutionContext(ActivityExecutionContext context) => _activityExecutionContexts.Remove(context); - /// /// Removes the specified from the workflow execution context. - /// /// The predicate used to filter the activity execution contexts to remove. public void RemoveActivityExecutionContext(Func predicate) => _activityExecutionContexts.RemoveWhere(predicate); - /// /// Records the output of the specified activity into the current workflow execution context. - /// /// The of the activity. /// The name of the output. /// The value of the output. @@ -669,8 +529,9 @@ public partial class WorkflowExecutionContext : IExecutionContext WorkflowSubStatus.Suspended => WorkflowStatus.Running, _ => throw new ArgumentOutOfRangeException(nameof(subStatus), subStatus, null) }; - - private bool ValidateStatusTransition(WorkflowSubStatus targetSubStatus) + + // TODO: Check if we should not use the target subStatus here instead. + private bool ValidateStatusTransition() { var currentMainStatus = GetMainStatus(SubStatus); return currentMainStatus != WorkflowStatus.Finished; diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Workflows/EngineExceptionHandlingMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/EngineExceptionHandlingMiddleware.cs new file mode 100644 index 000000000..0987b567c --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Middleware/Workflows/EngineExceptionHandlingMiddleware.cs @@ -0,0 +1,41 @@ +using Elsa.Common.Contracts; +using Elsa.Workflows.Contracts; +using Elsa.Workflows.Models; +using Elsa.Workflows.Pipelines.WorkflowExecution; +using Elsa.Workflows.State; +using Microsoft.Extensions.Logging; + +namespace Elsa.Workflows.Middleware.Workflows; + +/// Adds extension methods to . +public static class EngineExceptionHandlingMiddlewareExtensions +{ + /// Installs the component in the activity execution pipeline. + public static IWorkflowExecutionPipelineBuilder UseEngineExceptionHandling(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware(); +} + +/// Catches any exceptions thrown by downstream components and transitions the workflow into the faulted state. +public class EngineExceptionHandlingMiddleware(WorkflowMiddlewareDelegate next, ISystemClock systemClock, ILogger logger) : IWorkflowExecutionMiddleware +{ + /// + public async ValueTask InvokeAsync(WorkflowExecutionContext context) + { + try + { + await next(context); + } + catch (Exception e) + { + logger.LogWarning(e, "An exception was caught from a downstream middleware component"); + var exceptionState = ExceptionState.FromException(e); + var now = systemClock.UtcNow; + var activity = context.Workflow; + var incident = new ActivityIncident(activity.Id, activity.Type, e.Message, exceptionState, now); + + // No state change as the workflow / activities status should be leading. + // We will however be adding an incident to make the issue visible. + context.Incidents.Add(incident); + context.AddExecutionLogEntry("Faulted", e.Message, exceptionState); + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings index fa7725297..b3b92c608 100644 --- a/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings +++ b/src/modules/Elsa.Workflows.Runtime/Elsa.Workflows.Runtime.csproj.DotSettings @@ -1,2 +1,6 @@  - True \ No newline at end of file + True + True + True + True + True \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs index faf7a8ff9..cf8e7e983 100644 --- a/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs +++ b/src/modules/Elsa.Workflows.Runtime/Extensions/WorkflowExecutionPipelineBuilderExtensions.cs @@ -17,6 +17,7 @@ public static class WorkflowExecutionPipelineBuilderExtensions public static IWorkflowExecutionPipelineBuilder UseDefaultPipeline(this IWorkflowExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder .Reset() + .UseEngineExceptionHandling() .UseBackgroundActivities() .UseBookmarkPersistence() .UseActivityExecutionLogPersistence()