Raised by CodeRabbit on #1334 (#1334 (comment)).
TaskScheduler.Admit decides the owner quota by counting _pendingEntries.Values under _admissionLock (e => e.Owner == owner, already on main before #1334). #1334 adds the per-connection typed-input burst check with the same pattern. ConcurrentDictionary.Values copies, so each admission is O(pending) in time and allocation while holding the lock; filling the default 10,000-slot global queue is quadratic and serialises concurrent admissions.
Fix
Keep two counters under _admissionLock:
- charged entries per owner
- outstanding typed lines per connection incarnation (keyed the way
SameIncarnation compares: handle + Metadata instance)
Increment on admit, decrement where RemoveEntry releases a reservation (and in Halt/wipe paths that drop entries). Use the counts for both limit checks; do not enumerate _pendingEntries.
Acceptance
- No
_pendingEntries.Values enumeration on the admission path.
- Existing
QueueAdmissionTests / QueueQuotaTests pass unchanged.
- A test proves the counts return to zero after entries complete, are halted, and are cancelled.
Raised by CodeRabbit on #1334 (#1334 (comment)).
TaskScheduler.Admitdecides the owner quota by counting_pendingEntries.Valuesunder_admissionLock(e => e.Owner == owner, already onmainbefore #1334). #1334 adds the per-connection typed-input burst check with the same pattern.ConcurrentDictionary.Valuescopies, so each admission is O(pending) in time and allocation while holding the lock; filling the default 10,000-slot global queue is quadratic and serialises concurrent admissions.Fix
Keep two counters under
_admissionLock:SameIncarnationcompares: handle +Metadatainstance)Increment on admit, decrement where
RemoveEntryreleases a reservation (and inHalt/wipe paths that drop entries). Use the counts for both limit checks; do not enumerate_pendingEntries.Acceptance
_pendingEntries.Valuesenumeration on the admission path.QueueAdmissionTests/QueueQuotaTestspass unchanged.