Refactor Fault Propagation and Simplify Bookmark Management (#6545)
* Refactor bookmark management and add new features Streamlined bookmark handling by eliminating temporary storage in `ActivityExecutionContext` and directly managing bookmarks in `WorkflowExecutionContext`. Documented architectural decisions using ADRs. * Regenerate EF Core migrations * Refactor fault tracking to use AggregatedFaultCount property. Replaces FaultCount with AggregatedFaultCount across the codebase to improve clarity and consistency in fault tracking. Updates related methods, properties, and data mappings to align with the new terminology. Fixes initialization issue with nullable inputs in Fault class. * Add migration to track fault counts in runtime (V3.5) This migration adds a new column, "AggregatedFaultCount," to the "ActivityExecutionRecords" table. The column is an integer, non-nullable, with a default value of 0, and enables tracking aggregated fault occurrences. The migration also includes a rollback to remove this column if needed. * Fix typo in ADR 0004 regarding bookmark management convention Corrected a spelling mistake in the ADR documentation by changing "determins" to "determines." This ensures clarity and maintains the professionalism of the document. No functional changes were made. * Refine fault propagation logic for child-parent activities Replace automatic fault transitions of parent activities with an aggregate fault count for descendant activities. This avoids premature state changes while still indicating child activity faults, improving workflow resilience and accuracy. * Remove signal-driven fault propagation ADR and renumber bookmarks ADR The ADR for signal-driven fault propagation was deleted, and the direct bookmark management ADR was renamed and renumbered accordingly. Related references in the table of contents, graph, and solution file were updated to reflect these changes. * Refactor DeleteBookmarks to improve readability. Reformatted the BookmarkFilter initialization for better clarity and maintainability. This change ensures the code is more aligned with modern C# conventions and improves overall readability. No behavior or functionality has been altered. * Refactor naming for "AggregatedFaultCount" to "AggregateFaultCount" Standardized the terminology across the codebase and migrations by renaming all references of "AggregatedFaultCount" to "AggregateFaultCount" for improved consistency and readability. Updated relevant logic, models, migrations, and database contexts accordingly.
This commit is contained in:
parent
006ed54a92
commit
4266421a4c
23
Elsa.sln
23
Elsa.sln
|
|
@ -20,10 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "solution", "solution", "{7D
|
|||
README.md = README.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{0354F050-3992-4DD4-B0EE-5FBA04AC72B6}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
docs\bounty-footer.md = docs\bounty-footer.md
|
||||
EndProjectSection
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "doc", "doc", "{0354F050-3992-4DD4-B0EE-5FBA04AC72B6}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "modules", "modules", "{5BA4A8FA-F7F4-45B3-AEC8-8886D35AAC79}"
|
||||
EndProject
|
||||
|
|
@ -384,6 +381,22 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Connections.Persistenc
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite", "src\modules\Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite\Elsa.Connections.Persistence.EntityFrameworkCore.Sqlite.csproj", "{DB20B168-CD90-41F2-BE08-3D8C6F142459}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "adr", "adr", "{0A04B1FD-06C0-4271-A910-A08C263DBC44}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
doc\adr\0001-record-architecture-decisions.md = doc\adr\0001-record-architecture-decisions.md
|
||||
doc\adr\0002-fault-propagation-from-child-to-parent-activities.md = doc\adr\0002-fault-propagation-from-child-to-parent-activities.md
|
||||
doc\adr\toc.md = doc\adr\toc.md
|
||||
doc\adr\graph.dot = doc\adr\graph.dot
|
||||
doc\adr\0003-direct-bookmark-management-in-workflowexecutioncontext.md = doc\adr\0003-direct-bookmark-management-in-workflowexecutioncontext.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "bounty", "bounty", "{9B80A705-2E31-4012-964A-83963DCDB384}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
doc\bounty\bounty-announcement-template.md = doc\bounty\bounty-announcement-template.md
|
||||
doc\bounty\bounty-footer.md = doc\bounty\bounty-footer.md
|
||||
doc\bounty\bounty-quality-standards.md = doc\bounty\bounty-quality-standards.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -951,6 +964,8 @@ Global
|
|||
{DF0CA7E2-F5DF-40ED-91B6-748767084FC7} = {9DF81143-2F39-4CFB-9DA3-902279010622}
|
||||
{DB20B168-CD90-41F2-BE08-3D8C6F142459} = {9DF81143-2F39-4CFB-9DA3-902279010622}
|
||||
{8A050229-DB79-4E0B-9AFF-7565E87F2954} = {B08B4E00-C2AB-48F3-8389-449F42AEF179}
|
||||
{0A04B1FD-06C0-4271-A910-A08C263DBC44} = {0354F050-3992-4DD4-B0EE-5FBA04AC72B6}
|
||||
{9B80A705-2E31-4012-964A-83963DCDB384} = {0354F050-3992-4DD4-B0EE-5FBA04AC72B6}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E}
|
||||
|
|
|
|||
19
doc/adr/0001-record-architecture-decisions.md
Normal file
19
doc/adr/0001-record-architecture-decisions.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# 1. Record architecture decisions
|
||||
|
||||
Date: 2025-04-01
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
We need to record the architectural decisions made on this project.
|
||||
|
||||
## Decision
|
||||
|
||||
We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
|
||||
|
||||
## Consequences
|
||||
|
||||
See Michael Nygard's article, linked above. For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# 2. Fault Propagation from Child to Parent Activities
|
||||
|
||||
Date: 2025-04-01
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
When an activity throws an exception during execution, the `ExceptionHandlingMiddleware` is responsible for handling the fault. In addition to logging the exception and recording an incident, it also explicitly faults all ancestor activities using the `GetAncestors()` method and calling `TransitionTo(ActivityStatus.Faulted)` on each one.
|
||||
|
||||
At the time of implementing this behavior, the rationale was likely to bubble up the fault status to allow Elsa Studio to display that a given activity has one or more faulting descendant activities.
|
||||
|
||||
However, this behavior causes unintended side effects when the faulted activity is later resumed and completes successfully. Since the parent (e.g., a `Flowchart`) was faulted earlier, it remains in a `Faulted` state, unaware that the child it depends on has now succeeded. As a result, the Flowchart itself never reaches `Completed`, and the entire workflow instance is stuck in `Faulted` or `Running`, depending on the selected fault strategy.
|
||||
|
||||
## Decision
|
||||
A new field will be introduced that represents an aggregate count of the faults occurring in descendant activities.
|
||||
|
||||
## Consequences
|
||||
- Allows composite activities to reflect a failed state immediately and consistently when one of their children fails.
|
||||
|
||||
## Alternatives Considered
|
||||
- Query descendants at runtime to get an aggregate fault count instead. However, this is an expensive operation that can be time consuming.
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# 3. Direct Bookmark Management in WorkflowExecutionContext
|
||||
|
||||
Date: 2025-04-01
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
Currently, `ActivityExecutionContext` maintains a temporary list of bookmarks created by the activity during its execution. After the activity completes, a middleware is responsible for copying these bookmarks into the `WorkflowExecutionContext.Bookmarks` list, which is then persisted to the database.
|
||||
|
||||
This architecture leads to a subtle issue upon resumption:
|
||||
- When a workflow is resumed from a persisted state, only the `WorkflowExecutionContext` and its `Bookmarks` are restored from the database.
|
||||
- The `ActivityExecutionContext`'s temporary `Bookmarks` list remains empty.
|
||||
- As a result, if the resumed activity attempts to cancel (or modify) its previously created bookmarks, it cannot, because it has no access to them—changes are not tracked.
|
||||
|
||||
However, there is an existing convention that determines that if an activity created a bookmark during its execution, the activity will not automatically complete.
|
||||
Specifically, this is implemented in the `AutoCompleteBehavior`, which is installed by the `CodeActivity` and possibly by custom activities.
|
||||
To keep this behavior in tact, we will still maintain a private list of bookmarks, one that is explicitly purposed for maintaining *new** bookmarks, temporarily for the lifetime of the ActivityExecutionContext in memory.
|
||||
|
||||
## Decision
|
||||
Eliminate the temporary `Bookmarks` list in `ActivityExecutionContext`. Instead, have activities add bookmarks directly to the `WorkflowExecutionContext.Bookmarks` list.
|
||||
|
||||
This is possible and safe because each `Bookmark` already contains:
|
||||
- A reference to the originating `ActivityId`
|
||||
- A reference to the originating `ActivityInstanceId`
|
||||
|
||||
These references allow accurate tracking and filtering of bookmarks, even when multiple activities are active.
|
||||
|
||||
Additionally, maintain a new temporary bookmarks list that explicitly stores newly created bookmarks. This list does not have to be copied into the WorkflowExecutionContext's Bookmarks property, given that this property will now be updated directly.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Simplifies the architecture by removing the need for middleware to copy bookmarks post-execution.
|
||||
- Fixes a bug where resumed activities could not cancel their previously created bookmarks.
|
||||
- Reduces the chance of inconsistencies between temporary and persisted bookmark state.
|
||||
- Ensures bookmark changes are always applied to the persisted, canonical source of truth.
|
||||
|
||||
## Alternatives Considered
|
||||
- Keep the existing temporary list and enhance the system to hydrate the `ActivityExecutionContext.Bookmarks` list on resume. Rejected due to added complexity and duplication of state.
|
||||
10
doc/adr/graph.dot
Normal file
10
doc/adr/graph.dot
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
digraph {
|
||||
node [shape=plaintext];
|
||||
subgraph {
|
||||
_1 [label="1. Record architecture decisions"; URL="0001-record-architecture-decisions.html"];
|
||||
_2 [label="2. Fault Propagation from Child to Parent Activities"; URL="0002-fault-propagation-from-child-to-parent-activities.html"];
|
||||
_1 -> _2 [style="dotted", weight=1];
|
||||
_3 [label="3. Direct Bookmark Management in WorkflowExecutionContext"; URL="0003-direct-bookmark-management-in-workflowexecutioncontext.html"];
|
||||
_2 -> _3 [style="dotted", weight=1];
|
||||
}
|
||||
}
|
||||
5
doc/adr/toc.md
Normal file
5
doc/adr/toc.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Architecture Decision Records
|
||||
|
||||
* [1. Record architecture decisions](0001-record-architecture-decisions.md)
|
||||
* [2. Fault Propagation from Child to Parent Activities](0002-fault-propagation-from-child-to-parent-activities.md)
|
||||
* [3. Direct Bookmark Management in WorkflowExecutionContext](0003-direct-bookmark-management-in-workflowexecutioncontext.md)
|
||||
26
src/apps/Elsa.ServerAndStudio.Web/Activities/CustomDelay.cs
Normal file
26
src/apps/Elsa.ServerAndStudio.Web/Activities/CustomDelay.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
using Elsa.Extensions;
|
||||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.ServerAndStudio.Web.Activities;
|
||||
|
||||
public class CustomDelay : Activity
|
||||
{
|
||||
protected override ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
context.DelayFor(TimeSpan.FromSeconds(5), FailAsync);
|
||||
context.DelayFor(TimeSpan.FromSeconds(500), OnDelayElapsedAsync);
|
||||
return default;
|
||||
}
|
||||
|
||||
private ValueTask FailAsync(ActivityExecutionContext context)
|
||||
{
|
||||
Console.WriteLine("Failing!");
|
||||
throw new("Failing!");
|
||||
}
|
||||
|
||||
private async ValueTask OnDelayElapsedAsync(ActivityExecutionContext context)
|
||||
{
|
||||
Console.WriteLine("Delay elapsed");
|
||||
await context.CompleteActivityAsync();
|
||||
}
|
||||
}
|
||||
18
src/apps/Elsa.ServerAndStudio.Web/Activities/CustomEvent.cs
Normal file
18
src/apps/Elsa.ServerAndStudio.Web/Activities/CustomEvent.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using Elsa.Expressions.Models;
|
||||
using Elsa.Workflows;
|
||||
using Elsa.Workflows.Runtime.Activities;
|
||||
|
||||
namespace Elsa.ServerAndStudio.Web.Activities;
|
||||
|
||||
public class CustomEvent : EventBase<object>
|
||||
{
|
||||
protected override string GetEventName(ExpressionExecutionContext context)
|
||||
{
|
||||
return "MyEvent";
|
||||
}
|
||||
|
||||
protected override void OnEventReceived(ActivityExecutionContext context, object? eventData)
|
||||
{
|
||||
Console.WriteLine("Event received with data: " + eventData);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
using Elsa.Http;
|
||||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.ServerAndStudio.Web.Activities;
|
||||
|
||||
public class CustomHttpEndpoint : HttpEndpointBase
|
||||
{
|
||||
protected override HttpEndpointOptions GetOptions()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Path = "my-path",
|
||||
Methods = [HttpMethods.Get],
|
||||
};
|
||||
}
|
||||
|
||||
protected override async ValueTask OnHttpRequestReceivedAsync(ActivityExecutionContext context, HttpContext httpContext)
|
||||
{
|
||||
httpContext.Response.StatusCode = 200;
|
||||
await httpContext.Response.WriteAsync("Hello World", context.CancellationToken);
|
||||
}
|
||||
}
|
||||
18
src/apps/Elsa.ServerAndStudio.Web/Activities/CustomTimer.cs
Normal file
18
src/apps/Elsa.ServerAndStudio.Web/Activities/CustomTimer.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
using Elsa.Expressions.Models;
|
||||
using Elsa.Scheduling.Activities;
|
||||
using Elsa.Workflows;
|
||||
|
||||
namespace Elsa.ServerAndStudio.Web.Activities;
|
||||
|
||||
public class CustomTimer : TimerBase
|
||||
{
|
||||
protected override TimeSpan GetInterval(ExpressionExecutionContext context)
|
||||
{
|
||||
return TimeSpan.FromSeconds(5);
|
||||
}
|
||||
|
||||
protected override void OnTimerElapsed(ActivityExecutionContext context)
|
||||
{
|
||||
Console.WriteLine("Timer elapsed");
|
||||
}
|
||||
}
|
||||
|
|
@ -10,22 +10,22 @@ public class ActivityExecutionRecordSummary : Entity
|
|||
/// <summary>
|
||||
/// Gets or sets the workflow instance ID.
|
||||
/// </summary>
|
||||
public string WorkflowInstanceId { get; set; } = default!;
|
||||
public string WorkflowInstanceId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the activity ID.
|
||||
/// </summary>
|
||||
public string ActivityId { get; set; } = default!;
|
||||
public string ActivityId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the activity node ID.
|
||||
/// </summary>
|
||||
public string ActivityNodeId { get; set; } = default!;
|
||||
public string ActivityNodeId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The type of the activity.
|
||||
/// </summary>
|
||||
public string ActivityType { get; set; } = default!;
|
||||
public string ActivityType { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The version of the activity type.
|
||||
|
|
@ -62,7 +62,7 @@ public class ActivityExecutionRecordSummary : Entity
|
|||
/// </summary>
|
||||
public static ActivityExecutionRecordSummary FromRecord(ActivityExecutionRecord record)
|
||||
{
|
||||
return new ActivityExecutionRecordSummary
|
||||
return new()
|
||||
{
|
||||
Id = record.Id,
|
||||
WorkflowInstanceId = record.WorkflowInstanceId,
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ public class ActivityExecutionStats
|
|||
/// <summary>
|
||||
/// Gets or sets the ID of the activity.
|
||||
/// </summary>
|
||||
public string ActivityId { get; set; } = default!;
|
||||
public string ActivityId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the node ID of the activity.
|
||||
/// </summary>
|
||||
public string ActivityNodeId { get; set; } = default!;
|
||||
public string ActivityNodeId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of uncompleted executions.
|
||||
|
|
@ -39,4 +39,9 @@ public class ActivityExecutionStats
|
|||
/// Gets or sets a value whether this activity is faulted.
|
||||
/// </summary>
|
||||
public bool IsFaulted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the total count of faults aggregated from the activity execution and its descendants.
|
||||
/// </summary>
|
||||
public int AggregateFaultCount { get; set; }
|
||||
}
|
||||
25
src/modules/Elsa.Dapper.Migrations/Runtime/V3_5.cs
Normal file
25
src/modules/Elsa.Dapper.Migrations/Runtime/V3_5.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using FluentMigrator;
|
||||
using JetBrains.Annotations;
|
||||
using static System.Int32;
|
||||
|
||||
namespace Elsa.Dapper.Migrations.Runtime;
|
||||
|
||||
/// <inheritdoc />
|
||||
[Migration(20006, "Elsa:Runtime:V3.5")]
|
||||
[PublicAPI]
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
public class V3_5 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override void Up()
|
||||
{
|
||||
Alter.Table("ActivityExecutionRecords").AddColumn("AggregateFaultCount").AsInt32().NotNullable().WithDefault(0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Down()
|
||||
{
|
||||
Delete.Column("AggregateFaultCount").FromTable("ActivityExecutionRecords");
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||
namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime
|
||||
{
|
||||
[DbContext(typeof(RuntimeElsaDbContext))]
|
||||
[Migration("20250320220046_V3_5")]
|
||||
[Migration("20250401201339_V3_5")]
|
||||
partial class V3_5
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -68,6 +68,9 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime
|
|||
b.Property<int>("ActivityTypeVersion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("AggregateFaultCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
|
|
@ -37,6 +37,14 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime
|
|||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AggregateFaultCount",
|
||||
schema: _schema.Schema,
|
||||
table: "ActivityExecutionRecords",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StoredBookmark_Name",
|
||||
schema: _schema.Schema,
|
||||
|
|
@ -79,6 +87,11 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime
|
|||
schema: _schema.Schema,
|
||||
table: "Bookmarks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AggregateFaultCount",
|
||||
schema: _schema.Schema,
|
||||
table: "ActivityExecutionRecords");
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
schema: _schema.Schema,
|
||||
table: "Triggers",
|
||||
|
|
@ -65,6 +65,9 @@ namespace Elsa.EntityFrameworkCore.MySql.Migrations.Runtime
|
|||
b.Property<int>("ActivityTypeVersion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("AggregateFaultCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ using Oracle.EntityFrameworkCore.Metadata;
|
|||
namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime
|
||||
{
|
||||
[DbContext(typeof(RuntimeElsaDbContext))]
|
||||
[Migration("20250320220124_V3_5")]
|
||||
[Migration("20250401201436_V3_5")]
|
||||
partial class V3_5
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -68,6 +68,9 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime
|
|||
b.Property<int>("ActivityTypeVersion")
|
||||
.HasColumnType("NUMBER(10)");
|
||||
|
||||
b.Property<int>("AggregateFaultCount")
|
||||
.HasColumnType("NUMBER(10)");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("TIMESTAMP(7) WITH TIME ZONE");
|
||||
|
||||
|
|
@ -34,6 +34,14 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime
|
|||
type: "NVARCHAR2(450)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AggregateFaultCount",
|
||||
schema: _schema.Schema,
|
||||
table: "ActivityExecutionRecords",
|
||||
type: "NUMBER(10)",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StoredBookmark_Name",
|
||||
schema: _schema.Schema,
|
||||
|
|
@ -76,6 +84,11 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime
|
|||
schema: _schema.Schema,
|
||||
table: "Bookmarks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AggregateFaultCount",
|
||||
schema: _schema.Schema,
|
||||
table: "ActivityExecutionRecords");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
schema: _schema.Schema,
|
||||
|
|
@ -65,6 +65,9 @@ namespace Elsa.EntityFrameworkCore.Oracle.Migrations.Runtime
|
|||
b.Property<int>("ActivityTypeVersion")
|
||||
.HasColumnType("NUMBER(10)");
|
||||
|
||||
b.Property<int>("AggregateFaultCount")
|
||||
.HasColumnType("NUMBER(10)");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("TIMESTAMP(7) WITH TIME ZONE");
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||
namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime
|
||||
{
|
||||
[DbContext(typeof(RuntimeElsaDbContext))]
|
||||
[Migration("20250320220113_V3_5")]
|
||||
[Migration("20250401201425_V3_5")]
|
||||
partial class V3_5
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -68,6 +68,9 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime
|
|||
b.Property<int>("ActivityTypeVersion")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("AggregateFaultCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
|
|
@ -34,6 +34,14 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime
|
|||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AggregateFaultCount",
|
||||
schema: _schema.Schema,
|
||||
table: "ActivityExecutionRecords",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StoredBookmark_Name",
|
||||
schema: _schema.Schema,
|
||||
|
|
@ -76,6 +84,11 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime
|
|||
schema: _schema.Schema,
|
||||
table: "Bookmarks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AggregateFaultCount",
|
||||
schema: _schema.Schema,
|
||||
table: "ActivityExecutionRecords");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
schema: _schema.Schema,
|
||||
|
|
@ -65,6 +65,9 @@ namespace Elsa.EntityFrameworkCore.PostgreSql.Migrations.Runtime
|
|||
b.Property<int>("ActivityTypeVersion")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("AggregateFaultCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||
namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime
|
||||
{
|
||||
[DbContext(typeof(RuntimeElsaDbContext))]
|
||||
[Migration("20250320220055_V3_5")]
|
||||
[Migration("20250401201357_V3_5")]
|
||||
partial class V3_5
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -68,6 +68,9 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime
|
|||
b.Property<int>("ActivityTypeVersion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("AggregateFaultCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
|
|
@ -34,6 +34,14 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime
|
|||
type: "nvarchar(450)",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AggregateFaultCount",
|
||||
schema: _schema.Schema,
|
||||
table: "ActivityExecutionRecords",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StoredBookmark_Name",
|
||||
schema: _schema.Schema,
|
||||
|
|
@ -76,6 +84,11 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime
|
|||
schema: _schema.Schema,
|
||||
table: "Bookmarks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AggregateFaultCount",
|
||||
schema: _schema.Schema,
|
||||
table: "ActivityExecutionRecords");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
schema: _schema.Schema,
|
||||
|
|
@ -65,6 +65,9 @@ namespace Elsa.EntityFrameworkCore.SqlServer.Migrations.Runtime
|
|||
b.Property<int>("ActivityTypeVersion")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("AggregateFaultCount")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||
namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime
|
||||
{
|
||||
[DbContext(typeof(RuntimeElsaDbContext))]
|
||||
[Migration("20250320185742_V3_5")]
|
||||
[Migration("20250401201414_V3_5")]
|
||||
partial class V3_5
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -64,6 +64,9 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime
|
|||
b.Property<int>("ActivityTypeVersion")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AggregateFaultCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
|
@ -34,6 +34,14 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime
|
|||
type: "TEXT",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AggregateFaultCount",
|
||||
schema: _schema.Schema,
|
||||
table: "ActivityExecutionRecords",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_StoredBookmark_Name",
|
||||
schema: _schema.Schema,
|
||||
|
|
@ -76,6 +84,11 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime
|
|||
schema: _schema.Schema,
|
||||
table: "Bookmarks");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AggregateFaultCount",
|
||||
schema: _schema.Schema,
|
||||
table: "ActivityExecutionRecords");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
schema: _schema.Schema,
|
||||
|
|
@ -61,6 +61,9 @@ namespace Elsa.EntityFrameworkCore.Sqlite.Migrations.Runtime
|
|||
b.Property<int>("ActivityTypeVersion")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("AggregateFaultCount")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTimeOffset?>("CompletedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ internal class Request
|
|||
/// <summary>
|
||||
/// The ID of the workflow instance to get the execution log for.
|
||||
/// </summary>
|
||||
public string WorkflowInstanceId { get; set; } = default!;
|
||||
public string WorkflowInstanceId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The node IDs of the activities to get the execution records for.
|
||||
/// </summary>
|
||||
public ICollection<string> ActivityNodeIds { get; set; } = default!;
|
||||
public ICollection<string> ActivityNodeIds { get; set; } = null!;
|
||||
}
|
||||
|
||||
internal class Response
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public class Fault : Activity
|
|||
/// </summary>
|
||||
public static Fault Create(string code, string category, string type, string? message = null, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null)
|
||||
{
|
||||
return new Fault(source, line)
|
||||
return new(source, line)
|
||||
{
|
||||
Code = new(code),
|
||||
Message = new(message),
|
||||
|
|
@ -38,13 +38,13 @@ public class Fault : Activity
|
|||
/// Code to categorize the fault.
|
||||
/// </summary>
|
||||
[Input(Description = "Code to categorize the fault.")]
|
||||
public Input<string> Code { get; set; } = default!;
|
||||
public Input<string> Code { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Category to categorize the fault. Examples: HTTP, Alteration, Azure, etc.
|
||||
/// </summary>
|
||||
[Input(Description = "Category to categorize the fault. Examples: HTTP, Alteration, Azure, etc.")]
|
||||
public Input<string> Category { get; set; } = default!;
|
||||
public Input<string> Category { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The type of fault. Examples: System, Business, Integration, etc.
|
||||
|
|
@ -53,13 +53,13 @@ public class Fault : Activity
|
|||
DisplayName = "Type",
|
||||
Description = "The type of fault. Examples: System, Business, Integration, etc."
|
||||
)]
|
||||
public Input<string> FaultType { get; set; } = default!;
|
||||
public Input<string> FaultType { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The message to include with the fault.
|
||||
/// </summary>
|
||||
[Input(Description = "The message to include with the fault.")]
|
||||
public Input<string?> Message { get; set; } = default!;
|
||||
public Input<string?> Message { get; set; } = null!;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Execute(ActivityExecutionContext context)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
using JetBrains.Annotations;
|
||||
|
||||
namespace Elsa.Workflows.Behaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Automatically completes the currently executing activity.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class AutoCompleteBehavior : Behavior
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -14,7 +17,7 @@ public class AutoCompleteBehavior : Behavior
|
|||
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
|
||||
{
|
||||
// If the activity created any bookmarks, do not complete.
|
||||
if (context.Bookmarks.Any(x => x.ActivityId == context.Activity.Id))
|
||||
if (context.NewBookmarks.Any(x => x.ActivityId == context.Activity.Id))
|
||||
return;
|
||||
|
||||
await context.CompleteActivityAsync();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Elsa.Common;
|
||||
using Elsa.Expressions.Helpers;
|
||||
|
|
@ -19,11 +18,13 @@ namespace Elsa.Workflows;
|
|||
public partial class ActivityExecutionContext : IExecutionContext, IDisposable
|
||||
{
|
||||
private readonly ISystemClock _systemClock;
|
||||
private readonly List<Bookmark> _bookmarks = [];
|
||||
private ActivityStatus _status;
|
||||
private Exception? _exception;
|
||||
private long _executionCount;
|
||||
private ActivityExecutionContext? _parentActivityExecutionContext;
|
||||
|
||||
// Bookmarks created during the lifetime of this activity.
|
||||
private List<Bookmark> _newBookmarks = [];
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ActivityExecutionContext"/> class.
|
||||
|
|
@ -94,6 +95,11 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable
|
|||
/// </remarks>
|
||||
public bool IsExecuting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of faults encountered during the execution of the activity and its descendants.
|
||||
/// </summary>
|
||||
public int AggregateFaultCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The workflow execution context.
|
||||
/// </summary>
|
||||
|
|
@ -226,9 +232,14 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable
|
|||
public ISet<ActivityExecutionContext> Children { get; } = new HashSet<ActivityExecutionContext>();
|
||||
|
||||
/// <summary>
|
||||
/// A list of bookmarks created by the current activity.
|
||||
/// A list of bookmarks associated with the current activity.
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<Bookmark> Bookmarks => new ReadOnlyCollection<Bookmark>(_bookmarks);
|
||||
public IEnumerable<Bookmark> Bookmarks => WorkflowExecutionContext.Bookmarks.Where(x => x.ActivityInstanceId == Id);
|
||||
|
||||
/// <summary>
|
||||
/// A collection of bookmarks created during the execution of the activity.
|
||||
/// </summary>
|
||||
public IEnumerable<Bookmark> NewBookmarks => _newBookmarks.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// The number of times this <see cref="ActivityExecutionContext"/> has executed.
|
||||
|
|
@ -418,7 +429,7 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable
|
|||
/// <param name="bookmarks">The bookmarks to add.</param>
|
||||
public void AddBookmarks(IEnumerable<Bookmark> bookmarks)
|
||||
{
|
||||
_bookmarks.AddRange(bookmarks);
|
||||
WorkflowExecutionContext.Bookmarks.AddRange(bookmarks);
|
||||
Taint();
|
||||
}
|
||||
|
||||
|
|
@ -428,7 +439,8 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable
|
|||
/// <param name="bookmark">The bookmark to add.</param>
|
||||
public void AddBookmark(Bookmark bookmark)
|
||||
{
|
||||
_bookmarks.Add(bookmark);
|
||||
_newBookmarks.Add(bookmark);
|
||||
WorkflowExecutionContext.Bookmarks.Add(bookmark);
|
||||
Taint();
|
||||
}
|
||||
|
||||
|
|
@ -536,7 +548,8 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable
|
|||
/// </summary>
|
||||
public void ClearBookmarks()
|
||||
{
|
||||
_bookmarks.Clear();
|
||||
_newBookmarks.Clear();
|
||||
WorkflowExecutionContext.Bookmarks.RemoveWhere(x => x.ActivityInstanceId == Id);
|
||||
Taint();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Elsa.Common;
|
||||
using Elsa.Expressions.Contracts;
|
||||
using Elsa.Expressions.Helpers;
|
||||
using Elsa.Expressions.Models;
|
||||
|
|
@ -10,6 +11,7 @@ using Elsa.Workflows.Memory;
|
|||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Notifications;
|
||||
using Elsa.Workflows.Signals;
|
||||
using Elsa.Workflows.State;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
|
|
@ -79,8 +81,10 @@ public static partial class ActivityExecutionContextExtensions
|
|||
/// <param name="storageDriverType">The type of storage driver to use for the variable.</param>
|
||||
/// <param name="configure">A callback to configure the memory block.</param>
|
||||
/// <returns>The created <see cref="Variable"/>.</returns>
|
||||
public static Variable CreateVariable(this ActivityExecutionContext context, string name, object? value, Type? storageDriverType = null, Action<MemoryBlock>? configure = null) =>
|
||||
context.ExpressionExecutionContext.CreateVariable(name, value, storageDriverType, configure);
|
||||
public static Variable CreateVariable(this ActivityExecutionContext context, string name, object? value, Type? storageDriverType = null, Action<MemoryBlock>? configure = null)
|
||||
{
|
||||
return context.ExpressionExecutionContext.CreateVariable(name, value, storageDriverType, configure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a workflow variable by name.
|
||||
|
|
@ -103,7 +107,7 @@ public static partial class ActivityExecutionContextExtensions
|
|||
context.SetVariable(name, value);
|
||||
return predefinedVariable;
|
||||
}
|
||||
|
||||
|
||||
// No predefined variable exists, so we will add a dynamic variable to the current container in scope.
|
||||
var container = context.FindParentWithVariableContainer();
|
||||
|
||||
|
|
@ -300,6 +304,47 @@ public static partial class ActivityExecutionContextExtensions
|
|||
await publisher.SendAsync(new ActivityCancelled(context));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks the current activity as faulted, records an exception, and transitions its status to <see cref="ActivityStatus.Faulted"/>.
|
||||
/// An incident is logged, and the fault count for the activity and its ancestor activities is incremented.
|
||||
/// </summary>
|
||||
/// <param name="context">The <see cref="ActivityExecutionContext"/> representing the execution context of the current activity.</param>
|
||||
/// <param name="e">The exception to be recorded as the cause of the fault.</param>
|
||||
public static void Fault(this ActivityExecutionContext context, Exception e)
|
||||
{
|
||||
context.Exception = e;
|
||||
context.TransitionTo(ActivityStatus.Faulted);
|
||||
var activity = context.Activity;
|
||||
var exceptionState = ExceptionState.FromException(e);
|
||||
var systemClock = context.GetRequiredService<ISystemClock>();
|
||||
var now = systemClock.UtcNow;
|
||||
var incident = new ActivityIncident(activity.Id, activity.Type, e.Message, exceptionState, now);
|
||||
context.WorkflowExecutionContext.Incidents.Add(incident);
|
||||
context.AggregateFaultCount++;
|
||||
|
||||
var ancestors = context.GetAncestors();
|
||||
|
||||
foreach (var ancestor in ancestors)
|
||||
ancestor.AggregateFaultCount++;
|
||||
|
||||
context.TransitionTo(ActivityStatus.Faulted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recovers the current activity from a faulted state by resetting fault counts and transitioning the activity to the running status.
|
||||
/// </summary>
|
||||
public static void RecoverFromFault(this ActivityExecutionContext context)
|
||||
{
|
||||
context.AggregateFaultCount = 0;
|
||||
|
||||
var ancestors = context.GetAncestors();
|
||||
|
||||
foreach (var ancestor in ancestors)
|
||||
ancestor.AggregateFaultCount--;
|
||||
|
||||
context.TransitionTo(ActivityStatus.Running);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first context that is associated with a <see cref="IVariableContainer"/>.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -82,13 +82,13 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I
|
|||
// Invoke next middleware.
|
||||
await next(context);
|
||||
|
||||
// If the activity created any bookmarks, copy them into the workflow execution context.
|
||||
if (context.Bookmarks.Any())
|
||||
{
|
||||
// Store bookmarks.
|
||||
workflowExecutionContext.Bookmarks.AddRange(context.Bookmarks);
|
||||
logger.LogDebug("Added {BookmarkCount} bookmarks to the workflow execution context", context.Bookmarks.Count);
|
||||
}
|
||||
// // If the activity created any bookmarks, copy them into the workflow execution context.
|
||||
// if (context.Bookmarks.Any())
|
||||
// {
|
||||
// // Store bookmarks.
|
||||
// workflowExecutionContext.Bookmarks.AddRange(context.Bookmarks);
|
||||
// logger.LogDebug("Added {BookmarkCount} bookmarks to the workflow execution context", context.Bookmarks.Count);
|
||||
// }
|
||||
|
||||
// Conditionally commit the workflow state.
|
||||
if (ShouldCommit(context, ActivityLifetimeEvent.ActivityExecuted))
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
using Elsa.Common;
|
||||
using Elsa.Extensions;
|
||||
using Elsa.Workflows.Models;
|
||||
using Elsa.Workflows.Pipelines.ActivityExecution;
|
||||
using Elsa.Workflows.State;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Elsa.Workflows.Middleware.Activities;
|
||||
|
|
@ -34,8 +32,7 @@ public class ExceptionHandlingMiddleware(ActivityMiddlewareDelegate next, IIncid
|
|||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning(e, "An exception was caught from a downstream middleware component");
|
||||
LogExceptionAndTransition(context, e);
|
||||
FaultAncestors(context);
|
||||
context.Fault(e);
|
||||
await HandleIncidentAsync(context);
|
||||
}
|
||||
}
|
||||
|
|
@ -56,12 +53,4 @@ public class ExceptionHandlingMiddleware(ActivityMiddlewareDelegate next, IIncid
|
|||
var strategy = await incidentStrategyResolver.ResolveStrategyAsync(context);
|
||||
strategy.HandleIncident(context);
|
||||
}
|
||||
|
||||
private static void FaultAncestors(ActivityExecutionContext context)
|
||||
{
|
||||
var ancestors = context.GetAncestors();
|
||||
|
||||
foreach (var ancestor in ancestors)
|
||||
ancestor.TransitionTo(ActivityStatus.Faulted);
|
||||
}
|
||||
}
|
||||
|
|
@ -150,6 +150,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor
|
|||
|
||||
activityExecutionContext.TransitionTo(activityExecutionContextState.Status);
|
||||
activityExecutionContext.IsExecuting = activityExecutionContextState.IsExecuting;
|
||||
activityExecutionContext.AggregateFaultCount = activityExecutionContextState.FaultCount;
|
||||
activityExecutionContext.StartedAt = activityExecutionContextState.StartedAt;
|
||||
activityExecutionContext.CompletedAt = activityExecutionContextState.CompletedAt;
|
||||
activityExecutionContext.Tag = activityExecutionContextState.Tag;
|
||||
|
|
@ -238,6 +239,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor
|
|||
ActivityState = activityExecutionContext.ActivityState,
|
||||
Status = activityExecutionContext.Status,
|
||||
IsExecuting = activityExecutionContext.IsExecuting,
|
||||
FaultCount = activityExecutionContext.AggregateFaultCount,
|
||||
StartedAt = activityExecutionContext.StartedAt,
|
||||
CompletedAt = activityExecutionContext.CompletedAt,
|
||||
Tag = activityExecutionContext.Tag,
|
||||
|
|
|
|||
|
|
@ -61,6 +61,11 @@ public class ActivityExecutionContextState
|
|||
/// </summary>
|
||||
public bool IsExecuting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of faults recorded by this activity for itself and descendants.
|
||||
/// </summary>
|
||||
public int FaultCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time at which the activity execution began.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -12,22 +12,22 @@ public class ActivityExecutionRecord : Entity, ILogRecord
|
|||
/// <summary>
|
||||
/// Gets or sets the workflow instance ID.
|
||||
/// </summary>
|
||||
public string WorkflowInstanceId { get; set; } = default!;
|
||||
public string WorkflowInstanceId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the activity ID.
|
||||
/// </summary>
|
||||
public string ActivityId { get; set; } = default!;
|
||||
public string ActivityId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the activity node ID.
|
||||
/// </summary>
|
||||
public string ActivityNodeId { get; set; } = default!;
|
||||
public string ActivityNodeId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The type of the activity.
|
||||
/// </summary>
|
||||
public string ActivityType { get; set; } = default!;
|
||||
public string ActivityType { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The version of the activity type.
|
||||
|
|
@ -78,6 +78,11 @@ public class ActivityExecutionRecord : Entity, ILogRecord
|
|||
/// Gets or sets the status of the activity.
|
||||
/// </summary>
|
||||
public ActivityStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the aggregated count of faults encountered during the execution of the activity instance and its descendants.
|
||||
/// </summary>
|
||||
public int AggregateFaultCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time at which the activity execution completed.
|
||||
|
|
|
|||
|
|
@ -11,22 +11,22 @@ public class ActivityExecutionRecordSummary : Entity
|
|||
/// <summary>
|
||||
/// Gets or sets the workflow instance ID.
|
||||
/// </summary>
|
||||
public string WorkflowInstanceId { get; set; } = default!;
|
||||
public string WorkflowInstanceId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the activity ID.
|
||||
/// </summary>
|
||||
public string ActivityId { get; set; } = default!;
|
||||
public string ActivityId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the activity node ID.
|
||||
/// </summary>
|
||||
public string ActivityNodeId { get; set; } = default!;
|
||||
public string ActivityNodeId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The type of the activity.
|
||||
/// </summary>
|
||||
public string ActivityType { get; set; } = default!;
|
||||
public string ActivityType { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The version of the activity type.
|
||||
|
|
@ -52,6 +52,11 @@ public class ActivityExecutionRecordSummary : Entity
|
|||
/// Gets or sets the status of the activity.
|
||||
/// </summary>
|
||||
public ActivityStatus Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the aggregated count of faults encountered during the execution of the activity instance and its descendants.
|
||||
/// </summary>
|
||||
public int AggregateFaultCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the time at which the activity execution completed.
|
||||
|
|
@ -63,7 +68,7 @@ public class ActivityExecutionRecordSummary : Entity
|
|||
/// </summary>
|
||||
public static ActivityExecutionRecordSummary FromRecord(ActivityExecutionRecord record)
|
||||
{
|
||||
return new ActivityExecutionRecordSummary
|
||||
return new()
|
||||
{
|
||||
Id = record.Id,
|
||||
WorkflowInstanceId = record.WorkflowInstanceId,
|
||||
|
|
@ -75,7 +80,8 @@ public class ActivityExecutionRecordSummary : Entity
|
|||
StartedAt = record.StartedAt,
|
||||
HasBookmarks = record.HasBookmarks,
|
||||
Status = record.Status,
|
||||
CompletedAt = record.CompletedAt
|
||||
AggregateFaultCount = record.AggregateFaultCount,
|
||||
CompletedAt = record.CompletedAt,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +90,7 @@ public class ActivityExecutionRecordSummary : Entity
|
|||
/// </summary>
|
||||
public static Expression<Func<ActivityExecutionRecord, ActivityExecutionRecordSummary>> FromRecordExpression()
|
||||
{
|
||||
return record => new ActivityExecutionRecordSummary
|
||||
return record => new()
|
||||
{
|
||||
Id = record.Id,
|
||||
WorkflowInstanceId = record.WorkflowInstanceId,
|
||||
|
|
@ -96,6 +102,7 @@ public class ActivityExecutionRecordSummary : Entity
|
|||
StartedAt = record.StartedAt,
|
||||
HasBookmarks = record.HasBookmarks,
|
||||
Status = record.Status,
|
||||
AggregateFaultCount = record.AggregateFaultCount,
|
||||
CompletedAt = record.CompletedAt
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@ public class DeleteBookmarks(IBookmarkManager bookmarkManager) : INotificationHa
|
|||
/// <inheritdoc />
|
||||
public async Task HandleAsync(WorkflowInstancesDeleting notification, CancellationToken cancellationToken)
|
||||
{
|
||||
await bookmarkManager.DeleteManyAsync(new BookmarkFilter { WorkflowInstanceIds = notification.Ids }, cancellationToken);
|
||||
await bookmarkManager.DeleteManyAsync(new()
|
||||
{
|
||||
WorkflowInstanceIds = notification.Ids
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,12 +8,12 @@ public class ActivityExecutionStats
|
|||
/// <summary>
|
||||
/// Gets or sets the ID of the activity.
|
||||
/// </summary>
|
||||
public string ActivityId { get; set; } = default!;
|
||||
public string ActivityId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the node ID of the activity.
|
||||
/// </summary>
|
||||
public string ActivityNodeId { get; set; } = default!;
|
||||
public string ActivityNodeId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the number of uncompleted executions.
|
||||
|
|
@ -39,4 +39,9 @@ public class ActivityExecutionStats
|
|||
/// Gets or sets a value whether this activity is faulted.
|
||||
/// </summary>
|
||||
public bool IsFaulted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the total count of faults aggregated from the activity execution and its descendants.
|
||||
/// </summary>
|
||||
public int AggregateFaultCount { get; set; }
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ public class ActivityExecutionStatsService : IActivityExecutionStatsService
|
|||
ActivityNodeIds = activityNodeIds?.ToList()
|
||||
};
|
||||
var order = new ActivityExecutionRecordOrder<DateTimeOffset>(x => x.StartedAt, OrderDirection.Ascending);
|
||||
var records = (await _store.FindManyAsync(filter, order, cancellationToken)).ToList();
|
||||
var records = (await _store.FindManySummariesAsync(filter, order, cancellationToken)).ToList();
|
||||
var groupedRecords = records.GroupBy(x => x.ActivityNodeId).ToList();
|
||||
var stats = groupedRecords.Select(grouping => new ActivityExecutionStats
|
||||
{
|
||||
|
|
@ -36,7 +36,8 @@ public class ActivityExecutionStatsService : IActivityExecutionStatsService
|
|||
CompletedCount = grouping.Count(x => x.CompletedAt != null),
|
||||
UncompletedCount = grouping.Count(x => x.CompletedAt == null),
|
||||
IsBlocked = grouping.Any(x => x.HasBookmarks),
|
||||
IsFaulted = grouping.Any(x => x.Status == ActivityStatus.Faulted)
|
||||
IsFaulted = grouping.Any(x => x.Status == ActivityStatus.Faulted),
|
||||
AggregateFaultCount = grouping.Last().AggregateFaultCount
|
||||
}).ToList();
|
||||
|
||||
return stats;
|
||||
|
|
@ -45,7 +46,7 @@ public class ActivityExecutionStatsService : IActivityExecutionStatsService
|
|||
/// <inheritdoc />
|
||||
public async Task<ActivityExecutionStats> GetStatsAsync(string workflowInstanceId, string activityNodeId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var stats = (await GetStatsAsync(workflowInstanceId, new[] { activityNodeId }, cancellationToken)).FirstOrDefault();
|
||||
var stats = (await GetStatsAsync(workflowInstanceId, [activityNodeId], cancellationToken)).FirstOrDefault();
|
||||
|
||||
return stats ?? new ActivityExecutionStats
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ public class BookmarksPersister(IBookmarkUpdater bookmarkUpdater, INotificationS
|
|||
await bookmarkUpdater.UpdateBookmarksAsync(updateBookmarksRequest);
|
||||
|
||||
// Publish domain event.
|
||||
await notificationSender.SendAsync(new WorkflowBookmarksIndexed(new IndexedWorkflowBookmarks(
|
||||
await notificationSender.SendAsync(new WorkflowBookmarksIndexed(new(
|
||||
updateBookmarksRequest.WorkflowExecutionContext,
|
||||
updateBookmarksRequest.Diff.Added,
|
||||
updateBookmarksRequest.Diff.Removed,
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper
|
|||
StartedAt = source.StartedAt,
|
||||
HasBookmarks = source.Bookmarks.Any(),
|
||||
Status = source.Status,
|
||||
AggregateFaultCount = source.AggregateFaultCount,
|
||||
CompletedAt = source.CompletedAt
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue