Skip to content

fix(dispatcher): atomic select-and-reserve in LeastLoadedDispatcher - #20

Merged
adnanhd merged 1 commit into
mainfrom
fix/least-loaded-atomic
Jun 24, 2026
Merged

fix(dispatcher): atomic select-and-reserve in LeastLoadedDispatcher#20
adnanhd merged 1 commit into
mainfrom
fix/least-loaded-atomic

Conversation

@adnanhd

@adnanhd adnanhd commented Jun 24, 2026

Copy link
Copy Markdown
Owner

LeastLoadedDispatcher read load(), picked the least-loaded subscriber, then called it — a TOCTOU race where two concurrent fire()s could both pick the same capacity-1 node (the same double-booking bug fixed in torchestrator's pools, but on the fire-and-forget Eventful path).

Now selection reserves the pick atomically under a lock via Node.try_acquire, holds the slot for the call, and releases after — invoking the node's handler directly so the reservation isn't double-counted by Node.__call__. Subscribers that only implement load()/__call__ (not Nodes) keep the previous best-effort routing, so existing usage and tests are unchanged.

New test: a capacity-1 Node under concurrent dispatch is reserved for the call (second dispatch sees it saturated), never double-runs, and releases after. Full suite 187 green; lint/format/mypy clean.

The dispatcher read load(), picked the least-loaded subscriber, then called
it -- a TOCTOU race where two concurrent fires could both land on the same
capacity-1 node. Now it reserves the pick under a lock via Node.try_acquire
(holding the slot for the call, releasing after), invoking the node's handler
directly so it isn't double-counted. Plain load()-only subscribers keep the
prior best-effort behaviour, so existing usage is unchanged.
@adnanhd
adnanhd merged commit ae30089 into main Jun 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant