.NET: Add Message Delivery Callback Overloads to Executor (#5081)

* feat: Implement Executor Message Delivery Event callbacks

* fix: ResumeAsync does not run pending steps

* fix: address review comments
This commit is contained in:
Jacob Alber
2026-04-03 21:19:15 +00:00
committed by GitHub
parent d284d96a9e
commit 55ae57c0ed
4 changed files with 282 additions and 13 deletions
@@ -310,22 +310,40 @@ public abstract class Executor : IIdentified
return result.Result;
}
/// <summary>
/// Invoked once per superstep before any messages are delivered to the Executor.
/// </summary>
/// <param name="context">The workflow context.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
protected internal virtual ValueTask OnMessageDeliveryStartingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
/// <summary>
/// Invoked once per superstep after all messages have been delivered to the Executor.
/// </summary>
/// <param name="context">The workflow context.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
protected internal virtual ValueTask OnMessageDeliveryFinishedAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
/// <summary>
/// Invoked before a checkpoint is saved, allowing custom pre-save logic in derived classes.
/// </summary>
/// <param name="context">The workflow context.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
protected internal virtual ValueTask OnCheckpointingAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
/// <summary>
/// Invoked after a checkpoint is loaded, allowing custom post-load logic in derived classes.
/// </summary>
/// <param name="context">The workflow context.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests.
/// The default is <see cref="CancellationToken.None"/>.</param>
/// <returns>A ValueTask representing the asynchronous operation.</returns>
protected internal virtual ValueTask OnCheckpointRestoredAsync(IWorkflowContext context, CancellationToken cancellationToken = default) => default;
/// <summary>
@@ -182,6 +182,9 @@ public sealed class InProcessExecutionEnvironment : IWorkflowExecutionEnvironmen
AsyncRunHandle runHandle = await this.ResumeRunAsync(workflow, fromCheckpoint, [], cancellationToken)
.ConfigureAwait(false);
return new(runHandle);
Run run = new(runHandle);
await run.RunToNextHaltAsync(cancellationToken).ConfigureAwait(false);
return run;
}
}
@@ -249,17 +249,33 @@ internal sealed class InProcessRunner : ISuperStepRunner, ICheckpointingHandle
Executor executor = await this.RunContext.EnsureExecutorAsync(receiverId, this.StepTracer, cancellationToken).ConfigureAwait(false);
this.StepTracer.TraceActivated(receiverId);
while (envelopes.TryDequeue(out var envelope))
{
(object message, TypeId messageType) = await TranslateMessageAsync(envelope).ConfigureAwait(false);
await executor.ExecuteCoreAsync(
message,
messageType,
this.RunContext.BindWorkflowContext(receiverId, envelope.TraceContext),
this.TelemetryContext,
cancellationToken
).ConfigureAwait(false);
// TODO: #5084 - Add delivery-level activity (max one per step per executor) to capture non-message
// specific invocations of executor logic.
IWorkflowContext tracelessContext = this.RunContext.BindWorkflowContext(receiverId);
try
{
await executor.OnMessageDeliveryStartingAsync(tracelessContext, cancellationToken)
.ConfigureAwait(false);
while (envelopes.TryDequeue(out var envelope))
{
(object message, TypeId messageType) = await TranslateMessageAsync(envelope).ConfigureAwait(false);
await executor.ExecuteCoreAsync(
message,
messageType,
this.RunContext.BindWorkflowContext(receiverId, envelope.TraceContext),
this.TelemetryContext,
cancellationToken
).ConfigureAwait(false);
}
}
finally
{
await executor.OnMessageDeliveryFinishedAsync(tracelessContext, cancellationToken)
.ConfigureAwait(false);
}
async ValueTask<(object, TypeId)> TranslateMessageAsync(MessageEnvelope envelope)