Skip to content

[Mocha] Cancel message handlers and release interrupted messages on shutdown - #10504

Merged
tobias-tengler merged 10 commits into
mainfrom
mocha/shutdown-handler-cancellation
Oct 13, 2026
Merged

tobias-tengler merged 10 commits into
mainfrom
mocha/shutdown-handler-cancellation

Conversation

@tobias-tengler

@tobias-tengler tobias-tengler commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

When a receive endpoint stopped, in-flight handlers were never cancelled because ReceiveContext discarded the transport's cancellation token, so they kept running after shutdown began. Stopping also left PostgreSQL messages behind. A handler that finished after the stop began could not delete its message, because the delete used the already cancelled token, and messages that were leased but not finished stayed locked until the 5 minute lease timeout. Both were then redelivered and processed a second time. Finally, a transport or endpoint that failed to stop kept the remaining ones from stopping.

This PR passes the endpoint's cancellation token to handlers, including batch handlers, and keeps shutdown cancellation from triggering retries, fault handling or error logs. RabbitMQ and Azure Service Bus settle messages with CancellationToken.None, so an interrupted message is still nacked or abandoned. Batches are collected and processed per receive endpoint, so stopping one endpoint only cancels its own batches, and MaxConcurrentBatches now applies per endpoint.

The PostgreSQL endpoint deletes or releases a handled message with CancellationToken.None. A delete that fails after the handler completed is logged and the message is released with the error recorded. When polling is cancelled, the endpoint releases the messages of the current batch that it still holds and undoes their delivery count bump, so they are redelivered right away instead of after the lease timeout, and the interrupted attempt does not count toward max delivery.

MessagingRuntime.StopAsync and MessagingTransport.StopAsync keep stopping the remaining transports and endpoints when one fails, then rethrow the first failure. A transport that failed to stop stays started, so disposing the runtime tries it again. Disposal runs every remaining step when one fails and rethrows the first failure once everything is released, except a stop failure that an earlier stop already reported.

@tobias-tengler
tobias-tengler marked this pull request as draft October 7, 2026 20:00
@tobias-tengler
tobias-tengler force-pushed the mocha/shutdown-handler-cancellation branch 2 times, most recently from 171c463 to 2fa5296 Compare October 7, 2026 20:06
@tobias-tengler
tobias-tengler force-pushed the mocha/shutdown-minimal branch from 750e258 to 6167eff Compare October 7, 2026 20:06
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Patch coverage

85.1% of changed lines covered (240/282)

File Covered Changed Patch %
…/Middlewares/Receive/RabbitMQAcknowledgementMiddleware.cs 1 2 50.0% 🔴
src/Mocha/src/Mocha/Runtime/MessagingRuntime.cs 21 32 65.6% 🔴
src/Mocha/src/Mocha/Transport/MessagingTransport.cs 16 23 69.6% 🔴
…/src/Mocha.Transport.Postgres/PostgresReceiveEndpoint.cs 46 57 80.7% 🟡
…/Mocha/src/Mocha/Consumers/Implementations/BatchConsumer.cs 64 75 85.3% 🟡
src/Mocha/src/Mocha/Consumers/Batching/BatchCollector.cs 15 16 93.8% 🟡
…/AzureServiceBusReceiveEndpoint.cs 3 3 100.0% 🟢
…/Receive/AzureServiceBusAcknowledgementMiddleware.cs 2 2 100.0% 🟢
…/src/Mocha.Transport.InMemory/InMemoryReceiveEndpoint.cs 3 3 100.0% 🟢
…/Mocha.Transport.Postgres/Connection/PostgresMessageStore.cs 25 25 100.0% 🟢
…/Connection/RabbitMQConsumerManager.cs 1 1 100.0% 🟢
…/src/Mocha.Transport.RabbitMQ/RabbitMQReceiveEndpoint.cs 17 17 100.0% 🟢
…/Mocha/src/Mocha/Consumers/Batching/MessageBatchExtensions.cs 18 18 100.0% 🟢
src/Mocha/src/Mocha/Endpoints/ReceiveEndpoint.cs 2 2 100.0% 🟢
…/Mocha/src/Mocha/Middlewares/Consume/Retry/RetryExecutor.cs 2 2 100.0% 🟢
…/src/Mocha/Middlewares/Receive/ReceiveDeadLetterMiddleware.cs 1 1 100.0% 🟢
…/src/Mocha/Middlewares/Receive/ReceiveFaultMiddleware.cs 1 1 100.0% 🟢
…/Receive/Redelivery/ReceiveRedeliveryMiddleware.cs 1 1 100.0% 🟢
src/Mocha/src/Mocha/Middlewares/ReceiveContext.cs 1 1 100.0% 🟢
Uncovered changed lines (JSON)
{
  "sha": "6063f29b49b7fcc28de6e120be3387cce4dfab91",
  "files": [
    { "path": "src/Mocha/src/Mocha.Transport.RabbitMQ/Middlewares/Receive/RabbitMQAcknowledgementMiddleware.cs", "ranges": [[38, 38]] },
    { "path": "src/Mocha/src/Mocha/Runtime/MessagingRuntime.cs", "ranges": [[202, 208], [228, 231]] },
    { "path": "src/Mocha/src/Mocha/Transport/MessagingTransport.cs", "ranges": [[251, 254], [272, 274]] },
    { "path": "src/Mocha/src/Mocha.Transport.Postgres/PostgresReceiveEndpoint.cs", "ranges": [[209, 212], [240, 243], [261, 263]] },
    { "path": "src/Mocha/src/Mocha/Consumers/Implementations/BatchConsumer.cs", "ranges": [[91, 93], [240, 241], [244, 248], [250, 250]] },
    { "path": "src/Mocha/src/Mocha/Consumers/Batching/BatchCollector.cs", "ranges": [[85, 85]] }
  ]
}

Project coverage: 58.7% (315704/538143 lines)

@tobias-tengler
tobias-tengler force-pushed the mocha/shutdown-handler-cancellation branch from 2fa5296 to b84e810 Compare October 8, 2026 07:05
@tobias-tengler
tobias-tengler force-pushed the mocha/shutdown-minimal branch 2 times, most recently from 8bc09cf to 7bb10f2 Compare October 8, 2026 07:08
@tobias-tengler
tobias-tengler force-pushed the mocha/shutdown-handler-cancellation branch 3 times, most recently from f616eee to 57f2418 Compare October 8, 2026 08:39
@tobias-tengler tobias-tengler changed the title [Mocha] Cancel message handlers on shutdown [Mocha] Cancel message handlers and release interrupted messages on shutdown Oct 8, 2026
@tobias-tengler
tobias-tengler force-pushed the mocha/shutdown-handler-cancellation branch 2 times, most recently from 7a16145 to 85ffdaf Compare October 8, 2026 13:30
@tobias-tengler
tobias-tengler force-pushed the mocha/shutdown-minimal branch from aae6435 to 13d6f71 Compare October 8, 2026 13:30
@tobias-tengler
tobias-tengler force-pushed the mocha/shutdown-handler-cancellation branch 2 times, most recently from 29d485e to 7a28857 Compare October 12, 2026 14:48
@tobias-tengler
tobias-tengler marked this pull request as ready for review October 12, 2026 16:49
Copilot AI balanced review requested due to automatic review settings October 12, 2026 16:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

PostgreSQL lease release retains last_delivered, preventing the promised immediate redelivery.

1 open finding
What changed in this PR

This PR improves Mocha shutdown behavior by canceling in-flight handlers, safely settling interrupted messages, and making lifecycle cleanup resilient to individual failures.

Changes:

  • Propagates shutdown cancellation through individual and batch handlers.
  • Releases or requeues interrupted transport messages.
  • Continues stopping and disposing remaining resources after failures.
File Description
src/​Mocha/​test/​Mocha.Transport.RabbitMQ.Tests/​Behaviors/​ShutdownTests.cs Tests cancellation and requeue on shutdown.
src/​Mocha/​test/​Mocha.Transport.Postgres.Tests/​Behaviors/​ShutdownTests.cs Tests PostgreSQL shutdown settlement.
src/​Mocha/​test/​Mocha.Transport.AzureServiceBus.Tests/​ReceiveEndpointLifecycleUnitTests.cs Tests endpoint failures and cancellation.
src/​Mocha/​test/​Mocha.Transport.AzureServiceBus.Tests/​Helpers/​FakeServiceBusClient.cs Adds message-processing test hooks.
src/​Mocha/​test/​Mocha.Transport.AzureServiceBus.Tests/​AzureServiceBusAcknowledgementMiddlewareTests.cs Verifies uncancelled settlement tokens.
src/​Mocha/​test/​Mocha.Tests/​Runtime/​MessagingRuntimeTests.cs Tests resilient runtime disposal.
src/​Mocha/​test/​Mocha.Tests/​Runtime/​MessagingRuntimeShutdownTests.cs Tests handler and batch cancellation.
src/​Mocha/​test/​Mocha.Tests/​Middlewares/​Consume/​Retry/​RetryExecutorTests.cs Tests cancellation during retries.
src/​Mocha/​test/​Mocha.Tests/​Consumers/​Batching/​BatchCollectorTests.cs Tests buffered-entry removal.
src/​Mocha/​src/​Mocha/​Transport/​MessagingTransport.cs Continues stopping endpoints after failures.
src/​Mocha/​src/​Mocha/​Runtime/​MessagingRuntime.cs Makes stop and disposal failure-resilient.
src/​Mocha/​src/​Mocha/​Middlewares/​ReceiveContext.cs Retains the receive cancellation token.
src/​Mocha/​src/​Mocha/​Middlewares/​Receive/​Redelivery/​ReceiveRedeliveryMiddleware.cs Skips redelivery handling during cancellation.
src/​Mocha/​src/​Mocha/​Middlewares/​Receive/​ReceiveFaultMiddleware.cs Skips fault generation during cancellation.
src/​Mocha/​src/​Mocha/​Middlewares/​Receive/​ReceiveDeadLetterMiddleware.cs Suppresses cancellation error logging.
src/​Mocha/​src/​Mocha/​Middlewares/​Consume/​Retry/​RetryExecutor.cs Stops retry processing when cancelled.
src/​Mocha/​src/​Mocha/​Endpoints/​ReceiveEndpoint.cs Propagates cancellation without critical logging.
src/​Mocha/​src/​Mocha/​Consumers/​Implementations/​BatchConsumer.cs Isolates batching per receive endpoint.
src/​Mocha/​src/​Mocha/​Consumers/​Batching/​BatchOptions.cs Documents per-endpoint concurrency.
src/​Mocha/​src/​Mocha/​Consumers/​Batching/​BatchCollector.cs Supports removing buffered entries.
src/​Mocha/​src/​Mocha.Transport.RabbitMQ/​RabbitMQReceiveEndpoint.cs Handles shutdown cancellation.
src/​Mocha/​src/​Mocha.Transport.RabbitMQ/​Middlewares/​Receive/​RabbitMQAcknowledgementMiddleware.cs Settles messages despite cancellation.
src/​Mocha/​src/​Mocha.Transport.RabbitMQ/​Connection/​RabbitMQConsumerManager.cs Cancels handlers before disconnecting.
src/​Mocha/​src/​Mocha.Transport.Postgres/​PostgresReceiveEndpoint.cs Deletes or releases interrupted messages.
src/​Mocha/​src/​Mocha.Transport.Postgres/​Connection/​PostgresMessageStore.cs Adds bulk lease release support.
src/​Mocha/​src/​Mocha.Transport.InMemory/​InMemoryReceiveEndpoint.cs Handles expected shutdown cancellation.
src/​Mocha/​src/​Mocha.Transport.AzureServiceBus/​Middlewares/​Receive/​AzureServiceBusAcknowledgementMiddleware.cs Settles messages independently of cancellation.
src/​Mocha/​src/​Mocha.Transport.AzureServiceBus/​AzureServiceBusReceiveEndpoint.cs Handles cancelled processor callbacks.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@tobias-tengler
tobias-tengler removed this pull request from stack #10505 October 13, 2026 06:41
Base automatically changed from mocha/shutdown-minimal to main October 13, 2026 06:41
… failures

Delete or release handled Postgres messages with a token that stop does not cancel, and release the rest of a batch when polling is cancelled, so in-flight messages are not redelivered after the lock timeout.

Keep stopping the remaining transports and endpoints when one fails, then rethrow the first failure.
The shutdown tests asserted that stopping produced no warning or error
logs through a LogRecorder test helper. They now assert only the handler
cancellation and the resulting database state, and the helper is removed.
The acknowledgement middlewares complete or ack with CancellationToken.None
and the Postgres endpoint leaves an interrupted message to the batch release.
The reasons were only in the review threads, so each site now states the
invariant: once the handler returns its work is committed and settlement runs
to completion, while a cancelled handler throws and is abandoned, nacked or
released instead.
The receive endpoint swallowed the exception of a handler cancelled by the
stopping processor. A message cancelled before the acknowledgement middleware
ran was never settled, so it stayed locked until the lock expired and gained a
delivery count. The SDK abandons an unsettled message only when the handler
throws.

The handler now lets the exception bubble. The processor error handler
returns without logging when the reported exception is a cancellation of the
processor's own token. Messages the pipeline already abandoned are unaffected
because the SDK skips its abandon for settled messages.
…very

ReleaseLeasedMessagesAsync reset the delivery count of an interrupted message
but kept last_delivered, so the read query's backoff still skipped the row for
up to 2^delivery_count seconds after a stop. The release now clears
last_delivered as well, so the row looks like one that was never delivered.
@tobias-tengler
tobias-tengler merged commit 48700d3 into main Oct 13, 2026
158 checks passed
@tobias-tengler
tobias-tengler deleted the mocha/shutdown-handler-cancellation branch October 13, 2026 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants