Deliver queued notifications in the order they were received - #1162
Draft
dwcullop wants to merge 1 commit into
Draft
Deliver queued notifications in the order they were received#1162dwcullop wants to merge 1 commit into
dwcullop wants to merge 1 commit into
Conversation
SharedDeliveryQueue partitions pending notifications into a typed sub-queue per source, and the drain loop picked the next one with Bitset.FindHighest(). Selection was therefore by sub-queue index rather than by arrival, so notifications from two sources could be delivered in an order they were not received in. With receipt order A1, A2, Bx, delivery came out A1, Bx, A2. The sub-queues are still typed, so the payloads are still held as structs and queuing one still costs no allocation. What changes is that a second queue now records which source each pending notification came from, in arrival order, and the drain loop follows that instead of scanning a bitset. The entries are sub-queue references that already exist, so recording the order does not allocate either. Highest-index-first was there so a child sub-queue drained before a parent's delivery could dispose it. That is no longer needed. A disposed sub-queue drops its pending notifications, and its leftover order entries are skipped when the drain reaches them, which is the same outcome by a shorter route. This also removes Bitset, which had no other caller, along with the sub-queue index bookkeeping and the compaction pass that existed to keep the bitset dense. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9582bb33-26d3-4aa5-8dd7-57dc55304680
dwcullop
force-pushed
the
fix/delivery-queue-ordering
branch
from
August 6, 2026 02:09
273fb40 to
b07c03c
Compare
JakenVeina
approved these changes
Aug 6, 2026
JakenVeina
left a comment
Collaborator
There was a problem hiding this comment.
As far as the preservation of ordering goes, this looks right to me.
Feel free to ignore the bit about AggressiveInlining for now, as it's already littered throughout the rest of the class. But I figured I'd mention it.
| public DeliverySubQueue<T> CreateQueue<T>(IObserver<T> observer) => new(this, observer); | ||
|
|
||
| /// <summary>Acquires the gate for read-only inspection. Does not trigger delivery on dispose.</summary> | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] |
Collaborator
There was a problem hiding this comment.
Per official docs, don't use AggressiveInlining unless you can back it up with benchmarks to prove it's actually beneficial.
Adding AggressiveInlining is probably one of a variety of optimizations worth exploring, once the API is settled and proven in the field, especially since this whole class is basically a hot path. But I agree with the docs, it's premature. Plus, the runtime is QUITE good at applying optimizations like inlining on hot paths.
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.
SharedDeliveryQueuedoes not deliver in the order it received.Pending notifications are partitioned into a typed sub-queue per source, and
DrainPendingpicked the next one withBitset.FindHighest(). That selects by sub-queue index, not by arrival.Reachable through the operators, not just by driving the queue directly.
SynchronizeSafe(queue)creates a sub-queue per subscription, so inPagethe page requests and the data changes land in different sub-queues and both mutate the samePaginator.Change
_orderis aQueue<DrainableBase>holding one entry per enqueued notification, recording which sub-queue it came from. The payloads stay where they are:That part matters. The obvious alternative is one queue of type-erased notifications, but
Notification<T>is a readonly struct, so queuing one allocates nothing today. Recording only the order keeps it that way.Selecting by arrival leaves
Bitset,FindHighest,SetActive,NotifyQueueRemoved,CompactIfNeeded,_sourcesand the per-sub-queueIndexall dead.Bitsethad no other caller.Highest-index-first was deliberate
Newer sub-queues are children of older ones, and a child had to drain before a parent's delivery could dispose it. Arrival ordering drops that guarantee, so a disposed sub-queue's stale order entries get skipped when the drain reaches them instead. Third test covers it.
Tests
Two of the three fail on main:
The bug only bites when the queue holds more than one item, which needs cross-thread contention. Single threaded the reentrant path drains inline, so nothing is queued to reorder. That is why the suite passes without this.
Related
#1163 moves change batching out of the queue. Independent, but that batching was masking this one: everything in a drain cycle got folded into a single changeset before anything downstream saw the order. Same file, so whichever lands second needs a trivial merge.