From 974b3c2df28016a2b395fe307a5f7da8a651d940 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 14 Jan 2021 17:15:02 +0100 Subject: [PATCH] Elsa 2.0/faults (#570) * Update fault API and add stack trace. --- Elsa.sln | 7 +++++ .../ActivityResults/FaultResult.cs | 7 ++--- .../Elsa.Abstractions/Models/WorkflowFault.cs | 6 +---- .../Elsa.Abstractions/Services/Activity.cs | 2 +- .../Services/Models/IWorkflowFault.cs | 10 ------- .../Services/Models/ProcessFault.cs | 23 ---------------- .../Models/WorkflowExecutionContext.cs | 8 +++--- .../Services/Models/WorkflowFault.cs | 16 ----------- src/core/Elsa.Core/Services/WorkflowRunner.cs | 24 +++++++++++++++-- .../ResumeRunningWorkflowsTask.cs | 15 +++++++++-- .../Elsa.Core/Triggers/WorkflowSelector.cs | 2 +- .../Elsa.Samples.AzureServiceBusWorker.csproj | 1 - .../Elsa.Samples.Faulting.csproj | 17 ++++++++++++ .../worker/Elsa.Samples.Faulting/Program.cs | 27 +++++++++++++++++++ .../Properties/launchSettings.json | 11 ++++++++ .../Workflows/ProducerWorkflow.cs | 19 +++++++++++++ .../Elsa.Samples.Faulting/appsettings.json | 9 +++++++ 17 files changed, 137 insertions(+), 67 deletions(-) delete mode 100644 src/core/Elsa.Abstractions/Services/Models/IWorkflowFault.cs delete mode 100644 src/core/Elsa.Abstractions/Services/Models/ProcessFault.cs delete mode 100644 src/core/Elsa.Abstractions/Services/Models/WorkflowFault.cs create mode 100644 src/samples/worker/Elsa.Samples.Faulting/Elsa.Samples.Faulting.csproj create mode 100644 src/samples/worker/Elsa.Samples.Faulting/Program.cs create mode 100644 src/samples/worker/Elsa.Samples.Faulting/Properties/launchSettings.json create mode 100644 src/samples/worker/Elsa.Samples.Faulting/Workflows/ProducerWorkflow.cs create mode 100644 src/samples/worker/Elsa.Samples.Faulting/appsettings.json diff --git a/Elsa.sln b/Elsa.sln index 637bb1dc2..6bbfb7005 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -216,6 +216,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.EntityFram EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Persistence.EntityFramework.SqlServer", "src\persistence\Elsa.Persistence.EntityFramework\Elsa.Persistence.EntityFramework.SqlServer\Elsa.Persistence.EntityFramework.SqlServer.csproj", "{F97339EF-424F-46F1-AD8D-B36B8C2F716E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.Faulting", "src\samples\worker\Elsa.Samples.Faulting\Elsa.Samples.Faulting.csproj", "{76BF833E-3CAE-4402-99F1-B3BFD90622E4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -514,6 +516,10 @@ Global {F97339EF-424F-46F1-AD8D-B36B8C2F716E}.Debug|Any CPU.Build.0 = Debug|Any CPU {F97339EF-424F-46F1-AD8D-B36B8C2F716E}.Release|Any CPU.ActiveCfg = Release|Any CPU {F97339EF-424F-46F1-AD8D-B36B8C2F716E}.Release|Any CPU.Build.0 = Release|Any CPU + {76BF833E-3CAE-4402-99F1-B3BFD90622E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76BF833E-3CAE-4402-99F1-B3BFD90622E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76BF833E-3CAE-4402-99F1-B3BFD90622E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76BF833E-3CAE-4402-99F1-B3BFD90622E4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -615,6 +621,7 @@ Global {7240B7EE-53D7-4C94-9A5C-80D2D6C4BF0E} = {C865B0FD-E505-48F0-BFAF-0D4D7C1B5CA1} {8A7C2A43-BABF-4961-81D8-D16A702A2EE8} = {C865B0FD-E505-48F0-BFAF-0D4D7C1B5CA1} {F97339EF-424F-46F1-AD8D-B36B8C2F716E} = {C865B0FD-E505-48F0-BFAF-0D4D7C1B5CA1} + {76BF833E-3CAE-4402-99F1-B3BFD90622E4} = {E42743A0-FBDD-4150-9D53-6000496D9B87} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8B0975FD-7050-48B0-88C5-48C33378E158} diff --git a/src/core/Elsa.Abstractions/ActivityResults/FaultResult.cs b/src/core/Elsa.Abstractions/ActivityResults/FaultResult.cs index 3b9796b45..e99f08546 100644 --- a/src/core/Elsa.Abstractions/ActivityResults/FaultResult.cs +++ b/src/core/Elsa.Abstractions/ActivityResults/FaultResult.cs @@ -5,10 +5,11 @@ namespace Elsa.ActivityResults { public class FaultResult : ActivityExecutionResult { - public FaultResult(LocalizedString message) => Message = message; - public LocalizedString Message { get; } + public FaultResult(string message) => Message = message; + public string Message { get; set; } + public string? StackTrace { get; set; } protected override void Execute(ActivityExecutionContext activityExecutionContext) => - activityExecutionContext.WorkflowExecutionContext.Fault(activityExecutionContext.ActivityBlueprint.Id, Message); + activityExecutionContext.WorkflowExecutionContext.Fault(activityExecutionContext.ActivityBlueprint.Id, Message, StackTrace); } } \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Models/WorkflowFault.cs b/src/core/Elsa.Abstractions/Models/WorkflowFault.cs index a94ea5c17..5d661fcdc 100644 --- a/src/core/Elsa.Abstractions/Models/WorkflowFault.cs +++ b/src/core/Elsa.Abstractions/Models/WorkflowFault.cs @@ -1,8 +1,4 @@ namespace Elsa.Models { - public class WorkflowFault - { - public string? FaultedActivityId { get; set; } - public string? Message { get; set; } - } + public record WorkflowFault(string? FaultedActivityId, string? Message, string? StackTrace); } \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Services/Activity.cs b/src/core/Elsa.Abstractions/Services/Activity.cs index 297ff4e7f..ed0af9f9e 100644 --- a/src/core/Elsa.Abstractions/Services/Activity.cs +++ b/src/core/Elsa.Abstractions/Services/Activity.cs @@ -47,7 +47,7 @@ namespace Elsa.Services protected ScheduleActivitiesResult Schedule(IEnumerable activities) => new(activities); protected CombinedResult Combine(IEnumerable results) => new(results); protected CombinedResult Combine(params IActivityExecutionResult[] results) => new(results); - protected FaultResult Fault(LocalizedString message) => new(message); + protected FaultResult Fault(string message) => new(message); protected T? GetState([CallerMemberName] string name = null!) => Data.GetState(name); protected T GetState(Func defaultValue, [CallerMemberName] string name = null!) => Data.GetState(name, defaultValue); diff --git a/src/core/Elsa.Abstractions/Services/Models/IWorkflowFault.cs b/src/core/Elsa.Abstractions/Services/Models/IWorkflowFault.cs deleted file mode 100644 index 691c662ae..000000000 --- a/src/core/Elsa.Abstractions/Services/Models/IWorkflowFault.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.Extensions.Localization; - -namespace Elsa.Services.Models -{ - public interface IWorkflowFault - { - string? FaultedActivityId { get; } - LocalizedString? Message { get; } - } -} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Services/Models/ProcessFault.cs b/src/core/Elsa.Abstractions/Services/Models/ProcessFault.cs deleted file mode 100644 index d102059fc..000000000 --- a/src/core/Elsa.Abstractions/Services/Models/ProcessFault.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Elsa.Services.Models -{ - public class ProcessFault - { - public ProcessFault(IActivity faultedActivity, string message) - { - FaultedActivity = faultedActivity; - Message = message; - } - - public IActivity FaultedActivity { get; } - public string Message { get; } - - public Elsa.Models.WorkflowFault ToInstance() - { - return new() - { - FaultedActivityId = FaultedActivity?.Id, - Message = Message - }; - } - } -} \ No newline at end of file diff --git a/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs b/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs index d5e25801b..d5e2cab38 100644 --- a/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs +++ b/src/core/Elsa.Abstractions/Services/Models/WorkflowExecutionContext.cs @@ -8,6 +8,7 @@ using Elsa.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Newtonsoft.Json; +using NodaTime; namespace Elsa.Services.Models { @@ -34,7 +35,6 @@ namespace Elsa.Services.Models public JsonSerializer Serializer { get; } public object? Input { get; } public bool HasScheduledActivities => WorkflowInstance.ScheduledActivities.Any(); - public IWorkflowFault? WorkflowFault { get; private set; } public bool IsFirstPass { get; private set; } public bool ContextHasChanged { get; set; } @@ -114,10 +114,12 @@ namespace Elsa.Services.Models public void Resume() => WorkflowInstance.WorkflowStatus = WorkflowStatus.Running; public void Suspend() => WorkflowInstance.WorkflowStatus = WorkflowStatus.Suspended; - public void Fault(string? activityId, LocalizedString? message) + public void Fault(string? activityId, string? message, string? stackTrace) { + var clock = ServiceScope.ServiceProvider.GetRequiredService(); WorkflowInstance.WorkflowStatus = WorkflowStatus.Faulted; - WorkflowFault = new WorkflowFault(activityId, message); + WorkflowInstance.FaultedAt = clock.GetCurrentInstant(); + WorkflowInstance.Fault = new WorkflowFault(activityId, message, stackTrace); } public void Complete() => WorkflowInstance.WorkflowStatus = WorkflowStatus.Finished; diff --git a/src/core/Elsa.Abstractions/Services/Models/WorkflowFault.cs b/src/core/Elsa.Abstractions/Services/Models/WorkflowFault.cs deleted file mode 100644 index 0ac6ad151..000000000 --- a/src/core/Elsa.Abstractions/Services/Models/WorkflowFault.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.Extensions.Localization; - -namespace Elsa.Services.Models -{ - public class WorkflowFault : IWorkflowFault - { - public WorkflowFault(string? activityId = default, LocalizedString? message = default) - { - FaultedActivityId = activityId; - Message = message; - } - - public string? FaultedActivityId { get; } - public LocalizedString? Message { get; } - } -} \ No newline at end of file diff --git a/src/core/Elsa.Core/Services/WorkflowRunner.cs b/src/core/Elsa.Core/Services/WorkflowRunner.cs index 460d113dd..f48d8819a 100644 --- a/src/core/Elsa.Core/Services/WorkflowRunner.cs +++ b/src/core/Elsa.Core/Services/WorkflowRunner.cs @@ -269,7 +269,7 @@ namespace Elsa.Services catch (Exception e) { _logger.LogError(e, e.Message); - workflowExecutionContext.Fault(null, new LocalizedString(e.Message, e.Message)); + workflowExecutionContext.Fault(null, e.Message, e.StackTrace); } } @@ -286,7 +286,12 @@ namespace Elsa.Services var activityExecutionContext = new ActivityExecutionContext(scope, workflowExecutionContext, activityBlueprint, scheduledActivity.Input, cancellationToken); var activity = await activityExecutionContext.ActivateActivityAsync(cancellationToken); await _mediator.Publish(new ActivityExecuting(activityExecutionContext), cancellationToken); - var result = await activityOperation(activityExecutionContext, activity); + + var result = await TryExecuteActivityAsync(activityOperation, activityExecutionContext, activity); + + if(result == null) + return; + await _mediator.Publish(new ActivityExecuted(activityExecutionContext), cancellationToken); await result.ExecuteAsync(activityExecutionContext, cancellationToken); workflowExecutionContext.WorkflowInstance.Output = activityExecutionContext.Output; @@ -305,5 +310,20 @@ namespace Elsa.Services if (workflowExecutionContext.Status == WorkflowStatus.Running) workflowExecutionContext.Complete(); } + + private async ValueTask TryExecuteActivityAsync(ActivityOperation activityOperation, ActivityExecutionContext activityExecutionContext, RuntimeActivityInstance activity) + { + try + { + return await activityOperation(activityExecutionContext, activity); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + activityExecutionContext.WorkflowExecutionContext.Fault(activity.Id, e.Message, e.StackTrace); + } + + return null; + } } } \ No newline at end of file diff --git a/src/core/Elsa.Core/StartupTasks/ResumeRunningWorkflowsTask.cs b/src/core/Elsa.Core/StartupTasks/ResumeRunningWorkflowsTask.cs index cd71159a9..e186bea5f 100644 --- a/src/core/Elsa.Core/StartupTasks/ResumeRunningWorkflowsTask.cs +++ b/src/core/Elsa.Core/StartupTasks/ResumeRunningWorkflowsTask.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Elsa.DistributedLock; @@ -6,6 +7,8 @@ using Elsa.Models; using Elsa.Persistence; using Elsa.Persistence.Specifications; using Elsa.Services; +using Microsoft.Extensions.Logging; +using Open.Linq.AsyncExtensions; namespace Elsa.StartupTasks { @@ -18,15 +21,18 @@ namespace Elsa.StartupTasks private readonly IWorkflowInstanceStore _workflowInstanceStore; private readonly IWorkflowRunner _workflowScheduler; private readonly IDistributedLockProvider _distributedLockProvider; + private readonly ILogger _logger; public ResumeRunningWorkflowsTask( IWorkflowInstanceStore workflowInstanceStore, IWorkflowRunner workflowScheduler, - IDistributedLockProvider distributedLockProvider) + IDistributedLockProvider distributedLockProvider, + ILogger logger) { _workflowInstanceStore = workflowInstanceStore; _workflowScheduler = workflowScheduler; _distributedLockProvider = distributedLockProvider; + _logger = logger; } public async Task ExecuteAsync(CancellationToken cancellationToken = default) @@ -38,12 +44,17 @@ namespace Elsa.StartupTasks try { - var instances = await _workflowInstanceStore.FindManyAsync(new WorkflowStatusSpecification(WorkflowStatus.Running), cancellationToken: cancellationToken); + var instances = await _workflowInstanceStore.FindManyAsync(new WorkflowStatusSpecification(WorkflowStatus.Running), cancellationToken: cancellationToken).ToList(); + + _logger.LogInformation("Found {WorkflowInstanceCount} workflows with status 'Running'. Resuming each one of them.", instances.Count); foreach (var instance in instances) + { + _logger.LogInformation("Resuming {WorkflowInstanceId}", instance.Id); await _workflowScheduler.RunWorkflowAsync( instance, cancellationToken: cancellationToken); + } } finally { diff --git a/src/core/Elsa.Core/Triggers/WorkflowSelector.cs b/src/core/Elsa.Core/Triggers/WorkflowSelector.cs index 11f65b23e..970ee780d 100644 --- a/src/core/Elsa.Core/Triggers/WorkflowSelector.cs +++ b/src/core/Elsa.Core/Triggers/WorkflowSelector.cs @@ -129,7 +129,7 @@ namespace Elsa.Triggers var workflowBlueprints = await _workflowRegistry.GetWorkflowsAsync(cancellationToken).ToListAsync(cancellationToken); var descriptors = new List(); - foreach (var workflowBlueprint in workflowBlueprints) + foreach (var workflowBlueprint in workflowBlueprints.Where(x => x.IsEnabled)) { var blueprintDescriptors = await BuildDescriptorsForAsync(workflowBlueprint, null, cancellationToken); descriptors.AddRange(blueprintDescriptors); diff --git a/src/samples/worker/Elsa.Samples.AzureServiceBusWorker/Elsa.Samples.AzureServiceBusWorker.csproj b/src/samples/worker/Elsa.Samples.AzureServiceBusWorker/Elsa.Samples.AzureServiceBusWorker.csproj index 861fe8526..44b11988a 100644 --- a/src/samples/worker/Elsa.Samples.AzureServiceBusWorker/Elsa.Samples.AzureServiceBusWorker.csproj +++ b/src/samples/worker/Elsa.Samples.AzureServiceBusWorker/Elsa.Samples.AzureServiceBusWorker.csproj @@ -13,7 +13,6 @@ - diff --git a/src/samples/worker/Elsa.Samples.Faulting/Elsa.Samples.Faulting.csproj b/src/samples/worker/Elsa.Samples.Faulting/Elsa.Samples.Faulting.csproj new file mode 100644 index 000000000..9a7914c85 --- /dev/null +++ b/src/samples/worker/Elsa.Samples.Faulting/Elsa.Samples.Faulting.csproj @@ -0,0 +1,17 @@ + + + + net5.0 + false + + + + + + + + + + + + diff --git a/src/samples/worker/Elsa.Samples.Faulting/Program.cs b/src/samples/worker/Elsa.Samples.Faulting/Program.cs new file mode 100644 index 000000000..c6a79a5f6 --- /dev/null +++ b/src/samples/worker/Elsa.Samples.Faulting/Program.cs @@ -0,0 +1,27 @@ +using Elsa.Persistence.YesSql.Extensions; +using Elsa.Samples.Faulting.Workflows; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Elsa.Samples.Faulting +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((_, services) => + { + services + .AddElsa(options => options + .UseYesSqlPersistence() + .AddConsoleActivities() + .AddQuartzTimerActivities() + .AddWorkflow()); + }); + } +} \ No newline at end of file diff --git a/src/samples/worker/Elsa.Samples.Faulting/Properties/launchSettings.json b/src/samples/worker/Elsa.Samples.Faulting/Properties/launchSettings.json new file mode 100644 index 000000000..c8e970d6d --- /dev/null +++ b/src/samples/worker/Elsa.Samples.Faulting/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "Elsa.Samples.Faulting": { + "commandName": "Project", + "dotnetRunMessages": "true", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/samples/worker/Elsa.Samples.Faulting/Workflows/ProducerWorkflow.cs b/src/samples/worker/Elsa.Samples.Faulting/Workflows/ProducerWorkflow.cs new file mode 100644 index 000000000..699f8407e --- /dev/null +++ b/src/samples/worker/Elsa.Samples.Faulting/Workflows/ProducerWorkflow.cs @@ -0,0 +1,19 @@ +using System; +using Elsa.Activities.Console; +using Elsa.Activities.Timers; +using Elsa.Builders; +using NodaTime; + +namespace Elsa.Samples.Faulting.Workflows +{ + public class FaultyWorkflow : IWorkflow + { + public void Build(IWorkflowBuilder workflow) + { + workflow + .StartIn(Duration.FromSeconds(1)) + .WriteLine("Catch this!") + .Then(() => throw new ArithmeticException("Does not compute")); + } + } +} \ No newline at end of file diff --git a/src/samples/worker/Elsa.Samples.Faulting/appsettings.json b/src/samples/worker/Elsa.Samples.Faulting/appsettings.json new file mode 100644 index 000000000..8983e0fc1 --- /dev/null +++ b/src/samples/worker/Elsa.Samples.Faulting/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +}