Repository navigation
3.1: breaking API cleanup, batch handlers, pre-created Blob containers (#51) - #53
Merged
Merged
Conversation
…sages All three UseBlobStorageForLargeMessages overloads take an optional trailing `bool createContainerIfNotExists = true`, so an identity that can't create containers can use the short registration against a container created ahead of time instead of falling back to UseLargeMessageStore(new BlobLargeMessageStore(...)). Existing calls are unchanged. Docs and both READMEs updated; one Azurite test checks a missing container is not created and the send fails with 404. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erviceBuilder, camelCase JSON by default The four changes #51 held back for 4.0 go into 3.1 as clean breaks, since 3.0 shipped with negligible uptake: - IQueueSender.SendAsync returns SendResult and IStorageQueue.GetPropertiesAsync returns QueueStatus instead of the Azure SDK's SendReceipt and QueueProperties, so fakes no longer need QueuesModelFactory. Both transports convert at the edge. - QueueMessage<T>/IQueueMessage.RenewLockAsync is renamed RenewVisibilityAsync. - QueueClientSettingsBuilder is renamed QueueServiceBuilder. - The default serializer uses JsonSerializerDefaults.Web: camelCase out, case-insensitive in. Options passed by the application are used as given. README lists them as breaking changes from 3.0 with the migration for each; v3-design.md records why they moved into 3.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- IQueueBatchHandler<TMessage> and AddBatchProcessor<TMessage, THandler>() (plus an IConfiguration overload): up to MaxBatchSize (1-32, default 32) messages per handler call, MaxConcurrentBatches (default 1) batches at once, one scope per batch. - Every message keeps its own lease renewal (armed before any body is read), poison checks, error policy and settlement. Unreadable or over-delivered messages are dead-lettered individually and left out; return completes what the handler left unsettled; throw applies DeadLetterOn/OnHandlerError per unsettled message, else abandons each with RetryDelay for its own dequeue count. No middleware for batches. - The processor is split into QueueProcessorBase<TMessage> (lifecycle, receive loop, status, per-message steps) and two deliveries: one message, or one batch. - Options shared by both moved to QueueProcessorOptionsBase. - Fix: a processor with a RateLimiter that granted every requested permit called SemaphoreSlim.Release(0), which throws, so every receive failed. - One span per batch linked to every sender; per-message metrics; status counts messages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Finishes the "Remaining for 3.1" list in #51. Merging releases nothing: 3.1.0 still needs a tag, which the owner does after validating.
What changes for users
Breaking changes from 3.0 (each a small edit)
3.0 has been out only briefly, so these were made now rather than held for 4.0. No aliases or
[Obsolete]forwarders.SendAsyncreturnsSendResult, not the SDK'sSendReceipt. RenameInsertionTime→InsertedOn,ExpirationTime→ExpiresOn,TimeNextVisible→NextVisibleOn. A fake sender returnsnew SendResult(...), so tests no longer needQueuesModelFactory.GetPropertiesAsyncreturnsQueueStatus, not the SDK'sQueueProperties.ApproximateMessagesCountandMetadatakeep their names. A fake returnsnew QueueStatus(count, metadata).RenewLockAsyncis nowRenewVisibilityAsync. Rename the call.QueueClientSettingsBuilderis nowQueueServiceBuilder. Only code that names the type (e.g. a helper takingAction<QueueClientSettingsBuilder>) changes.{"orderId":42}). It reads either case, so messages from 3.0 senders still arrive. Other readers of the queue that expect PascalCase (an Azure Function, a script) will see empty fields. To keep PascalCase, passUseJsonSerializer(new JsonSerializerOptions { PropertyNameCaseInsensitive = true }).New: batch handlers
For work that's cheaper in bulk (database inserts, bulk APIs), a handler can now take up to 32 messages per call:
Each message is still handled as its own message:
MaxDequeueCounttimes, it goes to the poison queue on its own and isn't passed to the handler. If nothing is left, the handler isn't called.DeadLetterOn, thenOnHandlerError) runs once for each message the handler hadn't settled. A message the policy doesn't handle is abandoned, with the retry delay for its own dequeue count.A batch is whatever one receive returns, so the processor never waits to fill one. Each batch gets one DI scope, one trace span (linked to every sender's trace) and one log scope. Metrics are recorded per message, and status counts messages, so dashboards read the same as for single-message processors. Shutdown behaves exactly as for single-message processors. Middleware does not run for batch processors, because it is built around one message. The README says so.
The options both processor types share moved to a new base class,
QueueProcessorOptionsBase. Code that sets options doesn't change. The batch side hasMaxBatchSize(1–32, default 32) andMaxConcurrentBatches(default 1) instead ofMaxConcurrentMessages.New: pre-created Blob containers
UseBlobStorageForLargeMessages(..., createContainerIfNotExists: false)lets an identity that can't create containers use the short registration with a container created ahead of time. Existing calls are unchanged.Fixed (never released)
The 3.1 receive loop (#52) stalled when a
RateLimitergranted every permit it asked for. It calledSemaphoreSlim.Release(0), which throws, so every receive counted as failed and backed off. This is fixed, and a regression test covers it.Under the hood
The single-message and batch processors share one base class: start/stop, the receive loop, pausing, status, and the per-message steps (lease renewal, reading the body, poison checks, error policy, settlement). Only "deliver one message" vs "deliver a batch" differs, so there is no second copy of the processor.
Tests
Locally against Azurite: 204 passed, 0 failed (core 166, Blobs 21, integration 10, health checks 7). This PR adds 4 batch tests (batching up to
MaxBatchSize; partial settlement then completion; a throwing handler gets the per-message policy and abandon; unreadable messages are left out and dead-lettered) and 1 rate-limiter regression test. All run on in-memory queues with a fake clock, so none wait on real time.dotnet build -warnaserroris clean on all three src projects (docs plus trim/AOT analyzers).🤖 Generated with Claude Code