Remove obsolete properties and refactor activity evaluation (#6603)

* Remove obsolete properties and refactor activity evaluation

Refactored activity input and log persistence property evaluation using improved notification handlers. Removed redundant `ActivityState` property and associated serialization logic, ensuring payloads are only serialized when necessary. All changes streamline workflow processing and enhance maintainability.

* Refactor mediator call to inline cancellation token.

Replaced the separate variable for the cancellation token with an inline reference for clarity and reduced redundancy. This simplifies the code without altering functionality.
This commit is contained in:
Sipke Schoorstra 2025-04-18 16:21:25 +02:00 committed by GitHub
parent 476656ccce
commit c8ed08cc85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 92 additions and 50 deletions

View file

@ -104,12 +104,12 @@ internal class DapperWorkflowExecutionLogStore(Store<WorkflowExecutionLogRecordR
private Page<WorkflowExecutionLogRecord> Map(Page<WorkflowExecutionLogRecordRecord> source)
{
return new Page<WorkflowExecutionLogRecord>(source.Items.Select(Map).ToList(), source.TotalCount);
return new(source.Items.Select(Map).ToList(), source.TotalCount);
}
private WorkflowExecutionLogRecordRecord Map(WorkflowExecutionLogRecord source)
{
return new WorkflowExecutionLogRecordRecord
return new()
{
Id = source.Id,
WorkflowDefinitionId = source.WorkflowDefinitionId,
@ -128,15 +128,24 @@ internal class DapperWorkflowExecutionLogStore(Store<WorkflowExecutionLogRecordR
EventName = source.EventName,
Message = source.Message,
Source = source.Source,
SerializedActivityState = source.ActivityState != null ? payloadSerializer.Serialize(source.ActivityState) : null,
SerializedPayload = source.Payload != null ? payloadSerializer.Serialize(source.Payload) : null,
SerializedPayload = ShouldSerializePayload(source) ? payloadSerializer.Serialize(source.Payload!) : null,
TenantId = source.TenantId
};
}
private bool ShouldSerializePayload(WorkflowExecutionLogRecord source)
{
return source.Payload switch
{
null => false,
IDictionary<string, object> dictionary => dictionary.Count > 0,
_ => true
};
}
private WorkflowExecutionLogRecord Map(WorkflowExecutionLogRecordRecord source)
{
return new WorkflowExecutionLogRecord
return new()
{
Id = source.Id,
WorkflowDefinitionId = source.WorkflowDefinitionId,
@ -155,7 +164,6 @@ internal class DapperWorkflowExecutionLogStore(Store<WorkflowExecutionLogRecordR
EventName = source.EventName,
Message = source.Message,
Source = source.Source,
ActivityState = source.SerializedActivityState != null ? payloadSerializer.Deserialize<IDictionary<string, object>>(source.SerializedActivityState) : null,
Payload = source.SerializedPayload != null ? payloadSerializer.Deserialize(source.SerializedPayload) : null,
TenantId = source.TenantId
};

View file

@ -74,8 +74,7 @@ public class EFCoreWorkflowExecutionLogStore(EntityStore<RuntimeElsaDbContext, W
private async ValueTask OnSaveAsync(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord entity, CancellationToken cancellationToken)
{
entity = entity.SanitizeLogMessage();
dbContext.Entry(entity).Property("SerializedActivityState").CurrentValue = entity.ActivityState?.Any() == true ? safeSerializer.Serialize(entity.ActivityState) : null;
dbContext.Entry(entity).Property("SerializedPayload").CurrentValue = entity.Payload != null ? safeSerializer.Serialize(entity.Payload) : null;
dbContext.Entry(entity).Property("SerializedPayload").CurrentValue = ShouldSerializePayload(entity) ? safeSerializer.Serialize(entity.Payload) : null;
}
private async ValueTask OnLoadAsync(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord? entity, CancellationToken cancellationToken)
@ -84,7 +83,6 @@ public class EFCoreWorkflowExecutionLogStore(EntityStore<RuntimeElsaDbContext, W
return;
entity.Payload = await LoadPayload(dbContext, entity);
entity.ActivityState = await LoadActivityState(dbContext, entity);
}
private ValueTask<object?> LoadPayload(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord entity)
@ -93,10 +91,14 @@ public class EFCoreWorkflowExecutionLogStore(EntityStore<RuntimeElsaDbContext, W
return new(!string.IsNullOrEmpty(json) ? JsonSerializer.Deserialize<object>(json) : null);
}
private ValueTask<IDictionary<string, object>?> LoadActivityState(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord entity)
private bool ShouldSerializePayload(WorkflowExecutionLogRecord source)
{
var json = dbContext.Entry(entity).Property<string>("SerializedActivityState").CurrentValue;
return new(!string.IsNullOrEmpty(json) ? JsonSerializer.Deserialize<IDictionary<string, object>>(json) : null);
return source.Payload switch
{
null => false,
IDictionary<string, object> dictionary => dictionary.Count > 0,
_ => true
};
}
private static IQueryable<WorkflowExecutionLogRecord> Filter(IQueryable<WorkflowExecutionLogRecord> queryable, WorkflowExecutionLogRecordFilter filter) => filter.Apply(queryable);

View file

@ -14,17 +14,17 @@ namespace Elsa.Scheduling.Activities;
public class Cron : EventGenerator
{
/// <inheritdoc />
public Cron([CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : base(source, line)
public Cron([CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : base(source, line)
{
}
/// <inheritdoc />
public Cron(string cronExpression, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(new Input<string>(cronExpression), source, line)
public Cron(string cronExpression, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(new Input<string>(cronExpression), source, line)
{
}
/// <inheritdoc />
public Cron(Input<string> cronExpression, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) : this(source, line)
public Cron(Input<string> cronExpression, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) : this(source, line)
{
CronExpression = cronExpression;
}
@ -33,7 +33,7 @@ public class Cron : EventGenerator
/// The interval at which the timer should execute.
/// </summary>
[Input(Description = "The CRON expression at which the timer should execute.")]
public Input<string> CronExpression { get; set; } = default!;
public Input<string> CronExpression { get; set; } = null!;
/// <inheritdoc />
protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
@ -62,5 +62,5 @@ public class Cron : EventGenerator
/// <summary>
/// Creates a new <see cref="Cron"/> activity set to trigger at the specified cron expression.
/// </summary>
public static Cron FromCronExpression(string value, [CallerFilePath] string? source = default, [CallerLineNumber] int? line = default) => new(value, source, line);
public static Cron FromCronExpression(string value, [CallerFilePath] string? source = null, [CallerLineNumber] int? line = null) => new(value, source, line);
}

View file

@ -14,5 +14,5 @@ internal record ExecutionLogRecord(
string? EventName,
string? Message,
string? Source,
IDictionary<string, object>? ActivityState,
[property: Obsolete] IDictionary<string, object>? ActivityState,
object? Payload);

View file

@ -21,7 +21,7 @@ public abstract class Activity : IActivity, ISignalHandler
/// <summary>
/// Constructor.
/// </summary>
protected Activity(string? source = default, int? line = default)
protected Activity(string? source = null, int? line = null)
{
this.SetSource(source, line);
Type = ActivityTypeNameHelper.GenerateTypeName(GetType());
@ -30,17 +30,17 @@ public abstract class Activity : IActivity, ISignalHandler
}
/// <inheritdoc />
protected Activity(string activityType, int version = 1, string? source = default, int? line = default) : this(source, line)
protected Activity(string activityType, int version = 1, string? source = null, int? line = null) : this(source, line)
{
Type = activityType;
Version = version;
}
/// <inheritdoc />
public string Id { get; set; } = default!;
public string Id { get; set; } = null!;
/// <inheritdoc />
public string NodeId { get; set; } = default!;
public string NodeId { get; set; } = null!;
/// <inheritdoc />
public string? Name { get; set; }

View file

@ -1,11 +1,14 @@
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Notifications;
using Elsa.Workflows.Signals;
using JetBrains.Annotations;
namespace Elsa.Workflows.Behaviors;
/// <summary>
/// Implements a behavior that invokes "child completed" callbacks on parent activities.
/// </summary>
[UsedImplicitly]
public class ScheduledChildCallbackBehavior : Behavior
{
/// <inheritdoc />
@ -21,18 +24,16 @@ public class ScheduledChildCallbackBehavior : Behavior
var childActivityNode = childActivityExecutionContext.ActivityNode;
var callbackEntry = activityExecutionContext.WorkflowExecutionContext.PopCompletionCallback(activityExecutionContext, childActivityNode);
if (callbackEntry == null)
return;
// Before invoking the parent activity, make sure its properties are evaluated.
if (!activityExecutionContext.GetHasEvaluatedProperties())
await activityExecutionContext.EvaluateInputPropertiesAsync();
if (callbackEntry.CompletionCallback != null)
if (callbackEntry?.CompletionCallback != null)
{
var completedContext = new ActivityCompletedContext(activityExecutionContext, childActivityExecutionContext, signal.Result);
var tag = callbackEntry.Tag;
completedContext.TargetContext.Tag = tag;
var mediator = activityExecutionContext.GetRequiredService<IMediator>();
var invokingActivityCallbackNotification = new InvokingActivityCallback(activityExecutionContext, childActivityExecutionContext);
await mediator.SendAsync(invokingActivityCallbackNotification, context.CancellationToken);
await callbackEntry.CompletionCallback(completedContext);
}
}

View file

@ -23,7 +23,7 @@ public partial class ActivityExecutionContext
ClearBookmarks();
ClearCompletionCallbacks();
WorkflowExecutionContext.Bookmarks.RemoveWhere(x => x.ActivityNodeId == NodeId);
AddExecutionLogEntry("Canceled", payload: JournalData);
AddExecutionLogEntry("Canceled");
await this.SendSignalAsync(new CancelSignal());
await CancelChildActivitiesAsync();

View file

@ -41,7 +41,7 @@ public partial class ActivityExecutionContext
JournalData["Outcomes"] = outcomes.Names;
// Add an execution log entry.
AddExecutionLogEntry("Completed", payload: JournalData);
AddExecutionLogEntry("Completed");
// Send a signal.
await this.SendSignalAsync(new ActivityCompleted(result));

View file

@ -294,7 +294,7 @@ public static partial class ActivityExecutionContextExtensions
context.WorkflowExecutionContext.Bookmarks.RemoveWhere(x => x.ActivityNodeId == context.NodeId);
// Add an execution log entry.
context.AddExecutionLogEntry("Canceled", payload: context.JournalData);
context.AddExecutionLogEntry("Canceled");
await context.SendSignalAsync(new CancelSignal());
await publisher.SendAsync(new ActivityCancelled(context));

View file

@ -0,0 +1,15 @@
using Elsa.Extensions;
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Notifications;
namespace Elsa.Workflows.Handlers;
public class EvaluateParentInputProperties : INotificationHandler<InvokingActivityCallback>
{
public async Task HandleAsync(InvokingActivityCallback notification, CancellationToken cancellationToken)
{
// Before invoking the parent activity, make sure its properties are evaluated.
if (!notification.Parent.GetHasEvaluatedProperties())
await notification.Parent.EvaluateInputPropertiesAsync();
}
}

View file

@ -33,7 +33,7 @@ public class ExecutionLogMiddleware(ActivityMiddlewareDelegate next) : IActivity
if (context.Status == ActivityStatus.Running)
{
if (IsActivityBookmarked(context))
context.AddExecutionLogEntry("Suspended", payload: context.JournalData);
context.AddExecutionLogEntry("Suspended");
}
}
catch (Exception exception)

View file

@ -0,0 +1,5 @@
using Elsa.Mediator.Contracts;
namespace Elsa.Workflows.Notifications;
public record InvokingActivityCallback(ActivityExecutionContext Parent, ActivityExecutionContext Child) : INotification;

View file

@ -91,6 +91,7 @@ public class WorkflowExecutionLogRecord : Entity, ILogRecord
/// <summary>
/// The state of the activity at the time of the log entry.
/// </summary>
[Obsolete("Look at the ActivityExecutionRecord.ActivityState property instead.")]
public IDictionary<string, object>? ActivityState { get; set; }
/// <summary>

View file

@ -344,6 +344,7 @@ public class WorkflowRuntimeFeature(IModule module) : FeatureBase(module)
.AddNotificationHandler<DeleteWorkflowExecutionLogRecords>()
.AddNotificationHandler<RefreshActivityRegistry>()
.AddNotificationHandler<SignalBookmarkQueueWorker>()
.AddNotificationHandler<EvaluateParentLogPersistenceModes>()
// Workflow activation strategies.
.AddScoped<IWorkflowActivationStrategy, SingletonStrategy>()

View file

@ -6,6 +6,11 @@ public static class ActivityExecutionContextExtensions
{
private static object LogPersistenceMapKey { get; } = new();
public static bool HasLogPersistenceModeMap(this ActivityExecutionContext context)
{
return context.TransientProperties.ContainsKey(LogPersistenceMapKey);
}
public static ActivityLogPersistenceModeMap GetLogPersistenceModeMap(this ActivityExecutionContext context)
{
return context.TransientProperties.GetValueOrDefault(LogPersistenceMapKey, () => new ActivityLogPersistenceModeMap())!;

View file

@ -0,0 +1,17 @@
using Elsa.Mediator.Contracts;
using Elsa.Workflows.Notifications;
namespace Elsa.Workflows.Runtime.Handlers;
public class EvaluateParentLogPersistenceModes(IActivityPropertyLogPersistenceEvaluator persistenceEvaluator) : INotificationHandler<InvokingActivityCallback>
{
public async Task HandleAsync(InvokingActivityCallback notification, CancellationToken cancellationToken)
{
// Before invoking the parent activity, make sure its persistence log properties are evaluated.
if (!notification.Parent.HasLogPersistenceModeMap())
{
var persistenceLogMap = await persistenceEvaluator.EvaluateLogPersistenceModesAsync(notification.Parent);
notification.Parent.SetLogPersistenceModeMap(persistenceLogMap);
}
}
}

View file

@ -9,14 +9,13 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper
{
public ActivityExecutionRecord Map(ActivityExecutionContext source)
{
var payload = GetPayload(source);
var outputs = source.GetOutputs();
var inputs = source.GetInputs();
var persistenceMap = source.GetLogPersistenceModeMap();
var persistableInputs = GetPersistableInputOutput(inputs, persistenceMap.Inputs);
var persistableOutputs = GetPersistableInputOutput(outputs, persistenceMap.Outputs);
var persistableProperties = GetPersistableDictionary(source.Properties!, persistenceMap.InternalState);
var persistablePayload = GetPersistableDictionary(payload!, persistenceMap.InternalState);
var persistableJournalData = GetPersistableDictionary(source.JournalData!, persistenceMap.InternalState);
return new()
{
@ -29,7 +28,7 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper
ActivityState = persistableInputs,
Outputs = persistableOutputs,
Properties = persistableProperties,
Payload = persistablePayload!,
Payload = persistableJournalData!,
Exception = ExceptionState.FromException(source.Exception),
ActivityTypeVersion = source.Activity.Version,
StartedAt = source.StartedAt,
@ -62,15 +61,4 @@ public class DefaultActivityExecutionMapper : IActivityExecutionMapper
{
return mode == LogPersistenceMode.Include ? dictionary : null;
}
private static IDictionary<string, object> GetPayload(ActivityExecutionContext source)
{
var outcomes = source.JournalData.TryGetValue("Outcomes", out var resultValue) ? resultValue as string[] : null;
var payload = new Dictionary<string, object>();
if (outcomes != null)
payload.Add("Outcomes", outcomes);
return payload;
}
}

View file

@ -25,7 +25,6 @@ public class WorkflowExecutionLogRecordExtractor(IIdentityGenerator identityGene
WorkflowInstanceId = context.Id,
WorkflowVersion = context.Workflow.Version,
Source = x.Source,
ActivityState = x.ActivityState,
Payload = x.Payload,
Timestamp = x.Timestamp,
Sequence = x.Sequence