Merge remote-tracking branch 'origin/bug/activity-execution-record-state' into develop/3.6.0

This commit is contained in:
Sipke Schoorstra 2025-08-01 22:55:09 +02:00
commit 6d38ebbc97
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
11 changed files with 45 additions and 43 deletions

View file

@ -1,6 +1,7 @@
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Signals;
using Elsa.Workflows.Notifications;
using JetBrains.Annotations;
using ActivityCompleted = Elsa.Workflows.Signals.ActivityCompleted;
namespace Elsa.Workflows.Behaviors;

View file

@ -1,4 +1,5 @@
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Activities.Flowchart.Models;
using Elsa.Workflows.Signals;
@ -62,6 +63,9 @@ public partial class ActivityExecutionContext
// Update the completed at timestamp.
CompletedAt = WorkflowExecutionContext.SystemClock.UtcNow;
var mediator = GetRequiredService<INotificationSender>();
await mediator.SendAsync(new Notifications.ActivityCompleted(this), CancellationToken);
}
/// <summary>

View file

@ -4,4 +4,4 @@ namespace Elsa.Workflows.Signals;
/// Signaled when an activity has completed.
/// </summary>
/// <param name="Result">An optional result.</param>
public record ActivityCompleted(object? Result = default);
public record ActivityCompleted(object? Result = null);

View file

@ -33,7 +33,6 @@ public class DistributedRuntimeFeature : FeatureBase
{
Services
.AddScoped<DistributedWorkflowRuntime>()
.AddScoped<DistributedBookmarkQueueWorker>()
.AddCommandHandler<CancelWorkflowsCommandHandler>();
.AddScoped<DistributedBookmarkQueueWorker>();
}
}

View file

@ -8,34 +8,32 @@ namespace Elsa.Extensions;
public static class ActivityExecutionContextRecordExtensions
{
private const string ActivityExecutionRecordKey = "CapturedActivityExecutionRecord";
/// <summary>
/// Captures the activity execution record for the provided <see cref="ActivityExecutionContext"/> and stores it in the context's transient properties.
/// </summary>
public static async Task CaptureActivityExecutionRecordAsync(this ActivityExecutionContext context)
{
var mapper = context.GetRequiredService<IActivityExecutionMapper>();
var record = await mapper.MapAsync(context);
context.TransientProperties[ActivityExecutionRecordKey] = record;
}
/// <summary>
/// Retrieves the captured activity execution record from the transient properties of the provided <see cref="ActivityExecutionContext"/>.
/// If the record is not found, it maps and returns a new activity execution record using the <see cref="IActivityExecutionMapper"/> service.
/// </summary>
public static async Task<ActivityExecutionRecord> GetOrMapCapturedActivityExecutionRecordAsync(this ActivityExecutionContext context)
{
// If the record is already captured in the transient properties, return it, as it will contain the serialized state of the activity execution at the time of capture, rather than the current state.
// This is useful for scenarios where the activity execution state may change after the record is captured, such as referenced workflow variables.
if (context.TransientProperties.TryGetValue(ActivityExecutionRecordKey, out var capturedRecord))
return (ActivityExecutionRecord)capturedRecord;
// If the record is not captured, map a new activity execution record using the mapper.
var mapper = context.GetRequiredService<IActivityExecutionMapper>();
var record = await mapper.MapAsync(context);
if (context.TransientProperties.TryGetValue(ActivityExecutionRecordKey, out var capturedRecord))
{
var serializedSnapshot = ((ActivityExecutionRecord)capturedRecord).SerializedSnapshot!;
// Take the existing serialized snapshot.
record.SerializedSnapshot = serializedSnapshot;
// Update the serialized snapshot with the current record's properties.
// This will reflect the latest state of the activity execution context without losing the existing serialized snapshot representing e.g., variable values at the time of the record capture.
serializedSnapshot.HasBookmarks = record.HasBookmarks;
serializedSnapshot.Status = record.Status;
serializedSnapshot.AggregateFaultCount = record.AggregateFaultCount;
serializedSnapshot.CompletedAt = record.CompletedAt;
}
return record;
}
}

View file

@ -34,7 +34,6 @@ public static class PipelineWorkflowsFeatureExtensions
.UseExecutionLogging()
.UseNotifications()
.UseLogPersistenceModeEvaluation()
.UseActivityExecutionLogCapturing()
.UseBackgroundActivityInvoker();
configurePipeline?.Invoke(pipeline);

View file

@ -345,6 +345,7 @@ public class WorkflowRuntimeFeature(IModule module) : FeatureBase(module)
.AddNotificationHandler<RefreshActivityRegistry>()
.AddNotificationHandler<SignalBookmarkQueueWorker>()
.AddNotificationHandler<EvaluateParentLogPersistenceModes>()
.AddNotificationHandler<CaptureActivityExecutionState>()
.AddNotificationHandler<ValidateWorkflowRequestHandler>()
// Workflow activation strategies.

View file

@ -1,12 +1,14 @@
using Elsa.Mediator.Contracts;
using Elsa.Mediator.Models;
using Elsa.Workflows.Runtime.Commands;
using JetBrains.Annotations;
namespace Elsa.Workflows.Runtime.Handlers;
/// <summary>
/// Handles the <see cref="CancelWorkflowsCommand"/>.
/// </summary>
[UsedImplicitly]
public class CancelWorkflowsCommandHandler(IWorkflowRuntime workflowRuntime) : ICommandHandler<CancelWorkflowsCommand>
{
/// <inheritdoc />

View file

@ -0,0 +1,19 @@
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Notifications;
using JetBrains.Annotations;
namespace Elsa.Workflows.Runtime.Handlers;
/// <summary>
/// Captures the execution state of an activity when it completes.
/// </summary>
[UsedImplicitly]
public class CaptureActivityExecutionState : INotificationHandler<ActivityCompleted>
{
public async Task HandleAsync(ActivityCompleted notification, CancellationToken cancellationToken)
{
var context = notification.ActivityExecutionContext;
await context.CaptureActivityExecutionRecordAsync();
}
}

View file

@ -20,12 +20,4 @@ public static class ActivityExecutionPipelineBuilderExtensions
/// Installs the <see cref="EvaluateLogPersistenceModesMiddleware"/> which evaluates log persistence modes during activity execution.
/// </summary>
public static IActivityExecutionPipelineBuilder UseLogPersistenceModeEvaluation(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware<EvaluateLogPersistenceModesMiddleware>();
/// <summary>
/// Installs the <see cref="CaptureActivityExecutionRecordMiddleware"/> into the activity execution pipeline to capture and map activity execution details.
/// </summary>
public static IActivityExecutionPipelineBuilder UseActivityExecutionLogCapturing(this IActivityExecutionPipelineBuilder pipelineBuilder)
{
return pipelineBuilder.UseMiddleware<CaptureActivityExecutionRecordMiddleware>();
}
}

View file

@ -1,13 +0,0 @@
using Elsa.Extensions;
using Elsa.Workflows.Pipelines.ActivityExecution;
namespace Elsa.Workflows.Runtime.Middleware.Activities;
public class CaptureActivityExecutionRecordMiddleware(ActivityMiddlewareDelegate next) : IActivityExecutionMiddleware
{
public async ValueTask InvokeAsync(ActivityExecutionContext context)
{
await next(context);
await context.CaptureActivityExecutionRecordAsync();
}
}