Refactor activity execution state tracking

Introduce `IsExecuting` flag to explicitly track activity execution state, improving clarity and control over workflow activity handling. Adjust scheduling intervals and add concurrency handling for database updates to enhance reliability and performance in interrupted workflows.
This commit is contained in:
Sipke Schoorstra 2025-02-22 19:45:44 +01:00
parent 3c0ab8b172
commit 3beedb9ec0
No known key found for this signature in database
GPG key ID: 5C10502B28A4268F
8 changed files with 57 additions and 6 deletions

View file

@ -681,10 +681,10 @@ services.Configure<RecurringTaskOptions>(options =>
options.Schedule.ConfigureTask<TriggerBookmarkQueueRecurringTask>(TimeSpan.FromSeconds(300));
options.Schedule.ConfigureTask<PurgeBookmarkQueueRecurringTask>(TimeSpan.FromSeconds(300));
options.Schedule.ConfigureTask<UpdateExpiredSecretsRecurringTask>(TimeSpan.FromHours(4));
options.Schedule.ConfigureTask<RestartInterruptedWorkflowsTask>(TimeSpan.FromMinutes(1));
options.Schedule.ConfigureTask<RestartInterruptedWorkflowsTask>(TimeSpan.FromSeconds(15));
});
services.Configure<RuntimeOptions>(options => { options.InactivityThreshold = TimeSpan.FromMinutes(1); });
services.Configure<RuntimeOptions>(options => { options.InactivityThreshold = TimeSpan.FromSeconds(15); });
services.Configure<BookmarkQueuePurgeOptions>(options => options.Ttl = TimeSpan.FromSeconds(10));
services.Configure<CachingOptions>(options => options.CacheDuration = TimeSpan.FromDays(1));

View file

@ -157,7 +157,35 @@ public class EFCoreWorkflowInstanceStore : IWorkflowInstanceStore
Id = workflowInstanceId,
UpdatedAt = value
};
await _store.UpdatePartialAsync(entity, [x => x.UpdatedAt], cancellationToken);
await using var dbContext = await _store.CreateDbContextAsync(cancellationToken);
dbContext.Attach(entity);
dbContext.Entry(entity).Property(x => x.UpdatedAt).IsModified = true;
try
{
await dbContext.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateConcurrencyException e)
{
foreach (var entry in e.Entries)
{
var proposedValues = entry.CurrentValues;
var databaseValues = await entry.GetDatabaseValuesAsync(cancellationToken);
if(databaseValues == null)
continue;
var updatedAtProperty = entry.Metadata.GetProperty(nameof(WorkflowInstance.UpdatedAt));
var proposedValue = (DateTimeOffset)proposedValues[updatedAtProperty]!;
var databaseValue = (DateTimeOffset)databaseValues[updatedAtProperty]!;
if (proposedValue > databaseValue)
proposedValues[updatedAtProperty] = proposedValue;
entry.OriginalValues.SetValues(databaseValues);
}
}
}
/// <inheritdoc />

View file

@ -48,7 +48,6 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable
var expressionExecutionContextProps = ExpressionExecutionContextExtensions.CreateActivityExecutionContextPropertiesFrom(workflowExecutionContext, workflowExecutionContext.Input);
expressionExecutionContextProps[ExpressionExecutionContextExtensions.ActivityKey] = activity;
ExpressionExecutionContext = new(workflowExecutionContext.ServiceProvider, new(), parentActivityExecutionContext?.ExpressionExecutionContext ?? workflowExecutionContext.ExpressionExecutionContext, expressionExecutionContextProps, Taint, CancellationToken);
;
Activity = activity;
ActivityDescriptor = activityDescriptor;
StartedAt = startedAt;
@ -84,6 +83,17 @@ public partial class ActivityExecutionContext : IExecutionContext, IDisposable
/// </summary>
public bool IsCompleted => Status is ActivityStatus.Completed or ActivityStatus.Canceled;
/// <summary>
/// Gets or sets a value indicating whether the activity is actively executing.
/// </summary>
/// <remarks>
/// This flag is set to <c>true</c> immediately before the activity begins execution
/// and is set to <c>false</c> once the execution is completed.
/// It can be used to determine if an activity was in-progress in case of unexpected
/// application termination, allowing the system to retry execution upon restarting.
/// </remarks>
public bool IsExecuting { get; set; }
/// <summary>
/// The workflow execution context.
/// </summary>

View file

@ -11,7 +11,7 @@ public enum ActivityStatus
Pending,
/// <summary>
/// The activity is in the Running state. Note that event if an activity is running, it may not be executing.
/// The activity is in the Running state. While in this state, the activity is not necessarily being actively executed. This state represents a logical status rather than a physical action.
/// </summary>
Running,

View file

@ -48,6 +48,9 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I
context.AddExecutionLogEntry("Precondition Failed", "Cannot execute at this time");
return;
}
// Mark activity as executing.
context.IsExecuting = true;
// Conditionally commit the workflow state.
if (ShouldCommit(context, ActivityLifetimeEvent.ActivityExecuting))
@ -83,6 +86,9 @@ public class DefaultActivityInvokerMiddleware(ActivityMiddlewareDelegate next, I
workflowExecutionContext.Bookmarks.AddRange(context.Bookmarks);
logger.LogDebug("Added {BookmarkCount} bookmarks to the workflow execution context", context.Bookmarks.Count);
}
// Mark activity as executed.
context.IsExecuting = false;
// Conditionally commit the workflow state.
if (ShouldCommit(context, ActivityLifetimeEvent.ActivityExecuted))

View file

@ -158,7 +158,7 @@ public class WorkflowRunner(
else
{
// Check if there are any leaf nodes in the Pending state.
var pendingActivityExecutionContexts = workflowExecutionContext.ActivityExecutionContexts.Where(x => x.Status == ActivityStatus.Pending).ToList();
var pendingActivityExecutionContexts = workflowExecutionContext.ActivityExecutionContexts.Where(x => x.IsExecuting).ToList();
if( pendingActivityExecutionContexts.Count > 0)
{

View file

@ -145,6 +145,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor
activityExecutionContext.ActivityState.Merge(activityExecutionContextState.ActivityState);
activityExecutionContext.TransitionTo(activityExecutionContextState.Status);
activityExecutionContext.IsExecuting = activityExecutionContextState.IsExecuting;
activityExecutionContext.StartedAt = activityExecutionContextState.StartedAt;
activityExecutionContext.CompletedAt = activityExecutionContextState.CompletedAt;
activityExecutionContext.Tag = activityExecutionContextState.Tag;
@ -232,6 +233,7 @@ public class WorkflowStateExtractor : IWorkflowStateExtractor
Properties = activityExecutionContext.Properties,
ActivityState = activityExecutionContext.ActivityState,
Status = activityExecutionContext.Status,
IsExecuting = activityExecutionContext.IsExecuting,
StartedAt = activityExecutionContext.StartedAt,
CompletedAt = activityExecutionContext.CompletedAt,
Tag = activityExecutionContext.Tag,

View file

@ -55,6 +55,11 @@ public class ActivityExecutionContextState
/// The status of the activity.
/// </summary>
public ActivityStatus Status { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the activity is actively executing.
/// </summary>
public bool IsExecuting { get; set; }
/// <summary>
/// The time at which the activity execution began.