Elsa 2.0/faults (#570)

* Update fault API and add stack trace.
This commit is contained in:
Sipke Schoorstra 2021-01-14 17:15:02 +01:00 committed by GitHub
parent 6b380e86f4
commit 974b3c2df2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 137 additions and 67 deletions

View file

@ -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}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -47,7 +47,7 @@ namespace Elsa.Services
protected ScheduleActivitiesResult Schedule(IEnumerable<ScheduledActivity> activities) => new(activities);
protected CombinedResult Combine(IEnumerable<IActivityExecutionResult> 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<T>([CallerMemberName] string name = null!) => Data.GetState<T>(name);
protected T GetState<T>(Func<T> defaultValue, [CallerMemberName] string name = null!) => Data.GetState(name, defaultValue);

View file

@ -1,10 +0,0 @@
using Microsoft.Extensions.Localization;
namespace Elsa.Services.Models
{
public interface IWorkflowFault
{
string? FaultedActivityId { get; }
LocalizedString? Message { get; }
}
}

View file

@ -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
};
}
}
}

View file

@ -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<IClock>();
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;

View file

@ -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; }
}
}

View file

@ -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<IActivityExecutionResult?> 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;
}
}
}

View file

@ -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<ResumeRunningWorkflowsTask> _logger;
public ResumeRunningWorkflowsTask(
IWorkflowInstanceStore workflowInstanceStore,
IWorkflowRunner workflowScheduler,
IDistributedLockProvider distributedLockProvider)
IDistributedLockProvider distributedLockProvider,
ILogger<ResumeRunningWorkflowsTask> 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
{

View file

@ -129,7 +129,7 @@ namespace Elsa.Triggers
var workflowBlueprints = await _workflowRegistry.GetWorkflowsAsync(cancellationToken).ToListAsync(cancellationToken);
var descriptors = new List<TriggerDescriptor>();
foreach (var workflowBlueprint in workflowBlueprints)
foreach (var workflowBlueprint in workflowBlueprints.Where(x => x.IsEnabled))
{
var blueprintDescriptors = await BuildDescriptorsForAsync(workflowBlueprint, null, cancellationToken);
descriptors.AddRange(blueprintDescriptors);

View file

@ -13,7 +13,6 @@
<ProjectReference Include="..\..\..\activities\Elsa.Activities.AzureServiceBus\Elsa.Activities.AzureServiceBus.csproj" />
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Timers.Quartz\Elsa.Activities.Timers.Quartz.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\persistence\Elsa.Persistence.EntityFramework\Elsa.Persistence.EntityFramework.Sqlite\Elsa.Persistence.EntityFramework.Sqlite.csproj" />
<ProjectReference Include="..\..\..\persistence\Elsa.Persistence.YesSql\Elsa.Persistence.YesSql.csproj" />
</ItemGroup>

View file

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Timers.Quartz\Elsa.Activities.Timers.Quartz.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
<ProjectReference Include="..\..\..\persistence\Elsa.Persistence.YesSql\Elsa.Persistence.YesSql.csproj" />
</ItemGroup>
</Project>

View file

@ -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<FaultyWorkflow>());
});
}
}

View file

@ -0,0 +1,11 @@
{
"profiles": {
"Elsa.Samples.Faulting": {
"commandName": "Project",
"dotnetRunMessages": "true",
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}

View file

@ -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"));
}
}
}

View file

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}