Add scheduling support to the Mocha in-memory transport#9987
Conversation
There was a problem hiding this comment.
Pull request overview
Adds first-class scheduled delivery semantics to Mocha’s in-memory transport by wiring it into the shared scheduling middleware, backed by an in-process scheduled message store + hosted background worker, and updates docs/tests accordingly.
Changes:
- Register an in-memory
IScheduledMessageStore+IHostedServicedispatcher inAddInMemory()and enable scheduler core by default. - Add unit/integration coverage for due-time delivery, cancellation, and payload/header preservation under pooled context reuse.
- Update Mocha v16 docs (scheduling, sagas, in-memory transport) to reflect in-memory scheduling + cancellation support and non-durability.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| website/src/docs/mocha/v16/transports/in-memory.md | Documents in-memory scheduled delivery behavior, non-durability, and cancellation. |
| website/src/docs/mocha/v16/scheduling.md | Updates cancellation semantics and transport capability matrix for InMemory. |
| website/src/docs/mocha/v16/sagas.md | Updates saga Timeout() prerequisites/behavior notes for in-memory scheduling + cancellation. |
| src/Mocha/test/Mocha.Transport.InMemory.Tests/Scheduling/InMemorySchedulingTests.cs | New end-to-end tests for scheduled delivery timing, cancellation, and payload preservation. |
| src/Mocha/test/Mocha.Transport.InMemory.Tests/Scheduling/InMemoryScheduledMessageStoreTests.cs | New unit tests for store token format, ordering, cancellation, and deep-copy behavior. |
| src/Mocha/test/Mocha.Transport.InMemory.Tests/Mocha.Transport.InMemory.Tests.csproj | Adds Microsoft.Extensions.TimeProvider.Testing dependency for FakeTimeProvider. |
| src/Mocha/test/Mocha.Transport.InMemory.Tests/Helpers/MessageBusBuilder.cs | Starts hosted services in the in-memory transport test harness so scheduling worker runs. |
| src/Mocha/test/Mocha.Sagas.Tests/IntegrationTests.cs | Starts hosted services and adjusts timeout test flow to better synchronize with persistence/finalization. |
| src/Mocha/src/Mocha.Transport.InMemory/Scheduling/InMemoryScheduledMessageWorker.cs | New hosted worker that wakes on scheduler signal and dispatches due scheduled envelopes. |
| src/Mocha/src/Mocha.Transport.InMemory/Scheduling/InMemoryScheduledMessageStore.cs | New in-memory scheduled message store that deep-copies envelopes and supports cancellation. |
| src/Mocha/src/Mocha.Transport.InMemory/MessageBusBuilderExtensions.cs | Wires scheduling store/worker into AddInMemory() and enables scheduler core. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| while (!cancellationToken.IsCancellationRequested) | ||
| { | ||
| if (store.TryTakeDue(timeProvider.GetUtcNow(), out var envelope)) | ||
| { | ||
| await DispatchAsync(envelope, cancellationToken); | ||
| continue; | ||
| } | ||
|
|
||
| await signal.WaitUntilAsync(store.NextDueTime() ?? DateTimeOffset.MaxValue, cancellationToken); | ||
| } |
| public bool TryTakeDue(DateTimeOffset now, [NotNullWhen(true)] out MessageEnvelope? envelope) | ||
| { | ||
| lock (_lock) | ||
| { | ||
| var foundId = Guid.Empty; | ||
| var foundTime = DateTimeOffset.MaxValue; | ||
| MessageEnvelope? foundEnvelope = null; | ||
|
|
||
| foreach (var (id, entry) in _entries) | ||
| { | ||
| if (entry.ScheduledTime <= now && entry.ScheduledTime < foundTime) | ||
| { | ||
| foundId = id; | ||
| foundTime = entry.ScheduledTime; | ||
| foundEnvelope = entry.Envelope; | ||
| } | ||
| } | ||
|
|
||
| if (foundEnvelope is null) | ||
| { | ||
| envelope = null; | ||
| return false; | ||
| } | ||
|
|
||
| _entries.Remove(foundId); | ||
| envelope = foundEnvelope; | ||
| return true; | ||
| } |
| busBuilder.Services.TryAddSingleton<InMemoryScheduledMessageStore>(); | ||
| busBuilder.Services.TryAddSingleton<IScheduledMessageStore>( | ||
| sp => sp.GetRequiredService<InMemoryScheduledMessageStore>()); | ||
| busBuilder.Services.TryAddSingleton(sp => new InMemoryScheduledMessageWorker( | ||
| sp, | ||
| sp.GetRequiredService<IMessagingRuntime>(), | ||
| sp.GetRequiredService<IMessagingPools>(), | ||
| sp.GetRequiredService<ISchedulerSignal>(), | ||
| sp.GetRequiredService<InMemoryScheduledMessageStore>(), | ||
| sp.GetService<TimeProvider>() ?? TimeProvider.System, | ||
| sp.GetRequiredService<ILogger<InMemoryScheduledMessageWorker>>())); | ||
| busBuilder.Services.AddHostedService( | ||
| sp => sp.GetRequiredService<InMemoryScheduledMessageWorker>()); | ||
|
|
||
| busBuilder.UseSchedulerCore(); | ||
|
|
|
Closing in favor of #10119, which re-implements in-memory transport scheduling against the reworked scheduling subsystem introduced by #10036 ( That rework landed after this PR was opened and made this branch's approach obsolete: this PR wired a plain Drafted by Claude (Anthropic AI assistant). |
Summary
ScheduledTime(viaScheduleSendAsync/SchedulePublishAsync, a saga'sTimeout(), or delayed redelivery) is delivered at its scheduled time instead of immediately.IScheduledMessageStoreand a backgroundIHostedServiceworker, wired intoAddInMemorythrough Mocha's generic scheduling middleware (UseSchedulerCore). Cancellation is supported: a pending timeout is cancelled when a saga reaches a final state. Scheduled messages are held in process (non-durable); for durability useUsePostgresScheduling().Timeout()sagas are driven by real scheduling.Test plan
FakeTimeProvider): a scheduled message is delivered after its due time and not before, cancellation prevents delivery, and a scheduled message's body and headers are preserved until delivery.