Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public async Task force_catch_up_returns_for_async_daemon_without_side_effects()
exceptions.ShouldBeEmpty();
}

[Fact(Timeout = 30000, Skip = "Flaky in CI under timing pressure — see https://github.com/JasperFx/marten/issues/4462. Passes locally 3-for-3 but CI's Polly retry pipeline cancels the daemon catch-up socket I/O before it completes, surfacing an OperationCanceledException in the returned exceptions list.")]
[Fact(Timeout = 60000)]
public async Task force_catch_up_invokes_message_batch_lifecycle_with_custom_outbox()
{
var outbox = new RecordingOutbox();
Expand All @@ -108,7 +108,11 @@ public async Task force_catch_up_invokes_message_batch_lifecycle_with_custom_out
await session.SaveChangesAsync();
}

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
// Wider CT than `force_catch_up_returns_for_async_daemon_without_side_effects`
// because the custom-outbox lifecycle path adds extra hops (CreateBatch →
// BeforeCommit → AfterCommit per shard) on top of the catch-up loop —
// see #4462 for the timing context.
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45));
var exceptions = await host.ForceAllMartenDaemonActivityToCatchUpAsync(cts.Token);
exceptions.ShouldBeEmpty();

Expand Down
18 changes: 18 additions & 0 deletions src/Marten/Events/AsyncProjectionTestingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,17 @@ public static async Task<IReadOnlyList<Exception>> ForceAllMartenDaemonActivityT

logger.LogDebug("Executed a ProjectionDaemon.CatchUp() against {Daemon} in the main Marten store", daemon);
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
{
// Caller-initiated cancellation — propagate as a normal cancellation
// signal rather than reporting it as a daemon error. Without this,
// a test that supplies a CTS that fires while the daemon's
// catch-up pipeline (Polly-wrapped per-shard execution) is mid-
// flight would see the propagated OperationCanceledException
// surface as a fake "exceptions list is non-empty" assertion
// failure — see #4462.
throw;
}
catch (Exception e)
{
logger.LogError(e, "Error trying to execute a CatchUp on {Daemon} in the main Marten store", daemon);
Expand Down Expand Up @@ -405,6 +416,13 @@ public static async Task<IReadOnlyList<Exception>> ForceAllMartenDaemonActivityT

logger.LogDebug("Executed a ProjectionDaemon.CatchUp() against {Daemon} in Marten store {StoreType}", daemon, typeof(T).FullNameInCode());
}
catch (OperationCanceledException) when (cancellation.IsCancellationRequested)
{
// Caller-initiated cancellation — propagate, do not classify as
// a daemon error. See #4462 + matching guard on the main-store
// variant above.
throw;
}
catch (Exception e)
{
logger.LogError(e, "Error trying to execute a CatchUp on {Daemon} in Marten store {StoreType}", daemon, typeof(T).FullNameInCode());
Expand Down
Loading