@@ -5163,7 +5163,9 @@ return __qmret(results)
51635163 // :ckVtime / :ckVtimeFloor keys hold virtual times; ckIndex and the master
51645164 // queue keep their timestamp score domain. The per-candidate serve body is a
51655165 // verbatim copy of dequeueMessagesFromCkQueueTracked's, with the marked NEW
5166- // lines added (tag advance on serve, ZREM ckVtime on GC, floor advance from pass 1).
5166+ // lines added (tag advance on serve, ZREM ckVtime on GC, floor advance from pass 1,
5167+ // and the notReady report that lets pass 1 step over a future-headed variant without
5168+ // spending a window slot on it).
51675169 // Pass 2 always runs: when the batch is already full it registers the variants
51685170 // pass 1 could not see rather than serving them, which is what keeps a backlog
51695171 // queued before the flag went on from being unreachable.
@@ -5222,6 +5224,10 @@ if actualMaxCount <= 0 then
52225224end
52235225
52245226local window = actualMaxCount * windowMultiplier
5227+ -- Pass 1 reads further than it will spend, so a variant whose head is scheduled in the
5228+ -- future can be passed over without costing a window slot. Capped rather than unbounded:
5229+ -- a block wider than this still degrades to pass 2's age order, which is safe.
5230+ local scanLimit = window * 2
52255231
52265232-- Floor only ever rises, by two independent routes: to the lowest tag on record (repairs
52275233-- a floor that was lost while ckVtime survived), and to the lowest tag actually servable
@@ -5293,6 +5299,10 @@ local function tryServe(ckQueueName, mayRaiseFloor)
52935299 -- Pass 1 only: it walks in ascending tag order, so anything it has not visited
52945300 -- sits above this. Pass 2 goes by message age, so its tag says nothing about the
52955301 -- entries it skipped and must not move the floor over them.
5302+ -- Pass 1 now steps over future-headed variants below this tag, so the floor can
5303+ -- rise past one of them. That is the same forfeiture a variant at its concurrency
5304+ -- ceiling already takes: it keeps its entry, loses the sub-floor credit, and is
5305+ -- clamped up to the floor when it next becomes servable.
52965306 if mayRaiseFloor and (minServableTag == nil or tag < minServableTag) then
52975307 minServableTag = tag
52985308 end
@@ -5318,24 +5328,37 @@ local function tryServe(ckQueueName, mayRaiseFloor)
53185328 redis.call('ZREM', ckVtimeKey, ckQueueName) -- NEW
53195329 else
53205330 redis.call('ZADD', ckIndexKey, any[2], ckQueueName)
5331+ -- NEW: backlog, but the head is scheduled later, so nothing here is servable this
5332+ -- call. The readiness is already known from the ZRANGEBYSCORE above, so reporting
5333+ -- it costs nothing and lets pass 1 decline to spend a window slot on it.
5334+ return 'notReady'
53215335 end
53225336 end
53235337 end
53245338end
53255339
53265340-- Pass 1: fair order (lowest virtual start tag first)
5327- local vtimeCandidates = redis.call('ZRANGE', ckVtimeKey, 0, window - 1)
5328- -- NEW: the window read doubles as a free membership set for pass 2's discovery
5329- -- step. It is complete whenever ckVtime holds no more than window variants,
5341+ local vtimeCandidates = redis.call('ZRANGE', ckVtimeKey, 0, scanLimit - 1)
5342+ -- NEW: the scan read doubles as a free membership set for pass 2's discovery
5343+ -- step. It is complete whenever ckVtime holds no more than scanLimit variants,
53305344-- which is the common case; when it is truncated the discovery ZADD is NX so the
53315345-- variants it cannot rule out cost correctness nothing.
53325346local registered = {}
53335347for _, ckQueueName in ipairs(vtimeCandidates) do
53345348 registered[ckQueueName] = true
53355349end
5350+ -- NEW: a variant whose head is scheduled in the future stays registered and stays
5351+ -- scanned, it just does not spend one of the window's slots. Without this a retry storm
5352+ -- across enough keys fills the window with variants that cannot be served, pass 1 serves
5353+ -- nothing, and because minServableTag is the only route that can lift the floor over a
5354+ -- stale low tag, the floor freezes for as long as the storm lasts. Every other outcome
5355+ -- (served, gated on concurrency, drained, reaped) still spends a slot, as before.
5356+ local windowBudget = window
53365357for _, ckQueueName in ipairs(vtimeCandidates) do
5337- if dequeuedCount >= actualMaxCount then break end
5338- tryServe(ckQueueName, true)
5358+ if dequeuedCount >= actualMaxCount or windowBudget <= 0 then break end
5359+ if tryServe(ckQueueName, true) ~= 'notReady' then
5360+ windowBudget = windowBudget - 1
5361+ end
53395362end
53405363
53415364-- Pass 2: fill + discovery in age order (work conservation, mixed-deploy safety).
@@ -5369,13 +5392,21 @@ if discovered ~= nil then
53695392 redis.call('ZADD', unpack(discovered))
53705393end
53715394
5372- -- NEW: persist floor and refresh TTLs
5373- if minServableTag ~= nil and minServableTag > floor then
5374- floor = minServableTag
5375- end
5376- redis.call('SET', ckVtimeFloorKey, tostring(floor), 'EX', stateTtl)
5377- if redis.call('EXISTS', ckVtimeKey) == 1 then
5378- redis.call('EXPIRE', ckVtimeKey, stateTtl)
5395+ -- NEW: persist floor and refresh TTLs. A call that served nothing writes nothing: the two
5396+ -- things this block would persist are both re-derivable, since minServableTag is only set
5397+ -- inside a successful serve and pass 2's discovery only runs once the batch is full, so
5398+ -- the only floor movement on a zero-serve call is the min-tag read-repair, which is
5399+ -- recomputed from ckVtime at the top of every call anyway. Idle polling a queue whose work
5400+ -- is all future-scheduled or concurrency-gated therefore costs no writes, matching the old
5401+ -- command's early return.
5402+ if dequeuedCount > 0 then
5403+ if minServableTag ~= nil and minServableTag > floor then
5404+ floor = minServableTag
5405+ end
5406+ redis.call('SET', ckVtimeFloorKey, tostring(floor), 'EX', stateTtl)
5407+ if redis.call('EXISTS', ckVtimeKey) == 1 then
5408+ redis.call('EXPIRE', ckVtimeKey, stateTtl)
5409+ end
53795410end
53805411
53815412-- Rebalance master queue (ckIndex keeps its timestamp domain)
0 commit comments