From c3c6f12858b706ee5f07a2642866b719ec0d0b61 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 31 Jul 2026 04:50:31 +0200 Subject: [PATCH] fix: restore commit notification scope before flush Detach the AsyncLocal scope synchronously so notifications published after an asynchronous flush are not re-buffered and discarded. Add regression coverage that forces the flush across an async boundary. --- .../WorkflowCommitNotificationBuffer.cs | 7 ++++- .../WorkflowCommitNotificationBufferTests.cs | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/modules/Elsa.Workflows.Runtime/Services/WorkflowCommitNotificationBuffer.cs b/src/modules/Elsa.Workflows.Runtime/Services/WorkflowCommitNotificationBuffer.cs index 93d8cd73c..8b3db35a1 100644 --- a/src/modules/Elsa.Workflows.Runtime/Services/WorkflowCommitNotificationBuffer.cs +++ b/src/modules/Elsa.Workflows.Runtime/Services/WorkflowCommitNotificationBuffer.cs @@ -38,10 +38,15 @@ public class WorkflowCommitNotificationBuffer(IMediator mediator, ILogger? exceptions = null; foreach (var entry in _entries) diff --git a/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/WorkflowCommitNotificationBufferTests.cs b/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/WorkflowCommitNotificationBufferTests.cs index 6b099b5ff..97aa6d9f2 100644 --- a/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/WorkflowCommitNotificationBufferTests.cs +++ b/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/WorkflowCommitNotificationBufferTests.cs @@ -24,6 +24,36 @@ public class WorkflowCommitNotificationBufferTests await mediator.Received(1).SendAsync(notification, Arg.Any(), Arg.Any()); } + [Fact] + public async Task SendAsync_AfterScopeIsFlushed_PublishesImmediately() + { + var mediator = Substitute.For(); + var buffer = CreateBuffer(mediator); + var sender = new WorkflowCommitNotificationSender(mediator, buffer); + var bufferedNotification = new TestNotification(); + var subsequentNotification = new TestNotification(); + var notificationPublishingStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var continueNotificationPublishing = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + mediator + .SendAsync(bufferedNotification, Arg.Any(), Arg.Any()) + .Returns(async _ => + { + notificationPublishingStarted.SetResult(); + await continueNotificationPublishing.Task; + }); + + using var scope = buffer.Begin(); + await sender.SendAsync(bufferedNotification); + var flushTask = scope.FlushAsync(); + await notificationPublishingStarted.Task; + continueNotificationPublishing.SetResult(); + await flushTask; + await sender.SendAsync(subsequentNotification); + + await mediator.Received(1).SendAsync(bufferedNotification, Arg.Any(), Arg.Any()); + await mediator.Received(1).SendAsync(subsequentNotification, Arg.Any(), Arg.Any()); + } + [Fact] public async Task SendAsync_WhenBufferingScopeIsDisposedWithoutFlush_DiscardsNotifications() {