@@ -5105,7 +5105,9 @@ return __qmret(results)
51055105 // :ckVtime / :ckVtimeFloor keys hold virtual times; ckIndex and the master
51065106 // queue keep their timestamp score domain. The per-candidate serve body is a
51075107 // verbatim copy of dequeueMessagesFromCkQueueTracked's, with the marked NEW
5108- // lines added (tag advance on serve, ZREM ckVtime on GC, floor advance from pass 1).
5108+ // lines added (tag advance on serve, ZREM ckVtime on GC, floor advance from pass 1,
5109+ // and the notReady report that lets pass 1 step over a future-headed variant without
5110+ // spending a window slot on it).
51095111 // Pass 2 always runs: when the batch is already full it registers the variants
51105112 // pass 1 could not see rather than serving them, which is what keeps a backlog
51115113 // queued before the flag went on from being unreachable.
@@ -5164,6 +5166,10 @@ if actualMaxCount <= 0 then
51645166end
51655167
51665168local window = actualMaxCount * windowMultiplier
5169+ -- Pass 1 reads further than it will spend, so a variant whose head is scheduled in the
5170+ -- future can be passed over without costing a window slot. Capped rather than unbounded:
5171+ -- a block wider than this still degrades to pass 2's age order, which is safe.
5172+ local scanLimit = window * 2
51675173
51685174-- Floor only ever rises, by two independent routes: to the lowest tag on record (repairs
51695175-- a floor that was lost while ckVtime survived), and to the lowest tag actually servable
@@ -5235,6 +5241,10 @@ local function tryServe(ckQueueName, mayRaiseFloor)
52355241 -- Pass 1 only: it walks in ascending tag order, so anything it has not visited
52365242 -- sits above this. Pass 2 goes by message age, so its tag says nothing about the
52375243 -- entries it skipped and must not move the floor over them.
5244+ -- Pass 1 now steps over future-headed variants below this tag, so the floor can
5245+ -- rise past one of them. That is the same forfeiture a variant at its concurrency
5246+ -- ceiling already takes: it keeps its entry, loses the sub-floor credit, and is
5247+ -- clamped up to the floor when it next becomes servable.
52385248 if mayRaiseFloor and (minServableTag == nil or tag < minServableTag) then
52395249 minServableTag = tag
52405250 end
@@ -5260,24 +5270,37 @@ local function tryServe(ckQueueName, mayRaiseFloor)
52605270 redis.call('ZREM', ckVtimeKey, ckQueueName) -- NEW
52615271 else
52625272 redis.call('ZADD', ckIndexKey, any[2], ckQueueName)
5273+ -- NEW: backlog, but the head is scheduled later, so nothing here is servable this
5274+ -- call. The readiness is already known from the ZRANGEBYSCORE above, so reporting
5275+ -- it costs nothing and lets pass 1 decline to spend a window slot on it.
5276+ return 'notReady'
52635277 end
52645278 end
52655279 end
52665280end
52675281
52685282-- Pass 1: fair order (lowest virtual start tag first)
5269- local vtimeCandidates = redis.call('ZRANGE', ckVtimeKey, 0, window - 1)
5270- -- NEW: the window read doubles as a free membership set for pass 2's discovery
5271- -- step. It is complete whenever ckVtime holds no more than window variants,
5283+ local vtimeCandidates = redis.call('ZRANGE', ckVtimeKey, 0, scanLimit - 1)
5284+ -- NEW: the scan read doubles as a free membership set for pass 2's discovery
5285+ -- step. It is complete whenever ckVtime holds no more than scanLimit variants,
52725286-- which is the common case; when it is truncated the discovery ZADD is NX so the
52735287-- variants it cannot rule out cost correctness nothing.
52745288local registered = {}
52755289for _, ckQueueName in ipairs(vtimeCandidates) do
52765290 registered[ckQueueName] = true
52775291end
5292+ -- NEW: a variant whose head is scheduled in the future stays registered and stays
5293+ -- scanned, it just does not spend one of the window's slots. Without this a retry storm
5294+ -- across enough keys fills the window with variants that cannot be served, pass 1 serves
5295+ -- nothing, and because minServableTag is the only route that can lift the floor over a
5296+ -- stale low tag, the floor freezes for as long as the storm lasts. Every other outcome
5297+ -- (served, gated on concurrency, drained, reaped) still spends a slot, as before.
5298+ local windowBudget = window
52785299for _, ckQueueName in ipairs(vtimeCandidates) do
5279- if dequeuedCount >= actualMaxCount then break end
5280- tryServe(ckQueueName, true)
5300+ if dequeuedCount >= actualMaxCount or windowBudget <= 0 then break end
5301+ if tryServe(ckQueueName, true) ~= 'notReady' then
5302+ windowBudget = windowBudget - 1
5303+ end
52815304end
52825305
52835306-- Pass 2: fill + discovery in age order (work conservation, mixed-deploy safety).
@@ -5311,13 +5334,21 @@ if discovered ~= nil then
53115334 redis.call('ZADD', unpack(discovered))
53125335end
53135336
5314- -- NEW: persist floor and refresh TTLs
5315- if minServableTag ~= nil and minServableTag > floor then
5316- floor = minServableTag
5317- end
5318- redis.call('SET', ckVtimeFloorKey, tostring(floor), 'EX', stateTtl)
5319- if redis.call('EXISTS', ckVtimeKey) == 1 then
5320- redis.call('EXPIRE', ckVtimeKey, stateTtl)
5337+ -- NEW: persist floor and refresh TTLs. A call that served nothing writes nothing: the two
5338+ -- things this block would persist are both re-derivable, since minServableTag is only set
5339+ -- inside a successful serve and pass 2's discovery only runs once the batch is full, so
5340+ -- the only floor movement on a zero-serve call is the min-tag read-repair, which is
5341+ -- recomputed from ckVtime at the top of every call anyway. Idle polling a queue whose work
5342+ -- is all future-scheduled or concurrency-gated therefore costs no writes, matching the old
5343+ -- command's early return.
5344+ if dequeuedCount > 0 then
5345+ if minServableTag ~= nil and minServableTag > floor then
5346+ floor = minServableTag
5347+ end
5348+ redis.call('SET', ckVtimeFloorKey, tostring(floor), 'EX', stateTtl)
5349+ if redis.call('EXISTS', ckVtimeKey) == 1 then
5350+ redis.call('EXPIRE', ckVtimeKey, stateTtl)
5351+ end
53215352end
53225353
53235354-- Rebalance master queue (ckIndex keeps its timestamp domain)
0 commit comments