@@ -85,16 +85,24 @@ local function __gateKeys(gatesKeyPrefix, msg, gate)
8585 return base, variant, gateKey
8686end
8787
88- local function __gateReconcile(setKey, msgKeyPrefix)
88+ local function __gateReconcile(setKey, msgKeyPrefix, reconcileKeyPrefix )
8989 if not msgKeyPrefix then return end
9090 if redis.call('SET', setKey .. ':reconcileLock', '1', 'NX', 'EX', '10') then
9191 local cursorKey = setKey .. ':reconcileCursor'
9292 local cursor = redis.call('GET', cursorKey) or '0'
93- local scanResult = redis.call('SSCAN', setKey, cursor, 'COUNT', '500 ')
93+ local scanResult = redis.call('SSCAN', setKey, cursor, 'COUNT', '100 ')
9494 redis.call('SET', cursorKey, scanResult[1], 'EX', '3600')
9595 for _, memberId in ipairs(scanResult[2]) do
96- if redis.call('EXISTS', msgKeyPrefix .. memberId) == 0 then
96+ local rawMemberPayload = redis.call('GET', msgKeyPrefix .. memberId)
97+ if not rawMemberPayload then
9798 redis.call('SREM', setKey, memberId)
99+ elseif reconcileKeyPrefix then
100+ local okMember, member = pcall(cjson.decode, rawMemberPayload)
101+ if okMember and type(member) == 'table' and type(member.queue) == 'string' then
102+ if redis.call('ZSCORE', reconcileKeyPrefix .. member.queue, memberId) then
103+ redis.call('SREM', setKey, memberId)
104+ end
105+ end
98106 end
99107 end
100108 end
@@ -107,7 +115,7 @@ local function __gatesHaveCapacity(gatesKeyPrefix, msg, messageId, envLimit, msg
107115 local occupancy = tonumber(redis.call('SCARD', variant .. ':currentConcurrency') or '0')
108116 local perKeyLimit = math.min(tonumber(redis.call('GET', base .. ':concurrency') or '1000000'), envLimit)
109117 if occupancy >= perKeyLimit and redis.call('SISMEMBER', variant .. ':currentConcurrency', messageId) == 0 then
110- __gateReconcile(variant .. ':currentConcurrency', msgKeyPrefix)
118+ __gateReconcile(variant .. ':currentConcurrency', msgKeyPrefix, gatesKeyPrefix )
111119 return false
112120 end
113121 if gateKey and gateKey ~= '' then
@@ -116,7 +124,7 @@ local function __gatesHaveCapacity(gatesKeyPrefix, msg, messageId, envLimit, msg
116124 local totalLimit = math.min(tonumber(rawTotal), envLimit)
117125 local groupKey = base .. ':groupConcurrency'
118126 if tonumber(redis.call('SCARD', groupKey) or '0') >= totalLimit and redis.call('SISMEMBER', groupKey, messageId) == 0 then
119- __gateReconcile(groupKey, msgKeyPrefix)
127+ __gateReconcile(groupKey, msgKeyPrefix, gatesKeyPrefix )
120128 return false
121129 end
122130 end
@@ -4921,29 +4929,14 @@ if totalConcurrencyEnabled then
49214929 local totalConcurrencyLimit = math.min(tonumber(rawTotalLimit), envConcurrencyLimit)
49224930 local groupCurrentConcurrency = tonumber(redis.call('SCARD', groupConcurrencyKey) or '0')
49234931
4924- -- Self-heal before holding the queue at its limit. A terminal release path
4925- -- that misses the group mirror (an older build, or a future script) leaves
4926- -- a member behind, but every terminal path deletes the run's message key,
4927- -- so a member with no message key is provably dead. Members of re-queued
4928- -- runs keep their message key and clear through the mirrored ack when the
4929- -- run completes. The short lock bounds a saturated queue to one pass per
4930- -- interval, and SSCAN with a persisted cursor bounds each pass to one
4931- -- batch so a large set never blocks Redis for a full traversal; successive
4932- -- passes cover the whole set.
4932+ -- Self-heal before holding the queue at its limit: a member with no message
4933+ -- key was terminally released without the mirror and is dead, and a member
4934+ -- whose message is QUEUED (in its variant zset) holds no legitimate slot,
4935+ -- since queued and in-flight are mutually exclusive on every path. Both are
4936+ -- pruned by the shared bounded reconcile.
49334937 if groupCurrentConcurrency >= totalConcurrencyLimit then
4934- local reconcileLockKey = groupConcurrencyKey .. ':reconcileLock'
4935- if redis.call('SET', reconcileLockKey, '1', 'NX', 'EX', '10') then
4936- local reconcileCursorKey = groupConcurrencyKey .. ':reconcileCursor'
4937- local reconcileCursor = redis.call('GET', reconcileCursorKey) or '0'
4938- local scanResult = redis.call('SSCAN', groupConcurrencyKey, reconcileCursor, 'COUNT', '500')
4939- redis.call('SET', reconcileCursorKey, scanResult[1], 'EX', '3600')
4940- for _, groupMemberId in ipairs(scanResult[2]) do
4941- if redis.call('EXISTS', messageKeyPrefix .. groupMemberId) == 0 then
4942- redis.call('SREM', groupConcurrencyKey, groupMemberId)
4943- end
4944- end
4945- groupCurrentConcurrency = tonumber(redis.call('SCARD', groupConcurrencyKey) or '0')
4946- end
4938+ __gateReconcile(groupConcurrencyKey, messageKeyPrefix, keyPrefix)
4939+ groupCurrentConcurrency = tonumber(redis.call('SCARD', groupConcurrencyKey) or '0')
49474940 end
49484941
49494942 totalHeadroom = totalConcurrencyLimit - groupCurrentConcurrency
0 commit comments