From 2b6294bcc132d2e76c3cb7bd3c8379c711ae6c59 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 5 Jan 2024 21:45:02 +0100 Subject: [PATCH] Add background execution handling to activity context This commit adds the ability to manage the background execution state directly within the activity execution context. This includes adding methods to set and verify the background execution state, and modifying the existing code to use these new methods. A method for handling activity scheduling during background execution has also been started, but its implementation is not finished yet. The HTTP Request activities were updated accordingly to reflect these changes. --- .../Activities/FlowSendHttpRequest.cs | 3 ++- .../Elsa.Http/Activities/SendHttpRequest.cs | 2 +- .../Activities/SendHttpRequestTask.cs | 20 +++++++++--------- ...kflowContextActivityExecutionMiddleware.cs | 2 +- .../Contexts/ActivityExecutionContext.cs | 6 ++++++ .../ActivityExecutionContextExtensions.cs | 12 ++++++++++- ...roundActivityExecutionContextExtensions.cs | 21 +++++++++++++++++++ .../BackgroundActivityInvokerMiddleware.cs | 7 +------ .../DefaultBackgroundActivityInvoker.cs | 2 +- 9 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/modules/Elsa.Http/Activities/FlowSendHttpRequest.cs b/src/modules/Elsa.Http/Activities/FlowSendHttpRequest.cs index cae3cbacf..f46f0e4d2 100644 --- a/src/modules/Elsa.Http/Activities/FlowSendHttpRequest.cs +++ b/src/modules/Elsa.Http/Activities/FlowSendHttpRequest.cs @@ -12,7 +12,8 @@ namespace Elsa.Http; /// /// Send an HTTP request. /// -[Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "HTTP Request (flow)", Kind = ActivityKind.Action)] +[Activity("Elsa", "HTTP", "Send an HTTP request. This activity yis deprecated in favor of the SendHttpRequestTask activity", DisplayName = "HTTP Request (flow) [Deprecated] ", Kind = ActivityKind.Task)] +[Obsolete("Use SendHttpRequestTask instead.")] public class FlowSendHttpRequest : SendHttpRequestBase, IActivityPropertyDefaultValueProvider { /// diff --git a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs index 0fd76e72c..b45e5558f 100644 --- a/src/modules/Elsa.Http/Activities/SendHttpRequest.cs +++ b/src/modules/Elsa.Http/Activities/SendHttpRequest.cs @@ -10,7 +10,7 @@ namespace Elsa.Http; /// /// Send an HTTP request. /// -[Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "HTTP Request", Kind = ActivityKind.Action)] +[Activity("Elsa", "HTTP", "Send an HTTP request.", DisplayName = "HTTP Request", Kind = ActivityKind.Task)] public class SendHttpRequest : SendHttpRequestBase { /// diff --git a/src/modules/Elsa.Http/Activities/SendHttpRequestTask.cs b/src/modules/Elsa.Http/Activities/SendHttpRequestTask.cs index 3c2c114ba..911ca0039 100644 --- a/src/modules/Elsa.Http/Activities/SendHttpRequestTask.cs +++ b/src/modules/Elsa.Http/Activities/SendHttpRequestTask.cs @@ -15,7 +15,7 @@ namespace Elsa.Http; /// /// Sends HTTP requests from a background task. /// -[Activity("Elsa", "HTTP", "Send an HTTP request from a background task.", DisplayName = "HTTP Request Task", Kind = ActivityKind.Job)] +[Activity("Elsa", "HTTP", "Send an HTTP request from a background task.", DisplayName = "HTTP Request Task", Kind = ActivityKind.Task)] public class SendHttpRequestTask : Activity { /// @@ -115,23 +115,23 @@ public class SendHttpRequestTask : Activity context.SetResult(response.StatusCode); context.Set(ParsedContent, parsedContent); - HandleResponse(context, response); + await HandleResponseAsync(context, response); } catch (HttpRequestException e) { context.AddExecutionLogEntry("Error", e.Message, payload: new { StackTrace = e.StackTrace }); context.JournalData.Add("Error", e.Message); - HandleRequestException(context, e); + await HandleRequestExceptionAsync(context, e); } catch (TaskCanceledException e) { context.AddExecutionLogEntry("Error", e.Message, payload: new { StackTrace = e.StackTrace }); context.JournalData.Add("Cancelled", true); - HandleTaskCanceledException(context, e); + await HandleTaskCanceledExceptionAsync(context, e); } } - private void HandleResponse(ActivityExecutionContext context, HttpResponseMessage response) + private async Task HandleResponseAsync(ActivityExecutionContext context, HttpResponseMessage response) { var expectedStatusCodes = ExpectedStatusCodes.GetOrDefault(context) ?? new List(0); var statusCode = (int)response.StatusCode; @@ -144,17 +144,17 @@ public class SendHttpRequestTask : Activity outcomes.Add("Done"); context.JournalData["StatusCode"] = statusCode; - context.SetBackgroundOutcomes(outcomes); + await context.CompleteActivityWithOutcomesAsync(outcomes.ToArray()); } - private void HandleRequestException(ActivityExecutionContext context, HttpRequestException exception) + private async Task HandleRequestExceptionAsync(ActivityExecutionContext context, HttpRequestException exception) { - context.SetBackgroundOutcomes(new[] { "Failed to connect" }); + await context.CompleteActivityWithOutcomesAsync("Failed to connect"); } - private void HandleTaskCanceledException(ActivityExecutionContext context, TaskCanceledException exception) + private async Task HandleTaskCanceledExceptionAsync(ActivityExecutionContext context, TaskCanceledException exception) { - context.SetBackgroundOutcomes(new[] { "Timeout" }); + await context.CompleteActivityWithOutcomesAsync("Timeout"); } private async Task ParseContentAsync(ActivityExecutionContext context, HttpContent httpContent) diff --git a/src/modules/Elsa.WorkflowContexts/Middleware/WorkflowContextActivityExecutionMiddleware.cs b/src/modules/Elsa.WorkflowContexts/Middleware/WorkflowContextActivityExecutionMiddleware.cs index d587de713..ccb7d4ba2 100644 --- a/src/modules/Elsa.WorkflowContexts/Middleware/WorkflowContextActivityExecutionMiddleware.cs +++ b/src/modules/Elsa.WorkflowContexts/Middleware/WorkflowContextActivityExecutionMiddleware.cs @@ -36,7 +36,7 @@ public class WorkflowContextActivityExecutionMiddleware : IActivityExecutionMidd } // Check if this is a background execution. - var isBackgroundExecution = context.TransientProperties.GetValueOrDefault(BackgroundActivityInvokerMiddleware.IsBackgroundExecution); + var isBackgroundExecution = context.GetIsBackgroundExecution(); // Is the activity configured to load the context? foreach (var providerType in providerTypes) diff --git a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs index 2fd7df1b5..1aa60ee4a 100644 --- a/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs +++ b/src/modules/Elsa.Workflows.Core/Contexts/ActivityExecutionContext.cs @@ -230,6 +230,12 @@ public class ActivityExecutionContext : IExecutionContext /// The options used to schedule the activity. public async ValueTask ScheduleActivityAsync(ActivityNode? activityNode, ActivityExecutionContext? owner = default, ScheduleWorkOptions? options = default) { + if (this.GetIsBackgroundExecution()) + { + // TODO: Capture the information in a serializable format and store it in the workflow execution context Properties dictionary. + // The information should be stored in a way that allows the workflow execution context to resume the activity execution context. + } + var completionCallback = options?.CompletionCallback; owner ??= this; diff --git a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs index 773597845..937934592 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/ActivityExecutionContextExtensions.cs @@ -456,6 +456,16 @@ public static class ActivityExecutionContextExtensions /// public static async ValueTask CompleteActivityAsync(this ActivityExecutionContext context, object? result = default) { + var outcomes = result as Outcomes; + + // If the activity is executing in the background, simply capture the result and return. + if (context.GetIsBackgroundExecution()) + { + if (outcomes != null) + context.SetBackgroundOutcomes(outcomes.Names); + return; + } + // If the activity is not running, do nothing. if (context.Status != ActivityStatus.Running) return; @@ -470,7 +480,7 @@ public static class ActivityExecutionContextExtensions context.Status = ActivityStatus.Completed; // Record the outcomes, if any. - if (result is Outcomes outcomes) + if (outcomes != null) context.JournalData["Outcomes"] = outcomes.Names; // Record the output, if any. diff --git a/src/modules/Elsa.Workflows.Core/Extensions/BackgroundActivityExecutionContextExtensions.cs b/src/modules/Elsa.Workflows.Core/Extensions/BackgroundActivityExecutionContextExtensions.cs index c4cc0de38..72d84e745 100644 --- a/src/modules/Elsa.Workflows.Core/Extensions/BackgroundActivityExecutionContextExtensions.cs +++ b/src/modules/Elsa.Workflows.Core/Extensions/BackgroundActivityExecutionContextExtensions.cs @@ -5,6 +5,27 @@ namespace Elsa.Workflows; /// public static class BackgroundActivityExecutionContextExtensions { + /// + /// A key into the activity execution context's transient properties that indicates whether the current activity is being executed in the background. + /// + public static readonly object IsBackgroundExecution = new(); + + /// + /// Configures the activity execution context to execute the current activity in the background. + /// + public static void SetIsBackgroundExecution(this ActivityExecutionContext activityExecutionContext, bool value = true) + { + activityExecutionContext.TransientProperties[IsBackgroundExecution] = value; + } + + /// + /// Gets a value indicating whether the current activity is being executed in the background. + /// + public static bool GetIsBackgroundExecution(this ActivityExecutionContext activityExecutionContext) + { + return activityExecutionContext.TransientProperties.ContainsKey(IsBackgroundExecution); + } + /// /// Sets the background outcomes. /// diff --git a/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/BackgroundActivityInvokerMiddleware.cs b/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/BackgroundActivityInvokerMiddleware.cs index c10cf90d1..6f440c8f5 100644 --- a/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/BackgroundActivityInvokerMiddleware.cs +++ b/src/modules/Elsa.Workflows.Runtime/Middleware/Activities/BackgroundActivityInvokerMiddleware.cs @@ -14,11 +14,6 @@ namespace Elsa.Workflows.Runtime.Middleware.Activities; /// public class BackgroundActivityInvokerMiddleware : DefaultActivityInvokerMiddleware { - /// - /// A key into the activity execution context's transient properties that indicates whether the current activity is being executed in the background. - /// - public static readonly object IsBackgroundExecution = new(); - internal static string GetBackgroundActivityOutputKey(string activityNodeId) => $"__BackgroundActivityOutput:{activityNodeId}"; internal static string GetBackgroundActivityOutcomesKey(string activityNodeId) => $"__BackgroundActivityOutcomes:{activityNodeId}"; internal static string GetBackgroundActivityJournalDataKey(string activityNodeId) => $"__BackgroundActivityJournalData:{activityNodeId}"; @@ -66,7 +61,7 @@ public class BackgroundActivityInvokerMiddleware : DefaultActivityInvokerMiddlew && (kind is ActivityKind.Job || (kind == ActivityKind.Task && activity.GetRunAsynchronously())); } - private static bool GetIsBackgroundExecution(ActivityExecutionContext context) => context.TransientProperties.ContainsKey(IsBackgroundExecution); + private static bool GetIsBackgroundExecution(ActivityExecutionContext context) => context.TransientProperties.ContainsKey(BackgroundActivityExecutionContextExtensions.IsBackgroundExecution); /// /// Schedules the current activity for execution in the background. diff --git a/src/modules/Elsa.Workflows.Runtime/Services/DefaultBackgroundActivityInvoker.cs b/src/modules/Elsa.Workflows.Runtime/Services/DefaultBackgroundActivityInvoker.cs index 379a10e24..90b7557a2 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/DefaultBackgroundActivityInvoker.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/DefaultBackgroundActivityInvoker.cs @@ -77,7 +77,7 @@ public class DefaultBackgroundActivityInvoker : IBackgroundActivityInvoker await _variablePersistenceManager.LoadVariablesAsync(workflowExecutionContext); // Mark the activity as being invoked from a background worker. - activityExecutionContext.TransientProperties[BackgroundActivityInvokerMiddleware.IsBackgroundExecution] = true; + activityExecutionContext.SetIsBackgroundExecution(); // Invoke the activity. await _activityInvoker.InvokeAsync(activityExecutionContext);