Serve multiple queues fairly instead of by command-line order - #57
Merged
Merged
Conversation
`acquire.lua` walked the queue key list in the order the names were passed
and returned on the first non-empty queue, so `--queues a b` was strict
precedence: while `a` had a single ready task, nothing from `b` ran. A worker
serving a latency-sensitive queue next to a bulk one had no correct order,
only a choice of which side to starve.
The scan now starts at a rotating index and wraps:
local queue_index = (start_index + offset) % num_queues + 1
`RedisTaskBackend` keeps the counter and advances it only after a successful
acquire, so idle polls keep their backoff and do not disturb the rotation. A
ready task in another queue is therefore served after at most `num_queues - 1`
acquisitions of the winning queue.
Two details worth knowing:
- The counter is seeded with `random.randrange(len(self.queues))`. It is
per-process state, so without a seed every recycled worker (small
`--max-tasks`) would resume at queue 0 and starve the later queues forever.
- The wrap on the success path is the only modulo needed; the pop and the move
into the running set stay in the same script call, so atomicity is unchanged.
With a single queue the rotation is a no-op and behaviour is unchanged.
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.
--queues a bwas strict precedence, not a set of queues.acquire.luawalked the key list in the order the names were passed and returned on the first non-empty queue, so whileaheld a single ready task nothing frombran. A worker serving a latency-sensitive queue next to a bulk one had no correct order, only a choice of which side to starve.Approach
The scan now starts at a rotating index and wraps:
RedisTaskBackendowns the counter and advances it only after a successful acquire, so idle polls keep their backoff and do not disturb the rotation. The pop and the move into the running set stay in the same script call, so atomicity is unchanged.The result is a hard bound rather than a probabilistic one: a ready task in another queue is served after at most
num_queues - 1acquisitions of the winning queue, regardless of how deep that queue's backlog is.Two things worth knowing
The counter is seeded randomly. It is per-process state, which alone is not enough: a recycled worker builds a fresh backend, so every incarnation would resume at queue 0 and starve the later queues permanently. This is reachable in practice with
--max-tasksbelow the starved queue's position - reproduced end-to-end at--queues compute io --max-tasks 1, where 20 recycled processes poppedcompute20 times and the singleiotask never ran.random.randrange(len(self.queues))removes the systematic skew; within a process the rotation stays deterministic.Only one modulo is needed. The counter is wrapped on the success path (
(self._rotation_offset + 1) % len(queue_names)), which keeps it inside the queue range and out of float-precision trouble in Lua, so the value is sent raw andacquire.luarebases it against the queue count it is given.Behaviour changes
--queuesno longer implies precedence.QUEUESlist now fails at construction (randrange(0)) instead of at the first acquire (ZeroDivisionError). Nothing shipped reaches it:--queuesisnargs="+"and is validated againstbackend.queues.Tests
Real Redis, no mocks, matching the existing suite: fairness against a fed backlog, rotation order with each task landing in the running set of the queue it came from, the counter staying bounded across acquires, idle polls not advancing it, and the seed varying across constructions. The fairness test fails under the pre-diff script. 201 passed,
prekclean, patch coverage 100%.Two follow-ups from the same read of
acquire.luaare filed separately rather than folded in: batch acquire (#49) and the silent task loss when a task hash is missing (#50).Fixes: #48