glide-mq 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/flow-producer.d.ts.map +1 -1
- package/dist/flow-producer.js +17 -1
- package/dist/flow-producer.js.map +1 -1
- package/dist/functions/index.d.ts +15 -5
- package/dist/functions/index.d.ts.map +1 -1
- package/dist/functions/index.js +619 -28
- package/dist/functions/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/job.d.ts +1 -0
- package/dist/job.d.ts.map +1 -1
- package/dist/job.js +2 -0
- package/dist/job.js.map +1 -1
- package/dist/queue.d.ts +17 -1
- package/dist/queue.d.ts.map +1 -1
- package/dist/queue.js +105 -6
- package/dist/queue.js.map +1 -1
- package/dist/scheduler.d.ts +10 -0
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +11 -0
- package/dist/scheduler.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +1 -0
- package/dist/utils.js.map +1 -1
- package/dist/worker.d.ts +5 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +48 -7
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@ worker.on('failed', (job, err) => console.error(`Job ${job.id} failed:`, err.mes
|
|
|
75
75
|
## Documentation
|
|
76
76
|
|
|
77
77
|
- **[docs/USAGE.md](docs/USAGE.md)** — Queue & Worker basics, graceful shutdown, cluster mode, event listeners
|
|
78
|
-
- **[docs/ADVANCED.md](docs/ADVANCED.md)** — Schedulers, ordering, deduplication, global concurrency, revocation, compression, retries & DLQ
|
|
78
|
+
- **[docs/ADVANCED.md](docs/ADVANCED.md)** — Schedulers, ordering, token bucket, deduplication, global concurrency, revocation, compression, retries & DLQ
|
|
79
79
|
- **[docs/WORKFLOWS.md](docs/WORKFLOWS.md)** — FlowProducer, `chain`, `group`, `chord` pipelines
|
|
80
80
|
- **[docs/OBSERVABILITY.md](docs/OBSERVABILITY.md)** — Job logs, metrics, OpenTelemetry, `@glidemq/dashboard`
|
|
81
81
|
- **[docs/TESTING.md](docs/TESTING.md)** — `TestQueue` & `TestWorker`, in-memory testing without Valkey
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow-producer.d.ts","sourceRoot":"","sources":["../src/flow-producer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAU,MAAM,SAAS,CAAC;AACpE,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAM5B,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAsB;IAClC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,OAAO,CAAS;gBAEZ,IAAI,EAAE,mBAAmB;IAIrC,gBAAgB;YACF,SAAS;IAYvB;;;;OAIG;IACG,GAAG,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAe1C;;OAEG;IACG,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IASnD;;;;OAIG;YACW,gBAAgB;
|
|
1
|
+
{"version":3,"file":"flow-producer.d.ts","sourceRoot":"","sources":["../src/flow-producer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAU,MAAM,SAAS,CAAC;AACpE,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAM5B,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,GAAG,CAAC;IACT,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;CACtB;AAED,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAsB;IAClC,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,OAAO,CAAS;gBAEZ,IAAI,EAAE,mBAAmB;IAIrC,gBAAgB;YACF,SAAS;IAYvB;;;;OAIG;IACG,GAAG,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAe1C;;OAEG;IACG,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IASnD;;;;OAIG;YACW,gBAAgB;IAgK9B;;;OAGG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAQ7B"}
|
package/dist/flow-producer.js
CHANGED
|
@@ -94,7 +94,23 @@ class FlowProducer {
|
|
|
94
94
|
const { addJob } = await Promise.resolve().then(() => __importStar(require('./functions/index')));
|
|
95
95
|
const timestamp = Date.now();
|
|
96
96
|
const opts = flow.opts ?? {};
|
|
97
|
-
const
|
|
97
|
+
const groupRateMax = opts.ordering?.rateLimit?.max ?? 0;
|
|
98
|
+
const groupRateDuration = opts.ordering?.rateLimit?.duration ?? 0;
|
|
99
|
+
const tbCapacity = opts.ordering?.tokenBucket
|
|
100
|
+
? Math.round(opts.ordering.tokenBucket.capacity * 1000) : 0;
|
|
101
|
+
const tbRefillRate = opts.ordering?.tokenBucket
|
|
102
|
+
? Math.round(opts.ordering.tokenBucket.refillRate * 1000) : 0;
|
|
103
|
+
const jobCost = opts.cost != null
|
|
104
|
+
? Math.round(opts.cost * 1000) : 0;
|
|
105
|
+
let groupConcurrency = opts.ordering?.concurrency ?? 0;
|
|
106
|
+
// Force group path when rate limit or token bucket is set
|
|
107
|
+
if ((groupRateMax > 0 || tbCapacity > 0) && groupConcurrency < 1) {
|
|
108
|
+
groupConcurrency = 1;
|
|
109
|
+
}
|
|
110
|
+
const jobId = await addJob(client, parentKeys, flow.name, JSON.stringify(flow.data), JSON.stringify(opts), timestamp, opts.delay ?? 0, opts.priority ?? 0, '', opts.attempts ?? 0, opts.ordering?.key ?? '', groupConcurrency, groupRateMax, groupRateDuration, tbCapacity, tbRefillRate, jobCost);
|
|
111
|
+
if (String(jobId) === 'ERR:COST_EXCEEDS_CAPACITY') {
|
|
112
|
+
throw new Error('Job cost exceeds token bucket capacity');
|
|
113
|
+
}
|
|
98
114
|
const job = new job_1.Job(client, parentKeys, String(jobId), flow.name, flow.data, opts);
|
|
99
115
|
job.timestamp = timestamp;
|
|
100
116
|
return { job };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flow-producer.js","sourceRoot":"","sources":["../src/flow-producer.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"flow-producer.js","sourceRoot":"","sources":["../src/flow-producer.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import type { Client } from '../types';
|
|
2
2
|
import type { GlideReturnType } from '@glidemq/speedkey';
|
|
3
3
|
export declare const LIBRARY_NAME = "glidemq";
|
|
4
|
-
export declare const LIBRARY_VERSION = "
|
|
4
|
+
export declare const LIBRARY_VERSION = "19";
|
|
5
5
|
export declare const CONSUMER_GROUP = "workers";
|
|
6
|
-
export declare const LIBRARY_SOURCE = "#!lua name=glidemq\n\nlocal PRIORITY_SHIFT = 4398046511104\n\nlocal function emitEvent(eventsKey, eventType, jobId, extraFields)\n local fields = {'event', eventType, 'jobId', tostring(jobId)}\n if extraFields then\n for i = 1, #extraFields, 2 do\n fields[#fields + 1] = extraFields[i]\n fields[#fields + 1] = extraFields[i + 1]\n end\n end\n redis.call('XADD', eventsKey, 'MAXLEN', '~', '1000', '*', unpack(fields))\nend\n\nlocal function markOrderingDone(jobKey, jobId)\n local orderingKey = redis.call('HGET', jobKey, 'orderingKey')\n if not orderingKey or orderingKey == '' then\n return\n end\n local orderingSeq = tonumber(redis.call('HGET', jobKey, 'orderingSeq')) or 0\n if orderingSeq <= 0 then\n return\n end\n\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n local metaKey = prefix .. 'meta'\n local doneField = 'orderdone:' .. orderingKey\n local pendingKey = prefix .. 'orderdone:pending:' .. orderingKey\n\n local lastDone = tonumber(redis.call('HGET', metaKey, doneField)) or 0\n if orderingSeq <= lastDone then\n redis.call('HDEL', pendingKey, tostring(orderingSeq))\n return\n end\n\n redis.call('HSET', pendingKey, tostring(orderingSeq), '1')\n local advanced = lastDone\n while true do\n local nextSeq = advanced + 1\n if redis.call('HEXISTS', pendingKey, tostring(nextSeq)) == 0 then\n break\n end\n redis.call('HDEL', pendingKey, tostring(nextSeq))\n advanced = nextSeq\n end\n if advanced > lastDone then\n redis.call('HSET', metaKey, doneField, tostring(advanced))\n end\nend\n\nlocal function releaseGroupSlotAndPromote(jobKey, jobId)\n local gk = redis.call('HGET', jobKey, 'groupKey')\n if not gk or gk == '' then return end\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n local groupHashKey = prefix .. 'group:' .. gk\n local cur = tonumber(redis.call('HGET', groupHashKey, 'active')) or 0\n if cur > 0 then\n redis.call('HSET', groupHashKey, 'active', tostring(cur - 1))\n end\n local waitListKey = prefix .. 'groupq:' .. gk\n local nextJobId = redis.call('LPOP', waitListKey)\n if nextJobId then\n local streamKey = prefix .. 'stream'\n redis.call('XADD', streamKey, '*', 'jobId', nextJobId)\n local nextJobKey = prefix .. 'job:' .. nextJobId\n redis.call('HSET', nextJobKey, 'state', 'waiting')\n end\nend\n\nlocal function extractOrderingKeyFromOpts(optsJson)\n if not optsJson or optsJson == '' then\n return ''\n end\n local ok, decoded = pcall(cjson.decode, optsJson)\n if not ok or type(decoded) ~= 'table' then\n return ''\n end\n local ordering = decoded['ordering']\n if type(ordering) ~= 'table' then\n return ''\n end\n local key = ordering['key']\n if key == nil then\n return ''\n end\n return tostring(key)\nend\n\nlocal function extractGroupConcurrencyFromOpts(optsJson)\n if not optsJson or optsJson == '' then\n return 0\n end\n local ok, decoded = pcall(cjson.decode, optsJson)\n if not ok or type(decoded) ~= 'table' then\n return 0\n end\n local ordering = decoded['ordering']\n if type(ordering) ~= 'table' then\n return 0\n end\n local conc = ordering['concurrency']\n if conc == nil then\n return 0\n end\n return tonumber(conc) or 0\nend\n\nredis.register_function('glidemq_version', function(keys, args)\n return '16'\nend)\n\nredis.register_function('glidemq_addJob', function(keys, args)\n local idKey = keys[1]\n local streamKey = keys[2]\n local scheduledKey = keys[3]\n local eventsKey = keys[4]\n local jobName = args[1]\n local jobData = args[2]\n local jobOpts = args[3]\n local timestamp = tonumber(args[4])\n local delay = tonumber(args[5]) or 0\n local priority = tonumber(args[6]) or 0\n local parentId = args[7] or ''\n local maxAttempts = tonumber(args[8]) or 0\n local orderingKey = args[9] or ''\n local groupConcurrency = tonumber(args[10]) or 0\n local jobId = redis.call('INCR', idKey)\n local jobIdStr = tostring(jobId)\n local prefix = string.sub(idKey, 1, #idKey - 2)\n local jobKey = prefix .. 'job:' .. jobIdStr\n local useGroupConcurrency = (orderingKey ~= '' and groupConcurrency > 1)\n local orderingSeq = 0\n if orderingKey ~= '' and not useGroupConcurrency then\n local orderingMetaKey = prefix .. 'ordering'\n orderingSeq = redis.call('HINCRBY', orderingMetaKey, orderingKey, 1)\n end\n if useGroupConcurrency then\n local groupHashKey = prefix .. 'group:' .. orderingKey\n local curMax = tonumber(redis.call('HGET', groupHashKey, 'maxConcurrency')) or 0\n if curMax ~= groupConcurrency then\n redis.call('HSET', groupHashKey, 'maxConcurrency', tostring(groupConcurrency))\n end\n end\n local hashFields = {\n 'id', jobIdStr,\n 'name', jobName,\n 'data', jobData,\n 'opts', jobOpts,\n 'timestamp', tostring(timestamp),\n 'attemptsMade', '0',\n 'delay', tostring(delay),\n 'priority', tostring(priority),\n 'maxAttempts', tostring(maxAttempts)\n }\n if useGroupConcurrency then\n hashFields[#hashFields + 1] = 'groupKey'\n hashFields[#hashFields + 1] = orderingKey\n elseif orderingKey ~= '' then\n hashFields[#hashFields + 1] = 'orderingKey'\n hashFields[#hashFields + 1] = orderingKey\n hashFields[#hashFields + 1] = 'orderingSeq'\n hashFields[#hashFields + 1] = tostring(orderingSeq)\n end\n if parentId ~= '' then\n hashFields[#hashFields + 1] = 'parentId'\n hashFields[#hashFields + 1] = parentId\n end\n if delay > 0 or priority > 0 then\n hashFields[#hashFields + 1] = 'state'\n hashFields[#hashFields + 1] = delay > 0 and 'delayed' or 'prioritized'\n else\n hashFields[#hashFields + 1] = 'state'\n hashFields[#hashFields + 1] = 'waiting'\n end\n redis.call('HSET', jobKey, unpack(hashFields))\n if delay > 0 then\n local score = priority * PRIORITY_SHIFT + (timestamp + delay)\n redis.call('ZADD', scheduledKey, score, jobIdStr)\n elseif priority > 0 then\n local score = priority * PRIORITY_SHIFT\n redis.call('ZADD', scheduledKey, score, jobIdStr)\n else\n redis.call('XADD', streamKey, '*', 'jobId', jobIdStr)\n end\n emitEvent(eventsKey, 'added', jobIdStr, {'name', jobName})\n return jobIdStr\nend)\n\nredis.register_function('glidemq_promote', function(keys, args)\n local scheduledKey = keys[1]\n local streamKey = keys[2]\n local eventsKey = keys[3]\n local now = tonumber(args[1])\n local MAX_PROMOTIONS = 1000\n local count = 0\n local cursorMin = 0\n while count < MAX_PROMOTIONS do\n local nextEntry = redis.call('ZRANGEBYSCORE', scheduledKey, string.format('%.0f', cursorMin), '+inf', 'WITHSCORES', 'LIMIT', 0, 1)\n if not nextEntry or #nextEntry == 0 then\n break\n end\n local firstScore = tonumber(nextEntry[2]) or 0\n local priority = math.floor(firstScore / PRIORITY_SHIFT)\n local minScore = priority * PRIORITY_SHIFT\n local maxDueScore = minScore + now\n local remaining = MAX_PROMOTIONS - count\n local members = redis.call(\n 'ZRANGEBYSCORE',\n scheduledKey,\n string.format('%.0f', minScore),\n string.format('%.0f', maxDueScore),\n 'LIMIT',\n 0,\n remaining\n )\n for i = 1, #members do\n local jobId = members[i]\n redis.call('XADD', streamKey, '*', 'jobId', jobId)\n redis.call('ZREM', scheduledKey, jobId)\n local prefix = string.sub(scheduledKey, 1, #scheduledKey - 9)\n local jobKey = prefix .. 'job:' .. jobId\n redis.call('HSET', jobKey, 'state', 'waiting')\n emitEvent(eventsKey, 'promoted', jobId, nil)\n count = count + 1\n end\n cursorMin = (priority + 1) * PRIORITY_SHIFT\n end\n return count\nend)\n\nredis.register_function('glidemq_complete', function(keys, args)\n local streamKey = keys[1]\n local completedKey = keys[2]\n local eventsKey = keys[3]\n local jobKey = keys[4]\n local jobId = args[1]\n local entryId = args[2]\n local returnvalue = args[3]\n local timestamp = tonumber(args[4])\n local group = args[5]\n local removeMode = args[6] or '0'\n local removeCount = tonumber(args[7]) or 0\n local removeAge = tonumber(args[8]) or 0\n local depsMember = args[9] or ''\n local parentId = args[10] or ''\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n redis.call('ZADD', completedKey, timestamp, jobId)\n redis.call('HSET', jobKey,\n 'state', 'completed',\n 'returnvalue', returnvalue,\n 'finishedOn', tostring(timestamp)\n )\n markOrderingDone(jobKey, jobId)\n releaseGroupSlotAndPromote(jobKey, jobId)\n emitEvent(eventsKey, 'completed', jobId, {'returnvalue', returnvalue})\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n if removeMode == 'true' then\n redis.call('ZREM', completedKey, jobId)\n redis.call('DEL', jobKey)\n elseif removeMode == 'count' and removeCount > 0 then\n local total = redis.call('ZCARD', completedKey)\n if total > removeCount then\n local excess = redis.call('ZRANGE', completedKey, 0, total - removeCount - 1)\n for i = 1, #excess do\n local oldId = excess[i]\n redis.call('DEL', prefix .. 'job:' .. oldId)\n redis.call('ZREM', completedKey, oldId)\n end\n end\n elseif removeMode == 'age_count' then\n if removeAge > 0 then\n local cutoff = timestamp - (removeAge * 1000)\n local old = redis.call('ZRANGEBYSCORE', completedKey, '0', tostring(cutoff))\n for i = 1, #old do\n local oldId = old[i]\n redis.call('DEL', prefix .. 'job:' .. oldId)\n redis.call('ZREM', completedKey, oldId)\n end\n end\n if removeCount > 0 then\n local total = redis.call('ZCARD', completedKey)\n if total > removeCount then\n local excess = redis.call('ZRANGE', completedKey, 0, total - removeCount - 1)\n for i = 1, #excess do\n local oldId = excess[i]\n redis.call('DEL', prefix .. 'job:' .. oldId)\n redis.call('ZREM', completedKey, oldId)\n end\n end\n end\n end\n if depsMember ~= '' and parentId ~= '' and #keys >= 8 then\n local parentDepsKey = keys[5]\n local parentJobKey = keys[6]\n local parentStreamKey = keys[7]\n local parentEventsKey = keys[8]\n local doneCount = redis.call('HINCRBY', parentJobKey, 'depsCompleted', 1)\n local totalDeps = redis.call('SCARD', parentDepsKey)\n local remaining = totalDeps - doneCount\n if remaining <= 0 then\n redis.call('HSET', parentJobKey, 'state', 'waiting')\n redis.call('XADD', parentStreamKey, '*', 'jobId', parentId)\n emitEvent(parentEventsKey, 'active', parentId, nil)\n end\n end\n return 1\nend)\n\nredis.register_function('glidemq_completeAndFetchNext', function(keys, args)\n local streamKey = keys[1]\n local completedKey = keys[2]\n local eventsKey = keys[3]\n local jobKey = keys[4]\n local jobId = args[1]\n local entryId = args[2]\n local returnvalue = args[3]\n local timestamp = tonumber(args[4])\n local group = args[5]\n local consumer = args[6]\n local removeMode = args[7] or '0'\n local removeCount = tonumber(args[8]) or 0\n local removeAge = tonumber(args[9]) or 0\n local depsMember = args[10] or ''\n local parentId = args[11] or ''\n\n -- Phase 1: Complete current job (same as glidemq_complete)\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n redis.call('ZADD', completedKey, timestamp, jobId)\n redis.call('HSET', jobKey,\n 'state', 'completed',\n 'returnvalue', returnvalue,\n 'finishedOn', tostring(timestamp)\n )\n markOrderingDone(jobKey, jobId)\n releaseGroupSlotAndPromote(jobKey, jobId)\n emitEvent(eventsKey, 'completed', jobId, {'returnvalue', returnvalue})\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n\n -- Retention cleanup\n if removeMode == 'true' then\n redis.call('ZREM', completedKey, jobId)\n redis.call('DEL', jobKey)\n elseif removeMode == 'count' and removeCount > 0 then\n local total = redis.call('ZCARD', completedKey)\n if total > removeCount then\n local excess = redis.call('ZRANGE', completedKey, 0, total - removeCount - 1)\n for i = 1, #excess do\n redis.call('DEL', prefix .. 'job:' .. excess[i])\n redis.call('ZREM', completedKey, excess[i])\n end\n end\n end\n\n -- Parent deps\n if depsMember ~= '' and parentId ~= '' and #keys >= 8 then\n local parentDepsKey = keys[5]\n local parentJobKey = keys[6]\n local parentStreamKey = keys[7]\n local parentEventsKey = keys[8]\n local doneCount = redis.call('HINCRBY', parentJobKey, 'depsCompleted', 1)\n local totalDeps = redis.call('SCARD', parentDepsKey)\n if totalDeps - doneCount <= 0 then\n redis.call('HSET', parentJobKey, 'state', 'waiting')\n redis.call('XADD', parentStreamKey, '*', 'jobId', parentId)\n emitEvent(parentEventsKey, 'active', parentId, nil)\n end\n end\n\n -- Phase 2: Fetch next job (non-blocking XREADGROUP)\n local nextEntries = redis.call('XREADGROUP', 'GROUP', group, consumer, 'COUNT', 1, 'STREAMS', streamKey, '>')\n if not nextEntries or #nextEntries == 0 then\n return cjson.encode({completed = jobId, next = false})\n end\n local streamData = nextEntries[1]\n local entries = streamData[2]\n if not entries or #entries == 0 then\n return cjson.encode({completed = jobId, next = false})\n end\n local nextEntry = entries[1]\n local nextEntryId = nextEntry[1]\n local nextFields = nextEntry[2]\n local nextJobId = nil\n for i = 1, #nextFields, 2 do\n if nextFields[i] == 'jobId' then\n nextJobId = nextFields[i + 1]\n break\n end\n end\n if not nextJobId then\n return cjson.encode({completed = jobId, next = false})\n end\n\n -- Phase 3: Activate next job (same as moveToActive)\n local nextJobKey = prefix .. 'job:' .. nextJobId\n local nextExists = redis.call('EXISTS', nextJobKey)\n if nextExists == 0 then\n return cjson.encode({completed = jobId, next = false, nextEntryId = nextEntryId})\n end\n local revoked = redis.call('HGET', nextJobKey, 'revoked')\n if revoked == '1' then\n return cjson.encode({completed = jobId, next = 'REVOKED', nextJobId = nextJobId, nextEntryId = nextEntryId})\n end\n local nextGroupKey = redis.call('HGET', nextJobKey, 'groupKey')\n if nextGroupKey and nextGroupKey ~= '' then\n local nextGroupHashKey = prefix .. 'group:' .. nextGroupKey\n local nextMaxConc = tonumber(redis.call('HGET', nextGroupHashKey, 'maxConcurrency')) or 0\n local nextActive = tonumber(redis.call('HGET', nextGroupHashKey, 'active')) or 0\n if nextMaxConc > 0 and nextActive >= nextMaxConc then\n redis.call('XACK', streamKey, group, nextEntryId)\n redis.call('XDEL', streamKey, nextEntryId)\n local nextWaitListKey = prefix .. 'groupq:' .. nextGroupKey\n redis.call('RPUSH', nextWaitListKey, nextJobId)\n redis.call('HSET', nextJobKey, 'state', 'group-waiting')\n return cjson.encode({completed = jobId, next = false})\n end\n redis.call('HINCRBY', nextGroupHashKey, 'active', 1)\n end\n redis.call('HSET', nextJobKey, 'state', 'active', 'processedOn', tostring(timestamp), 'lastActive', tostring(timestamp))\n local nextHash = redis.call('HGETALL', nextJobKey)\n return cjson.encode({completed = jobId, next = nextHash, nextJobId = nextJobId, nextEntryId = nextEntryId})\nend)\n\nredis.register_function('glidemq_fail', function(keys, args)\n local streamKey = keys[1]\n local failedKey = keys[2]\n local scheduledKey = keys[3]\n local eventsKey = keys[4]\n local jobKey = keys[5]\n local jobId = args[1]\n local entryId = args[2]\n local failedReason = args[3]\n local timestamp = tonumber(args[4])\n local maxAttempts = tonumber(args[5]) or 0\n local backoffDelay = tonumber(args[6]) or 0\n local group = args[7]\n local removeMode = args[8] or '0'\n local removeCount = tonumber(args[9]) or 0\n local removeAge = tonumber(args[10]) or 0\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n local attemptsMade = redis.call('HINCRBY', jobKey, 'attemptsMade', 1)\n if maxAttempts > 0 and attemptsMade < maxAttempts then\n local retryAt = timestamp + backoffDelay\n local priority = tonumber(redis.call('HGET', jobKey, 'priority')) or 0\n local score = priority * PRIORITY_SHIFT + retryAt\n redis.call('ZADD', scheduledKey, score, jobId)\n redis.call('HSET', jobKey,\n 'state', 'delayed',\n 'failedReason', failedReason,\n 'processedOn', tostring(timestamp)\n )\n releaseGroupSlotAndPromote(jobKey, jobId)\n emitEvent(eventsKey, 'retrying', jobId, {\n 'failedReason', failedReason,\n 'attemptsMade', tostring(attemptsMade),\n 'delay', tostring(backoffDelay)\n })\n return 'retrying'\n else\n redis.call('ZADD', failedKey, timestamp, jobId)\n redis.call('HSET', jobKey,\n 'state', 'failed',\n 'failedReason', failedReason,\n 'finishedOn', tostring(timestamp),\n 'processedOn', tostring(timestamp)\n )\n markOrderingDone(jobKey, jobId)\n releaseGroupSlotAndPromote(jobKey, jobId)\n emitEvent(eventsKey, 'failed', jobId, {'failedReason', failedReason})\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n if removeMode == 'true' then\n redis.call('ZREM', failedKey, jobId)\n redis.call('DEL', jobKey)\n elseif removeMode == 'count' and removeCount > 0 then\n local total = redis.call('ZCARD', failedKey)\n if total > removeCount then\n local excess = redis.call('ZRANGE', failedKey, 0, total - removeCount - 1)\n for i = 1, #excess do\n local oldId = excess[i]\n redis.call('DEL', prefix .. 'job:' .. oldId)\n redis.call('ZREM', failedKey, oldId)\n end\n end\n elseif removeMode == 'age_count' then\n if removeAge > 0 then\n local cutoff = timestamp - (removeAge * 1000)\n local old = redis.call('ZRANGEBYSCORE', failedKey, '0', tostring(cutoff))\n for i = 1, #old do\n local oldId = old[i]\n redis.call('DEL', prefix .. 'job:' .. oldId)\n redis.call('ZREM', failedKey, oldId)\n end\n end\n if removeCount > 0 then\n local total = redis.call('ZCARD', failedKey)\n if total > removeCount then\n local excess = redis.call('ZRANGE', failedKey, 0, total - removeCount - 1)\n for i = 1, #excess do\n local oldId = excess[i]\n redis.call('DEL', prefix .. 'job:' .. oldId)\n redis.call('ZREM', failedKey, oldId)\n end\n end\n end\n end\n return 'failed'\n end\nend)\n\nredis.register_function('glidemq_reclaimStalled', function(keys, args)\n local streamKey = keys[1]\n local eventsKey = keys[2]\n local group = args[1]\n local consumer = args[2]\n local minIdleMs = tonumber(args[3])\n local maxStalledCount = tonumber(args[4]) or 1\n local timestamp = tonumber(args[5])\n local failedKey = args[6]\n local result = redis.call('XAUTOCLAIM', streamKey, group, consumer, minIdleMs, '0-0')\n local entries = result[2]\n if not entries or #entries == 0 then\n return 0\n end\n local prefix = string.sub(streamKey, 1, #streamKey - 6)\n local count = 0\n for i = 1, #entries do\n local entry = entries[i]\n local entryId = entry[1]\n local fields = entry[2]\n local jobId = nil\n if type(fields) == 'table' then\n for j = 1, #fields, 2 do\n if fields[j] == 'jobId' then\n jobId = fields[j + 1]\n break\n end\n end\n end\n if jobId then\n local jobKey = prefix .. 'job:' .. jobId\n local lastActive = tonumber(redis.call('HGET', jobKey, 'lastActive'))\n if lastActive and (timestamp - lastActive) < minIdleMs then\n count = count + 1\n else\n local stalledCount = redis.call('HINCRBY', jobKey, 'stalledCount', 1)\n if stalledCount > maxStalledCount then\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n redis.call('ZADD', failedKey, timestamp, jobId)\n redis.call('HSET', jobKey,\n 'state', 'failed',\n 'failedReason', 'job stalled more than maxStalledCount',\n 'finishedOn', tostring(timestamp)\n )\n markOrderingDone(jobKey, jobId)\n releaseGroupSlotAndPromote(jobKey, jobId)\n emitEvent(eventsKey, 'failed', jobId, {\n 'failedReason', 'job stalled more than maxStalledCount'\n })\n else\n redis.call('HSET', jobKey, 'state', 'active')\n emitEvent(eventsKey, 'stalled', jobId, nil)\n end\n count = count + 1\n end\n end\n end\n return count\nend)\n\nredis.register_function('glidemq_pause', function(keys, args)\n local metaKey = keys[1]\n local eventsKey = keys[2]\n redis.call('HSET', metaKey, 'paused', '1')\n emitEvent(eventsKey, 'paused', '0', nil)\n return 1\nend)\n\nredis.register_function('glidemq_resume', function(keys, args)\n local metaKey = keys[1]\n local eventsKey = keys[2]\n redis.call('HSET', metaKey, 'paused', '0')\n emitEvent(eventsKey, 'resumed', '0', nil)\n return 1\nend)\n\nredis.register_function('glidemq_dedup', function(keys, args)\n local dedupKey = keys[1]\n local idKey = keys[2]\n local streamKey = keys[3]\n local scheduledKey = keys[4]\n local eventsKey = keys[5]\n local dedupId = args[1]\n local ttlMs = tonumber(args[2]) or 0\n local mode = args[3]\n local jobName = args[4]\n local jobData = args[5]\n local jobOpts = args[6]\n local timestamp = tonumber(args[7])\n local delay = tonumber(args[8]) or 0\n local priority = tonumber(args[9]) or 0\n local parentId = args[10] or ''\n local maxAttempts = tonumber(args[11]) or 0\n local orderingKey = args[12] or ''\n local groupConcurrency = tonumber(args[13]) or 0\n local prefix = string.sub(idKey, 1, #idKey - 2)\n local existing = redis.call('HGET', dedupKey, dedupId)\n if mode == 'simple' then\n if existing then\n local sep = string.find(existing, ':')\n if sep then\n local existingJobId = string.sub(existing, 1, sep - 1)\n local jobKey = prefix .. 'job:' .. existingJobId\n local state = redis.call('HGET', jobKey, 'state')\n if state and state ~= 'completed' and state ~= 'failed' then\n return 'skipped'\n end\n end\n end\n elseif mode == 'throttle' then\n if existing and ttlMs > 0 then\n local sep = string.find(existing, ':')\n if sep then\n local storedTs = tonumber(string.sub(existing, sep + 1))\n if storedTs and (timestamp - storedTs) < ttlMs then\n return 'skipped'\n end\n end\n end\n elseif mode == 'debounce' then\n if existing then\n local sep = string.find(existing, ':')\n if sep then\n local existingJobId = string.sub(existing, 1, sep - 1)\n local jobKey = prefix .. 'job:' .. existingJobId\n local state = redis.call('HGET', jobKey, 'state')\n if state == 'delayed' or state == 'prioritized' then\n redis.call('ZREM', scheduledKey, existingJobId)\n markOrderingDone(jobKey, existingJobId)\n redis.call('DEL', jobKey)\n emitEvent(eventsKey, 'removed', existingJobId, nil)\n elseif state and state ~= 'completed' and state ~= 'failed' then\n return 'skipped'\n end\n end\n end\n end\n local jobId = redis.call('INCR', idKey)\n local jobIdStr = tostring(jobId)\n local jobKey = prefix .. 'job:' .. jobIdStr\n local useGroupConcurrency = (orderingKey ~= '' and groupConcurrency > 1)\n local orderingSeq = 0\n if orderingKey ~= '' and not useGroupConcurrency then\n local orderingMetaKey = prefix .. 'ordering'\n orderingSeq = redis.call('HINCRBY', orderingMetaKey, orderingKey, 1)\n end\n if useGroupConcurrency then\n local groupHashKey = prefix .. 'group:' .. orderingKey\n local curMax = tonumber(redis.call('HGET', groupHashKey, 'maxConcurrency')) or 0\n if curMax ~= groupConcurrency then\n redis.call('HSET', groupHashKey, 'maxConcurrency', tostring(groupConcurrency))\n end\n end\n local hashFields = {\n 'id', jobIdStr,\n 'name', jobName,\n 'data', jobData,\n 'opts', jobOpts,\n 'timestamp', tostring(timestamp),\n 'attemptsMade', '0',\n 'delay', tostring(delay),\n 'priority', tostring(priority),\n 'maxAttempts', tostring(maxAttempts)\n }\n if useGroupConcurrency then\n hashFields[#hashFields + 1] = 'groupKey'\n hashFields[#hashFields + 1] = orderingKey\n elseif orderingKey ~= '' then\n hashFields[#hashFields + 1] = 'orderingKey'\n hashFields[#hashFields + 1] = orderingKey\n hashFields[#hashFields + 1] = 'orderingSeq'\n hashFields[#hashFields + 1] = tostring(orderingSeq)\n end\n if parentId ~= '' then\n hashFields[#hashFields + 1] = 'parentId'\n hashFields[#hashFields + 1] = parentId\n end\n if delay > 0 or priority > 0 then\n hashFields[#hashFields + 1] = 'state'\n hashFields[#hashFields + 1] = delay > 0 and 'delayed' or 'prioritized'\n else\n hashFields[#hashFields + 1] = 'state'\n hashFields[#hashFields + 1] = 'waiting'\n end\n redis.call('HSET', jobKey, unpack(hashFields))\n if delay > 0 then\n local score = priority * PRIORITY_SHIFT + (timestamp + delay)\n redis.call('ZADD', scheduledKey, score, jobIdStr)\n elseif priority > 0 then\n local score = priority * PRIORITY_SHIFT\n redis.call('ZADD', scheduledKey, score, jobIdStr)\n else\n redis.call('XADD', streamKey, '*', 'jobId', jobIdStr)\n end\n redis.call('HSET', dedupKey, dedupId, jobIdStr .. ':' .. tostring(timestamp))\n emitEvent(eventsKey, 'added', jobIdStr, {'name', jobName})\n return jobIdStr\nend)\n\nredis.register_function('glidemq_rateLimit', function(keys, args)\n local rateKey = keys[1]\n local metaKey = keys[2]\n local maxPerWindow = tonumber(args[1])\n local windowDuration = tonumber(args[2])\n local now = tonumber(args[3])\n local windowStart = tonumber(redis.call('HGET', rateKey, 'windowStart')) or 0\n local count = tonumber(redis.call('HGET', rateKey, 'count')) or 0\n if now - windowStart >= windowDuration then\n redis.call('HSET', rateKey, 'windowStart', tostring(now), 'count', '1')\n return 0\n end\n if count >= maxPerWindow then\n local delayMs = windowDuration - (now - windowStart)\n return delayMs\n end\n redis.call('HSET', rateKey, 'count', tostring(count + 1))\n return 0\nend)\n\nredis.register_function('glidemq_checkConcurrency', function(keys, args)\n local metaKey = keys[1]\n local streamKey = keys[2]\n local group = args[1]\n local gc = tonumber(redis.call('HGET', metaKey, 'globalConcurrency')) or 0\n if gc <= 0 then\n return -1\n end\n local pending = redis.call('XPENDING', streamKey, group)\n local pendingCount = tonumber(pending[1]) or 0\n local remaining = gc - pendingCount\n if remaining <= 0 then\n return 0\n end\n return remaining\nend)\n\nredis.register_function('glidemq_moveToActive', function(keys, args)\n local jobKey = keys[1]\n local streamKey = keys[2] or ''\n local timestamp = args[1]\n local entryId = args[2] or ''\n local group = args[3] or ''\n local jobId = args[4] or ''\n local exists = redis.call('EXISTS', jobKey)\n if exists == 0 then\n return ''\n end\n local revoked = redis.call('HGET', jobKey, 'revoked')\n if revoked == '1' then\n return 'REVOKED'\n end\n local groupKey = redis.call('HGET', jobKey, 'groupKey')\n if groupKey and groupKey ~= '' then\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n local groupHashKey = prefix .. 'group:' .. groupKey\n local maxConc = tonumber(redis.call('HGET', groupHashKey, 'maxConcurrency')) or 0\n local active = tonumber(redis.call('HGET', groupHashKey, 'active')) or 0\n if maxConc > 0 and active >= maxConc then\n if streamKey ~= '' and entryId ~= '' and group ~= '' then\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n end\n local waitListKey = prefix .. 'groupq:' .. groupKey\n redis.call('RPUSH', waitListKey, jobId)\n redis.call('HSET', jobKey, 'state', 'group-waiting')\n return 'GROUP_FULL'\n end\n redis.call('HINCRBY', groupHashKey, 'active', 1)\n end\n redis.call('HSET', jobKey, 'state', 'active', 'processedOn', timestamp, 'lastActive', timestamp)\n local fields = redis.call('HGETALL', jobKey)\n return cjson.encode(fields)\nend)\n\nredis.register_function('glidemq_deferActive', function(keys, args)\n local streamKey = keys[1]\n local jobKey = keys[2]\n local jobId = args[1]\n local entryId = args[2]\n local group = args[3]\n local exists = redis.call('EXISTS', jobKey)\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n if exists == 0 then\n return 0\n end\n redis.call('XADD', streamKey, '*', 'jobId', jobId)\n redis.call('HSET', jobKey, 'state', 'waiting')\n return 1\nend)\n\nredis.register_function('glidemq_addFlow', function(keys, args)\n local parentIdKey = keys[1]\n local parentStreamKey = keys[2]\n local parentScheduledKey = keys[3]\n local parentEventsKey = keys[4]\n local parentName = args[1]\n local parentData = args[2]\n local parentOpts = args[3]\n local timestamp = tonumber(args[4])\n local parentDelay = tonumber(args[5]) or 0\n local parentPriority = tonumber(args[6]) or 0\n local parentMaxAttempts = tonumber(args[7]) or 0\n local numChildren = tonumber(args[8])\n local parentJobId = redis.call('INCR', parentIdKey)\n local parentJobIdStr = tostring(parentJobId)\n local parentPrefix = string.sub(parentIdKey, 1, #parentIdKey - 2)\n local parentJobKey = parentPrefix .. 'job:' .. parentJobIdStr\n local depsKey = parentPrefix .. 'deps:' .. parentJobIdStr\n local parentOrderingKey = extractOrderingKeyFromOpts(parentOpts)\n local parentGroupConc = extractGroupConcurrencyFromOpts(parentOpts)\n local parentOrderingSeq = 0\n if parentOrderingKey ~= '' and parentGroupConc <= 1 then\n local parentOrderingMetaKey = parentPrefix .. 'ordering'\n parentOrderingSeq = redis.call('HINCRBY', parentOrderingMetaKey, parentOrderingKey, 1)\n end\n local parentHash = {\n 'id', parentJobIdStr,\n 'name', parentName,\n 'data', parentData,\n 'opts', parentOpts,\n 'timestamp', tostring(timestamp),\n 'attemptsMade', '0',\n 'delay', tostring(parentDelay),\n 'priority', tostring(parentPriority),\n 'maxAttempts', tostring(parentMaxAttempts),\n 'state', 'waiting-children'\n }\n if parentOrderingKey ~= '' and parentGroupConc > 1 then\n parentHash[#parentHash + 1] = 'groupKey'\n parentHash[#parentHash + 1] = parentOrderingKey\n local groupHashKey = parentPrefix .. 'group:' .. parentOrderingKey\n redis.call('HSET', groupHashKey, 'maxConcurrency', tostring(parentGroupConc))\n redis.call('HSETNX', groupHashKey, 'active', '0')\n elseif parentOrderingKey ~= '' then\n parentHash[#parentHash + 1] = 'orderingKey'\n parentHash[#parentHash + 1] = parentOrderingKey\n parentHash[#parentHash + 1] = 'orderingSeq'\n parentHash[#parentHash + 1] = tostring(parentOrderingSeq)\n end\n redis.call('HSET', parentJobKey, unpack(parentHash))\n local childIds = {}\n local childArgOffset = 8\n local childKeyOffset = 4\n for i = 1, numChildren do\n local base = childArgOffset + (i - 1) * 8\n local childName = args[base + 1]\n local childData = args[base + 2]\n local childOpts = args[base + 3]\n local childDelay = tonumber(args[base + 4]) or 0\n local childPriority = tonumber(args[base + 5]) or 0\n local childMaxAttempts = tonumber(args[base + 6]) or 0\n local childQueuePrefix = args[base + 7]\n local childParentQueue = args[base + 8]\n local ckBase = childKeyOffset + (i - 1) * 4\n local childIdKey = keys[ckBase + 1]\n local childStreamKey = keys[ckBase + 2]\n local childScheduledKey = keys[ckBase + 3]\n local childEventsKey = keys[ckBase + 4]\n local childJobId = redis.call('INCR', childIdKey)\n local childJobIdStr = tostring(childJobId)\n local childPrefix = string.sub(childIdKey, 1, #childIdKey - 2)\n local childJobKey = childPrefix .. 'job:' .. childJobIdStr\n local childOrderingKey = extractOrderingKeyFromOpts(childOpts)\n local childGroupConc = extractGroupConcurrencyFromOpts(childOpts)\n local childOrderingSeq = 0\n if childOrderingKey ~= '' and childGroupConc <= 1 then\n local childOrderingMetaKey = childPrefix .. 'ordering'\n childOrderingSeq = redis.call('HINCRBY', childOrderingMetaKey, childOrderingKey, 1)\n end\n local childHash = {\n 'id', childJobIdStr,\n 'name', childName,\n 'data', childData,\n 'opts', childOpts,\n 'timestamp', tostring(timestamp),\n 'attemptsMade', '0',\n 'delay', tostring(childDelay),\n 'priority', tostring(childPriority),\n 'maxAttempts', tostring(childMaxAttempts),\n 'parentId', parentJobIdStr,\n 'parentQueue', childParentQueue\n }\n if childOrderingKey ~= '' and childGroupConc > 1 then\n childHash[#childHash + 1] = 'groupKey'\n childHash[#childHash + 1] = childOrderingKey\n local childGroupHashKey = childPrefix .. 'group:' .. childOrderingKey\n redis.call('HSETNX', childGroupHashKey, 'maxConcurrency', tostring(childGroupConc))\n redis.call('HSETNX', childGroupHashKey, 'active', '0')\n elseif childOrderingKey ~= '' then\n childHash[#childHash + 1] = 'orderingKey'\n childHash[#childHash + 1] = childOrderingKey\n childHash[#childHash + 1] = 'orderingSeq'\n childHash[#childHash + 1] = tostring(childOrderingSeq)\n end\n if childDelay > 0 or childPriority > 0 then\n childHash[#childHash + 1] = 'state'\n childHash[#childHash + 1] = childDelay > 0 and 'delayed' or 'prioritized'\n else\n childHash[#childHash + 1] = 'state'\n childHash[#childHash + 1] = 'waiting'\n end\n redis.call('HSET', childJobKey, unpack(childHash))\n local depsMember = childQueuePrefix .. ':' .. childJobIdStr\n redis.call('SADD', depsKey, depsMember)\n if childDelay > 0 then\n local score = childPriority * PRIORITY_SHIFT + (timestamp + childDelay)\n redis.call('ZADD', childScheduledKey, score, childJobIdStr)\n elseif childPriority > 0 then\n local score = childPriority * PRIORITY_SHIFT\n redis.call('ZADD', childScheduledKey, score, childJobIdStr)\n else\n redis.call('XADD', childStreamKey, '*', 'jobId', childJobIdStr)\n end\n emitEvent(childEventsKey, 'added', childJobIdStr, {'name', childName})\n childIds[#childIds + 1] = childJobIdStr\n end\n local extraDepsOffset = childArgOffset + numChildren * 8\n local numExtraDeps = tonumber(args[extraDepsOffset + 1]) or 0\n for i = 1, numExtraDeps do\n local extraMember = args[extraDepsOffset + 1 + i]\n redis.call('SADD', depsKey, extraMember)\n end\n emitEvent(parentEventsKey, 'added', parentJobIdStr, {'name', parentName})\n local result = {parentJobIdStr}\n for i = 1, #childIds do\n result[#result + 1] = childIds[i]\n end\n return cjson.encode(result)\nend)\n\nredis.register_function('glidemq_completeChild', function(keys, args)\n local depsKey = keys[1]\n local parentJobKey = keys[2]\n local parentStreamKey = keys[3]\n local parentEventsKey = keys[4]\n local depsMember = args[1]\n local parentId = args[2]\n local doneCount = redis.call('HINCRBY', parentJobKey, 'depsCompleted', 1)\n local totalDeps = redis.call('SCARD', depsKey)\n local remaining = totalDeps - doneCount\n if remaining <= 0 then\n redis.call('HSET', parentJobKey, 'state', 'waiting')\n redis.call('XADD', parentStreamKey, '*', 'jobId', parentId)\n emitEvent(parentEventsKey, 'active', parentId, nil)\n end\n return remaining\nend)\n\nredis.register_function('glidemq_removeJob', function(keys, args)\n local jobKey = keys[1]\n local streamKey = keys[2]\n local scheduledKey = keys[3]\n local completedKey = keys[4]\n local failedKey = keys[5]\n local eventsKey = keys[6]\n local logKey = keys[7]\n local jobId = args[1]\n local exists = redis.call('EXISTS', jobKey)\n if exists == 0 then\n return 0\n end\n local state = redis.call('HGET', jobKey, 'state')\n local groupKey = redis.call('HGET', jobKey, 'groupKey')\n if groupKey and groupKey ~= '' then\n if state == 'active' then\n releaseGroupSlotAndPromote(jobKey, jobId)\n elseif state == 'group-waiting' then\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n local waitListKey = prefix .. 'groupq:' .. groupKey\n redis.call('LREM', waitListKey, 1, jobId)\n end\n end\n redis.call('ZREM', scheduledKey, jobId)\n redis.call('ZREM', completedKey, jobId)\n redis.call('ZREM', failedKey, jobId)\n markOrderingDone(jobKey, jobId)\n redis.call('DEL', jobKey)\n redis.call('DEL', logKey)\n emitEvent(eventsKey, 'removed', jobId, nil)\n return 1\nend)\n\nredis.register_function('glidemq_revoke', function(keys, args)\n local jobKey = keys[1]\n local streamKey = keys[2]\n local scheduledKey = keys[3]\n local failedKey = keys[4]\n local eventsKey = keys[5]\n local jobId = args[1]\n local timestamp = tonumber(args[2])\n local group = args[3]\n local exists = redis.call('EXISTS', jobKey)\n if exists == 0 then\n return 'not_found'\n end\n redis.call('HSET', jobKey, 'revoked', '1')\n local state = redis.call('HGET', jobKey, 'state')\n if state == 'group-waiting' then\n local gk = redis.call('HGET', jobKey, 'groupKey')\n if gk and gk ~= '' then\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n local waitListKey = prefix .. 'groupq:' .. gk\n redis.call('LREM', waitListKey, 1, jobId)\n end\n redis.call('ZADD', failedKey, timestamp, jobId)\n redis.call('HSET', jobKey,\n 'state', 'failed',\n 'failedReason', 'revoked',\n 'finishedOn', tostring(timestamp)\n )\n emitEvent(eventsKey, 'revoked', jobId, nil)\n return 'revoked'\n end\n if state == 'waiting' or state == 'delayed' or state == 'prioritized' then\n redis.call('ZREM', scheduledKey, jobId)\n local entries = redis.call('XRANGE', streamKey, '-', '+')\n for i = 1, #entries do\n local entryId = entries[i][1]\n local fields = entries[i][2]\n for j = 1, #fields, 2 do\n if fields[j] == 'jobId' and fields[j+1] == jobId then\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n break\n end\n end\n end\n redis.call('ZADD', failedKey, timestamp, jobId)\n redis.call('HSET', jobKey,\n 'state', 'failed',\n 'failedReason', 'revoked',\n 'finishedOn', tostring(timestamp)\n )\n markOrderingDone(jobKey, jobId)\n emitEvent(eventsKey, 'revoked', jobId, nil)\n return 'revoked'\n end\n emitEvent(eventsKey, 'revoked', jobId, nil)\n return 'flagged'\nend)\n\nredis.register_function('glidemq_searchByName', function(keys, args)\n local stateKey = keys[1]\n local stateType = args[1]\n local nameFilter = args[2]\n local limit = tonumber(args[3]) or 100\n local prefix = args[4]\n local matched = {}\n if stateType == 'zset' then\n local members = redis.call('ZRANGE', stateKey, 0, -1)\n for i = 1, #members do\n if #matched >= limit then break end\n local jobId = members[i]\n local jobKey = prefix .. 'job:' .. jobId\n local name = redis.call('HGET', jobKey, 'name')\n if name == nameFilter then\n matched[#matched + 1] = jobId\n end\n end\n elseif stateType == 'stream' then\n local entries = redis.call('XRANGE', stateKey, '-', '+')\n for i = 1, #entries do\n if #matched >= limit then break end\n local fields = entries[i][2]\n local jobId = nil\n for j = 1, #fields, 2 do\n if fields[j] == 'jobId' then\n jobId = fields[j + 1]\n break\n end\n end\n if jobId then\n local jobKey = prefix .. 'job:' .. jobId\n local name = redis.call('HGET', jobKey, 'name')\n if name == nameFilter then\n matched[#matched + 1] = jobId\n end\n end\n end\n end\n return matched\nend)\n";
|
|
6
|
+
export declare const LIBRARY_SOURCE = "#!lua name=glidemq\n\nlocal PRIORITY_SHIFT = 4398046511104\n\nlocal function emitEvent(eventsKey, eventType, jobId, extraFields)\n local fields = {'event', eventType, 'jobId', tostring(jobId)}\n if extraFields then\n for i = 1, #extraFields, 2 do\n fields[#fields + 1] = extraFields[i]\n fields[#fields + 1] = extraFields[i + 1]\n end\n end\n redis.call('XADD', eventsKey, 'MAXLEN', '~', '1000', '*', unpack(fields))\nend\n\nlocal function markOrderingDone(jobKey, jobId)\n local orderingKey = redis.call('HGET', jobKey, 'orderingKey')\n if not orderingKey or orderingKey == '' then\n return\n end\n local orderingSeq = tonumber(redis.call('HGET', jobKey, 'orderingSeq')) or 0\n if orderingSeq <= 0 then\n return\n end\n\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n local metaKey = prefix .. 'meta'\n local doneField = 'orderdone:' .. orderingKey\n local pendingKey = prefix .. 'orderdone:pending:' .. orderingKey\n\n local lastDone = tonumber(redis.call('HGET', metaKey, doneField)) or 0\n if orderingSeq <= lastDone then\n redis.call('HDEL', pendingKey, tostring(orderingSeq))\n return\n end\n\n redis.call('HSET', pendingKey, tostring(orderingSeq), '1')\n local advanced = lastDone\n while true do\n local nextSeq = advanced + 1\n if redis.call('HEXISTS', pendingKey, tostring(nextSeq)) == 0 then\n break\n end\n redis.call('HDEL', pendingKey, tostring(nextSeq))\n advanced = nextSeq\n end\n if advanced > lastDone then\n redis.call('HSET', metaKey, doneField, tostring(advanced))\n end\nend\n\n-- Refill token bucket using remainder accumulator for precision.\n-- tbRefillRate is in millitokens/second. Returns current millitokens after refill.\n-- Side effect: updates tbTokens, tbLastRefill, tbRefillRemainder on the group hash.\nlocal function tbRefill(groupHashKey, g, now)\n local tbCapacity = tonumber(g.tbCapacity) or 0\n if tbCapacity <= 0 then return 0 end\n local tbTokens = tonumber(g.tbTokens) or tbCapacity\n local tbRefillRate = tonumber(g.tbRefillRate) or 0\n local tbLastRefill = tonumber(g.tbLastRefill) or now\n local tbRefillRemainder = tonumber(g.tbRefillRemainder) or 0\n local elapsed = now - tbLastRefill\n if elapsed <= 0 or tbRefillRate <= 0 then return tbTokens end\n -- Cap elapsed to prevent overflow in long-idle buckets\n local maxElapsed = math.ceil(tbCapacity * 1000 / tbRefillRate)\n if elapsed > maxElapsed then elapsed = maxElapsed end\n local raw = elapsed * tbRefillRate + tbRefillRemainder\n local added = math.floor(raw / 1000)\n local newRemainder = raw % 1000\n local newTokens = math.min(tbCapacity, tbTokens + added)\n redis.call('HSET', groupHashKey,\n 'tbTokens', tostring(newTokens),\n 'tbLastRefill', tostring(now),\n 'tbRefillRemainder', tostring(newRemainder))\n return newTokens\nend\n\nlocal function releaseGroupSlotAndPromote(jobKey, jobId, now)\n local gk = redis.call('HGET', jobKey, 'groupKey')\n if not gk or gk == '' then return end\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n local groupHashKey = prefix .. 'group:' .. gk\n -- Load all group fields in one call\n local gFields = redis.call('HGETALL', groupHashKey)\n local g = {}\n for gf = 1, #gFields, 2 do g[gFields[gf]] = gFields[gf + 1] end\n local cur = tonumber(g.active) or 0\n local newActive = (cur > 0) and (cur - 1) or 0\n if cur > 0 then\n redis.call('HSET', groupHashKey, 'active', tostring(newActive))\n end\n local waitListKey = prefix .. 'groupq:' .. gk\n local waitLen = redis.call('LLEN', waitListKey)\n if waitLen == 0 then return end\n -- Concurrency gate: if still at or above max after decrement, do not promote\n local maxConc = tonumber(g.maxConcurrency) or 0\n if maxConc > 0 and newActive >= maxConc then return end\n -- Rate limit gate (skip if now is nil or 0 for safe fallback)\n -- Only blocks promotion; does NOT increment rateCount. moveToActive handles counting.\n local rateMax = tonumber(g.rateMax) or 0\n local rateRemaining = 0\n local ts = tonumber(now) or 0\n if ts > 0 and rateMax > 0 then\n local rateDuration = tonumber(g.rateDuration) or 0\n if rateDuration > 0 then\n local rateWindowStart = tonumber(g.rateWindowStart) or 0\n local rateCount = tonumber(g.rateCount) or 0\n if ts - rateWindowStart < rateDuration then\n if rateCount >= rateMax then\n -- Window active and at capacity: do not promote, register for scheduler\n local rateLimitedKey = prefix .. 'ratelimited'\n redis.call('ZADD', rateLimitedKey, rateWindowStart + rateDuration, gk)\n return\n end\n rateRemaining = rateMax - rateCount\n end\n end\n end\n -- Token bucket gate: check head job cost before promoting\n local tbCap = tonumber(g.tbCapacity) or 0\n if ts > 0 and tbCap > 0 then\n local tbTokensCur = tbRefill(groupHashKey, g, ts)\n -- Peek at head job, skipping tombstones and DLQ'd jobs (up to 10 iterations)\n local tbCheckPasses = 0\n local tbOk = false\n while tbCheckPasses < 10 do\n tbCheckPasses = tbCheckPasses + 1\n local headJobId = redis.call('LINDEX', waitListKey, 0)\n if not headJobId then break end\n local headJobKey = prefix .. 'job:' .. headJobId\n -- Tombstone guard: job hash deleted - pop and check next\n if redis.call('EXISTS', headJobKey) == 0 then\n redis.call('LPOP', waitListKey)\n else\n local headCost = tonumber(redis.call('HGET', headJobKey, 'cost')) or 1000\n -- DLQ guard: cost > capacity - pop, fail, check next\n if headCost > tbCap then\n redis.call('LPOP', waitListKey)\n redis.call('ZADD', prefix .. 'failed', ts, headJobId)\n redis.call('HSET', headJobKey,\n 'state', 'failed',\n 'failedReason', 'cost exceeds token bucket capacity',\n 'finishedOn', tostring(ts))\n emitEvent(prefix .. 'events', 'failed', headJobId, {'failedReason', 'cost exceeds token bucket capacity'})\n elseif tbTokensCur < headCost then\n -- Not enough tokens: register delay and skip promotion\n local tbRateVal = tonumber(g.tbRefillRate) or 0\n if tbRateVal <= 0 then break end\n local tbDelayMs = math.ceil((headCost - tbTokensCur) * 1000 / tbRateVal)\n local rateLimitedKey = prefix .. 'ratelimited'\n redis.call('ZADD', rateLimitedKey, ts + tbDelayMs, gk)\n return\n else\n tbOk = true\n break\n end\n end\n end\n if not tbOk and tbCheckPasses >= 10 then return end\n end\n -- Calculate how many slots are available for promotion\n local available = 1\n if maxConc > 0 then\n available = maxConc - newActive\n else\n available = math.min(waitLen, 1000)\n end\n -- Cap by rate limit remaining if a window is active\n if rateRemaining > 0 then\n available = math.min(available, rateRemaining)\n end\n local streamKey = prefix .. 'stream'\n for p = 1, available do\n local nextJobId = redis.call('LPOP', waitListKey)\n if not nextJobId then break end\n redis.call('XADD', streamKey, '*', 'jobId', nextJobId)\n local nextJobKey = prefix .. 'job:' .. nextJobId\n redis.call('HSET', nextJobKey, 'state', 'waiting')\n end\nend\n\nlocal function extractOrderingKeyFromOpts(optsJson)\n if not optsJson or optsJson == '' then\n return ''\n end\n local ok, decoded = pcall(cjson.decode, optsJson)\n if not ok or type(decoded) ~= 'table' then\n return ''\n end\n local ordering = decoded['ordering']\n if type(ordering) ~= 'table' then\n return ''\n end\n local key = ordering['key']\n if key == nil then\n return ''\n end\n return tostring(key)\nend\n\nlocal function extractGroupConcurrencyFromOpts(optsJson)\n if not optsJson or optsJson == '' then\n return 0\n end\n local ok, decoded = pcall(cjson.decode, optsJson)\n if not ok or type(decoded) ~= 'table' then\n return 0\n end\n local ordering = decoded['ordering']\n if type(ordering) ~= 'table' then\n return 0\n end\n local conc = ordering['concurrency']\n if conc == nil then\n return 0\n end\n return tonumber(conc) or 0\nend\n\nlocal function extractGroupRateLimitFromOpts(optsJson)\n if not optsJson or optsJson == '' then\n return 0, 0\n end\n local ok, decoded = pcall(cjson.decode, optsJson)\n if not ok or type(decoded) ~= 'table' then\n return 0, 0\n end\n local ordering = decoded['ordering']\n if type(ordering) ~= 'table' then\n return 0, 0\n end\n local rl = ordering['rateLimit']\n if type(rl) ~= 'table' then\n return 0, 0\n end\n local max = tonumber(rl['max']) or 0\n local duration = tonumber(rl['duration']) or 0\n return max, duration\nend\n\nlocal function extractTokenBucketFromOpts(optsJson)\n if not optsJson or optsJson == '' then return 0, 0 end\n local ok, decoded = pcall(cjson.decode, optsJson)\n if not ok or type(decoded) ~= 'table' then return 0, 0 end\n local ordering = decoded['ordering']\n if type(ordering) ~= 'table' then return 0, 0 end\n local tb = ordering['tokenBucket']\n if type(tb) ~= 'table' then return 0, 0 end\n local capacity = tonumber(tb['capacity']) or 0\n local refillRate = tonumber(tb['refillRate']) or 0\n return math.floor(capacity * 1000), math.floor(refillRate * 1000)\nend\n\nlocal function extractCostFromOpts(optsJson)\n if not optsJson or optsJson == '' then return 0 end\n local ok, decoded = pcall(cjson.decode, optsJson)\n if not ok or type(decoded) ~= 'table' then return 0 end\n local cost = tonumber(decoded['cost']) or 0\n return math.floor(cost * 1000)\nend\n\nredis.register_function('glidemq_version', function(keys, args)\n return '19'\nend)\n\nredis.register_function('glidemq_addJob', function(keys, args)\n local idKey = keys[1]\n local streamKey = keys[2]\n local scheduledKey = keys[3]\n local eventsKey = keys[4]\n local jobName = args[1]\n local jobData = args[2]\n local jobOpts = args[3]\n local timestamp = tonumber(args[4])\n local delay = tonumber(args[5]) or 0\n local priority = tonumber(args[6]) or 0\n local parentId = args[7] or ''\n local maxAttempts = tonumber(args[8]) or 0\n local orderingKey = args[9] or ''\n local groupConcurrency = tonumber(args[10]) or 0\n local groupRateMax = tonumber(args[11]) or 0\n local groupRateDuration = tonumber(args[12]) or 0\n local tbCapacity = tonumber(args[13]) or 0\n local tbRefillRate = tonumber(args[14]) or 0\n local jobCost = tonumber(args[15]) or 0\n local jobId = redis.call('INCR', idKey)\n local jobIdStr = tostring(jobId)\n local prefix = string.sub(idKey, 1, #idKey - 2)\n local jobKey = prefix .. 'job:' .. jobIdStr\n local useGroupConcurrency = (orderingKey ~= '' and (groupConcurrency > 1 or groupRateMax > 0 or tbCapacity > 0))\n local orderingSeq = 0\n if orderingKey ~= '' and not useGroupConcurrency then\n local orderingMetaKey = prefix .. 'ordering'\n orderingSeq = redis.call('HINCRBY', orderingMetaKey, orderingKey, 1)\n end\n if useGroupConcurrency then\n local groupHashKey = prefix .. 'group:' .. orderingKey\n local curMax = tonumber(redis.call('HGET', groupHashKey, 'maxConcurrency')) or 0\n if curMax ~= groupConcurrency then\n redis.call('HSET', groupHashKey, 'maxConcurrency', tostring(groupConcurrency))\n end\n -- When rate limit or token bucket forces group path but concurrency is 0 or 1, ensure maxConcurrency >= 1\n if curMax == 0 and groupConcurrency <= 1 then\n redis.call('HSET', groupHashKey, 'maxConcurrency', '1')\n end\n -- Upsert rate limit fields on group hash\n if groupRateMax > 0 then\n local curRateMax = tonumber(redis.call('HGET', groupHashKey, 'rateMax')) or 0\n if curRateMax ~= groupRateMax then\n redis.call('HSET', groupHashKey, 'rateMax', tostring(groupRateMax))\n end\n local curRateDuration = tonumber(redis.call('HGET', groupHashKey, 'rateDuration')) or 0\n if curRateDuration ~= groupRateDuration then\n redis.call('HSET', groupHashKey, 'rateDuration', tostring(groupRateDuration))\n end\n else\n -- Clear stale rate limit fields if group was previously rate-limited\n local oldRateMax = tonumber(redis.call('HGET', groupHashKey, 'rateMax')) or 0\n if oldRateMax > 0 then\n redis.call('HDEL', groupHashKey, 'rateMax', 'rateDuration', 'rateWindowStart', 'rateCount')\n end\n end\n -- Upsert token bucket fields on group hash\n if tbCapacity > 0 then\n local curTbCap = tonumber(redis.call('HGET', groupHashKey, 'tbCapacity')) or 0\n if curTbCap ~= tbCapacity then\n redis.call('HSET', groupHashKey, 'tbCapacity', tostring(tbCapacity))\n end\n local curTbRate = tonumber(redis.call('HGET', groupHashKey, 'tbRefillRate')) or 0\n if curTbRate ~= tbRefillRate then\n redis.call('HSET', groupHashKey, 'tbRefillRate', tostring(tbRefillRate))\n end\n -- Initialize tokens on first setup\n if curTbCap == 0 then\n redis.call('HSET', groupHashKey,\n 'tbTokens', tostring(tbCapacity),\n 'tbLastRefill', tostring(timestamp),\n 'tbRefillRemainder', '0')\n end\n -- Validate cost <= capacity at enqueue\n -- Validate cost (explicit or default 1000 millitokens) against capacity\n local effectiveCost = (jobCost > 0) and jobCost or 1000\n if effectiveCost > tbCapacity then\n return 'ERR:COST_EXCEEDS_CAPACITY'\n end\n else\n -- Clear stale tb fields\n local oldTbCap = tonumber(redis.call('HGET', groupHashKey, 'tbCapacity')) or 0\n if oldTbCap > 0 then\n redis.call('HDEL', groupHashKey, 'tbCapacity', 'tbRefillRate', 'tbTokens', 'tbLastRefill', 'tbRefillRemainder')\n end\n end\n end\n local hashFields = {\n 'id', jobIdStr,\n 'name', jobName,\n 'data', jobData,\n 'opts', jobOpts,\n 'timestamp', tostring(timestamp),\n 'attemptsMade', '0',\n 'delay', tostring(delay),\n 'priority', tostring(priority),\n 'maxAttempts', tostring(maxAttempts)\n }\n if useGroupConcurrency then\n hashFields[#hashFields + 1] = 'groupKey'\n hashFields[#hashFields + 1] = orderingKey\n elseif orderingKey ~= '' then\n hashFields[#hashFields + 1] = 'orderingKey'\n hashFields[#hashFields + 1] = orderingKey\n hashFields[#hashFields + 1] = 'orderingSeq'\n hashFields[#hashFields + 1] = tostring(orderingSeq)\n end\n if jobCost > 0 then\n hashFields[#hashFields + 1] = 'cost'\n hashFields[#hashFields + 1] = tostring(jobCost)\n end\n if parentId ~= '' then\n hashFields[#hashFields + 1] = 'parentId'\n hashFields[#hashFields + 1] = parentId\n end\n if delay > 0 or priority > 0 then\n hashFields[#hashFields + 1] = 'state'\n hashFields[#hashFields + 1] = delay > 0 and 'delayed' or 'prioritized'\n else\n hashFields[#hashFields + 1] = 'state'\n hashFields[#hashFields + 1] = 'waiting'\n end\n redis.call('HSET', jobKey, unpack(hashFields))\n if delay > 0 then\n local score = priority * PRIORITY_SHIFT + (timestamp + delay)\n redis.call('ZADD', scheduledKey, score, jobIdStr)\n elseif priority > 0 then\n local score = priority * PRIORITY_SHIFT\n redis.call('ZADD', scheduledKey, score, jobIdStr)\n else\n redis.call('XADD', streamKey, '*', 'jobId', jobIdStr)\n end\n emitEvent(eventsKey, 'added', jobIdStr, {'name', jobName})\n return jobIdStr\nend)\n\nredis.register_function('glidemq_promote', function(keys, args)\n local scheduledKey = keys[1]\n local streamKey = keys[2]\n local eventsKey = keys[3]\n local now = tonumber(args[1])\n local MAX_PROMOTIONS = 1000\n local count = 0\n local cursorMin = 0\n while count < MAX_PROMOTIONS do\n local nextEntry = redis.call('ZRANGEBYSCORE', scheduledKey, string.format('%.0f', cursorMin), '+inf', 'WITHSCORES', 'LIMIT', 0, 1)\n if not nextEntry or #nextEntry == 0 then\n break\n end\n local firstScore = tonumber(nextEntry[2]) or 0\n local priority = math.floor(firstScore / PRIORITY_SHIFT)\n local minScore = priority * PRIORITY_SHIFT\n local maxDueScore = minScore + now\n local remaining = MAX_PROMOTIONS - count\n local members = redis.call(\n 'ZRANGEBYSCORE',\n scheduledKey,\n string.format('%.0f', minScore),\n string.format('%.0f', maxDueScore),\n 'LIMIT',\n 0,\n remaining\n )\n for i = 1, #members do\n local jobId = members[i]\n redis.call('XADD', streamKey, '*', 'jobId', jobId)\n redis.call('ZREM', scheduledKey, jobId)\n local prefix = string.sub(scheduledKey, 1, #scheduledKey - 9)\n local jobKey = prefix .. 'job:' .. jobId\n redis.call('HSET', jobKey, 'state', 'waiting')\n emitEvent(eventsKey, 'promoted', jobId, nil)\n count = count + 1\n end\n cursorMin = (priority + 1) * PRIORITY_SHIFT\n end\n return count\nend)\n\nredis.register_function('glidemq_complete', function(keys, args)\n local streamKey = keys[1]\n local completedKey = keys[2]\n local eventsKey = keys[3]\n local jobKey = keys[4]\n local jobId = args[1]\n local entryId = args[2]\n local returnvalue = args[3]\n local timestamp = tonumber(args[4])\n local group = args[5]\n local removeMode = args[6] or '0'\n local removeCount = tonumber(args[7]) or 0\n local removeAge = tonumber(args[8]) or 0\n local depsMember = args[9] or ''\n local parentId = args[10] or ''\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n redis.call('ZADD', completedKey, timestamp, jobId)\n redis.call('HSET', jobKey,\n 'state', 'completed',\n 'returnvalue', returnvalue,\n 'finishedOn', tostring(timestamp)\n )\n markOrderingDone(jobKey, jobId)\n releaseGroupSlotAndPromote(jobKey, jobId, timestamp)\n emitEvent(eventsKey, 'completed', jobId, {'returnvalue', returnvalue})\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n if removeMode == 'true' then\n redis.call('ZREM', completedKey, jobId)\n redis.call('DEL', jobKey)\n elseif removeMode == 'count' and removeCount > 0 then\n local total = redis.call('ZCARD', completedKey)\n if total > removeCount then\n local excess = redis.call('ZRANGE', completedKey, 0, total - removeCount - 1)\n for i = 1, #excess do\n local oldId = excess[i]\n redis.call('DEL', prefix .. 'job:' .. oldId)\n redis.call('ZREM', completedKey, oldId)\n end\n end\n elseif removeMode == 'age_count' then\n if removeAge > 0 then\n local cutoff = timestamp - (removeAge * 1000)\n local old = redis.call('ZRANGEBYSCORE', completedKey, '0', tostring(cutoff))\n for i = 1, #old do\n local oldId = old[i]\n redis.call('DEL', prefix .. 'job:' .. oldId)\n redis.call('ZREM', completedKey, oldId)\n end\n end\n if removeCount > 0 then\n local total = redis.call('ZCARD', completedKey)\n if total > removeCount then\n local excess = redis.call('ZRANGE', completedKey, 0, total - removeCount - 1)\n for i = 1, #excess do\n local oldId = excess[i]\n redis.call('DEL', prefix .. 'job:' .. oldId)\n redis.call('ZREM', completedKey, oldId)\n end\n end\n end\n end\n if depsMember ~= '' and parentId ~= '' and #keys >= 8 then\n local parentDepsKey = keys[5]\n local parentJobKey = keys[6]\n local parentStreamKey = keys[7]\n local parentEventsKey = keys[8]\n local doneCount = redis.call('HINCRBY', parentJobKey, 'depsCompleted', 1)\n local totalDeps = redis.call('SCARD', parentDepsKey)\n local remaining = totalDeps - doneCount\n if remaining <= 0 then\n redis.call('HSET', parentJobKey, 'state', 'waiting')\n redis.call('XADD', parentStreamKey, '*', 'jobId', parentId)\n emitEvent(parentEventsKey, 'active', parentId, nil)\n end\n end\n return 1\nend)\n\nredis.register_function('glidemq_completeAndFetchNext', function(keys, args)\n local streamKey = keys[1]\n local completedKey = keys[2]\n local eventsKey = keys[3]\n local jobKey = keys[4]\n local jobId = args[1]\n local entryId = args[2]\n local returnvalue = args[3]\n local timestamp = tonumber(args[4])\n local group = args[5]\n local consumer = args[6]\n local removeMode = args[7] or '0'\n local removeCount = tonumber(args[8]) or 0\n local removeAge = tonumber(args[9]) or 0\n local depsMember = args[10] or ''\n local parentId = args[11] or ''\n\n -- Phase 1: Complete current job (same as glidemq_complete)\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n redis.call('ZADD', completedKey, timestamp, jobId)\n redis.call('HSET', jobKey,\n 'state', 'completed',\n 'returnvalue', returnvalue,\n 'finishedOn', tostring(timestamp)\n )\n markOrderingDone(jobKey, jobId)\n releaseGroupSlotAndPromote(jobKey, jobId, timestamp)\n emitEvent(eventsKey, 'completed', jobId, {'returnvalue', returnvalue})\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n\n -- Retention cleanup\n if removeMode == 'true' then\n redis.call('ZREM', completedKey, jobId)\n redis.call('DEL', jobKey)\n elseif removeMode == 'count' and removeCount > 0 then\n local total = redis.call('ZCARD', completedKey)\n if total > removeCount then\n local excess = redis.call('ZRANGE', completedKey, 0, total - removeCount - 1)\n for i = 1, #excess do\n redis.call('DEL', prefix .. 'job:' .. excess[i])\n redis.call('ZREM', completedKey, excess[i])\n end\n end\n end\n\n -- Parent deps\n if depsMember ~= '' and parentId ~= '' and #keys >= 8 then\n local parentDepsKey = keys[5]\n local parentJobKey = keys[6]\n local parentStreamKey = keys[7]\n local parentEventsKey = keys[8]\n local doneCount = redis.call('HINCRBY', parentJobKey, 'depsCompleted', 1)\n local totalDeps = redis.call('SCARD', parentDepsKey)\n if totalDeps - doneCount <= 0 then\n redis.call('HSET', parentJobKey, 'state', 'waiting')\n redis.call('XADD', parentStreamKey, '*', 'jobId', parentId)\n emitEvent(parentEventsKey, 'active', parentId, nil)\n end\n end\n\n -- Phase 2: Fetch next job (non-blocking XREADGROUP)\n local nextEntries = redis.call('XREADGROUP', 'GROUP', group, consumer, 'COUNT', 1, 'STREAMS', streamKey, '>')\n if not nextEntries or #nextEntries == 0 then\n return cjson.encode({completed = jobId, next = false})\n end\n local streamData = nextEntries[1]\n local entries = streamData[2]\n if not entries or #entries == 0 then\n return cjson.encode({completed = jobId, next = false})\n end\n local nextEntry = entries[1]\n local nextEntryId = nextEntry[1]\n local nextFields = nextEntry[2]\n local nextJobId = nil\n for i = 1, #nextFields, 2 do\n if nextFields[i] == 'jobId' then\n nextJobId = nextFields[i + 1]\n break\n end\n end\n if not nextJobId then\n return cjson.encode({completed = jobId, next = false})\n end\n\n -- Phase 3: Activate next job (same as moveToActive)\n local nextJobKey = prefix .. 'job:' .. nextJobId\n local nextExists = redis.call('EXISTS', nextJobKey)\n if nextExists == 0 then\n return cjson.encode({completed = jobId, next = false, nextEntryId = nextEntryId})\n end\n local revoked = redis.call('HGET', nextJobKey, 'revoked')\n if revoked == '1' then\n return cjson.encode({completed = jobId, next = 'REVOKED', nextJobId = nextJobId, nextEntryId = nextEntryId})\n end\n local nextGroupKey = redis.call('HGET', nextJobKey, 'groupKey')\n if nextGroupKey and nextGroupKey ~= '' then\n local nextGroupHashKey = prefix .. 'group:' .. nextGroupKey\n -- Load all group fields in one call\n local nGrpFields = redis.call('HGETALL', nextGroupHashKey)\n local nGrp = {}\n for nf = 1, #nGrpFields, 2 do nGrp[nGrpFields[nf]] = nGrpFields[nf + 1] end\n local nextMaxConc = tonumber(nGrp.maxConcurrency) or 0\n local nextActive = tonumber(nGrp.active) or 0\n -- Concurrency gate first (avoids burning rate/token slots on parked jobs)\n if nextMaxConc > 0 and nextActive >= nextMaxConc then\n redis.call('XACK', streamKey, group, nextEntryId)\n redis.call('XDEL', streamKey, nextEntryId)\n local nextWaitListKey = prefix .. 'groupq:' .. nextGroupKey\n redis.call('RPUSH', nextWaitListKey, nextJobId)\n redis.call('HSET', nextJobKey, 'state', 'group-waiting')\n return cjson.encode({completed = jobId, next = false})\n end\n -- Token bucket gate (read-only)\n local nextTbCapacity = tonumber(nGrp.tbCapacity) or 0\n local nextTbBlocked = false\n local nextTbDelay = 0\n local nextTbTokens = 0\n local nextJobCostVal = 0\n if nextTbCapacity > 0 then\n nextTbTokens = tbRefill(nextGroupHashKey, nGrp, tonumber(timestamp))\n nextJobCostVal = tonumber(redis.call('HGET', nextJobKey, 'cost')) or 1000\n -- DLQ guard: cost > capacity\n if nextJobCostVal > nextTbCapacity then\n redis.call('XACK', streamKey, group, nextEntryId)\n redis.call('XDEL', streamKey, nextEntryId)\n redis.call('ZADD', prefix .. 'failed', tonumber(timestamp), nextJobId)\n redis.call('HSET', nextJobKey,\n 'state', 'failed',\n 'failedReason', 'cost exceeds token bucket capacity',\n 'finishedOn', tostring(timestamp))\n emitEvent(prefix .. 'events', 'failed', nextJobId, {'failedReason', 'cost exceeds token bucket capacity'})\n return cjson.encode({completed = jobId, next = false})\n end\n if nextTbTokens < nextJobCostVal then\n nextTbBlocked = true\n local nextTbRefillRateVal = math.max(tonumber(nGrp.tbRefillRate) or 0, 1)\n nextTbDelay = math.ceil((nextJobCostVal - nextTbTokens) * 1000 / nextTbRefillRateVal)\n end\n end\n -- Sliding window gate (read-only)\n local nextRateMax = tonumber(nGrp.rateMax) or 0\n local nextRlBlocked = false\n local nextRlDelay = 0\n if nextRateMax > 0 then\n local nextRateDuration = tonumber(nGrp.rateDuration) or 0\n local nextRateWindowStart = tonumber(nGrp.rateWindowStart) or 0\n local nextRateCount = tonumber(nGrp.rateCount) or 0\n if nextRateDuration > 0 and timestamp - nextRateWindowStart < nextRateDuration and nextRateCount >= nextRateMax then\n nextRlBlocked = true\n nextRlDelay = (nextRateWindowStart + nextRateDuration) - timestamp\n end\n end\n -- If ANY gate blocked: park + register\n if nextTbBlocked or nextRlBlocked then\n redis.call('XACK', streamKey, group, nextEntryId)\n redis.call('XDEL', streamKey, nextEntryId)\n local nextWaitListKey = prefix .. 'groupq:' .. nextGroupKey\n redis.call('RPUSH', nextWaitListKey, nextJobId)\n redis.call('HSET', nextJobKey, 'state', 'group-waiting')\n local nextMaxDelay = math.max(nextTbDelay, nextRlDelay)\n local rateLimitedKey = prefix .. 'ratelimited'\n redis.call('ZADD', rateLimitedKey, tonumber(timestamp) + nextMaxDelay, nextGroupKey)\n return cjson.encode({completed = jobId, next = false})\n end\n -- All gates passed: mutate state\n if nextTbCapacity > 0 then\n redis.call('HINCRBY', nextGroupHashKey, 'tbTokens', -nextJobCostVal)\n end\n if nextRateMax > 0 then\n local nextRateDuration = tonumber(nGrp.rateDuration) or 0\n if nextRateDuration > 0 then\n local nextRateWindowStart = tonumber(nGrp.rateWindowStart) or 0\n if timestamp - nextRateWindowStart >= nextRateDuration then\n redis.call('HSET', nextGroupHashKey, 'rateWindowStart', tostring(timestamp), 'rateCount', '1')\n else\n redis.call('HINCRBY', nextGroupHashKey, 'rateCount', 1)\n end\n end\n end\n redis.call('HINCRBY', nextGroupHashKey, 'active', 1)\n end\n redis.call('HSET', nextJobKey, 'state', 'active', 'processedOn', tostring(timestamp), 'lastActive', tostring(timestamp))\n local nextHash = redis.call('HGETALL', nextJobKey)\n return cjson.encode({completed = jobId, next = nextHash, nextJobId = nextJobId, nextEntryId = nextEntryId})\nend)\n\nredis.register_function('glidemq_fail', function(keys, args)\n local streamKey = keys[1]\n local failedKey = keys[2]\n local scheduledKey = keys[3]\n local eventsKey = keys[4]\n local jobKey = keys[5]\n local jobId = args[1]\n local entryId = args[2]\n local failedReason = args[3]\n local timestamp = tonumber(args[4])\n local maxAttempts = tonumber(args[5]) or 0\n local backoffDelay = tonumber(args[6]) or 0\n local group = args[7]\n local removeMode = args[8] or '0'\n local removeCount = tonumber(args[9]) or 0\n local removeAge = tonumber(args[10]) or 0\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n local attemptsMade = redis.call('HINCRBY', jobKey, 'attemptsMade', 1)\n if maxAttempts > 0 and attemptsMade < maxAttempts then\n local retryAt = timestamp + backoffDelay\n local priority = tonumber(redis.call('HGET', jobKey, 'priority')) or 0\n local score = priority * PRIORITY_SHIFT + retryAt\n redis.call('ZADD', scheduledKey, score, jobId)\n redis.call('HSET', jobKey,\n 'state', 'delayed',\n 'failedReason', failedReason,\n 'processedOn', tostring(timestamp)\n )\n releaseGroupSlotAndPromote(jobKey, jobId, timestamp)\n emitEvent(eventsKey, 'retrying', jobId, {\n 'failedReason', failedReason,\n 'attemptsMade', tostring(attemptsMade),\n 'delay', tostring(backoffDelay)\n })\n return 'retrying'\n else\n redis.call('ZADD', failedKey, timestamp, jobId)\n redis.call('HSET', jobKey,\n 'state', 'failed',\n 'failedReason', failedReason,\n 'finishedOn', tostring(timestamp),\n 'processedOn', tostring(timestamp)\n )\n markOrderingDone(jobKey, jobId)\n releaseGroupSlotAndPromote(jobKey, jobId, timestamp)\n emitEvent(eventsKey, 'failed', jobId, {'failedReason', failedReason})\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n if removeMode == 'true' then\n redis.call('ZREM', failedKey, jobId)\n redis.call('DEL', jobKey)\n elseif removeMode == 'count' and removeCount > 0 then\n local total = redis.call('ZCARD', failedKey)\n if total > removeCount then\n local excess = redis.call('ZRANGE', failedKey, 0, total - removeCount - 1)\n for i = 1, #excess do\n local oldId = excess[i]\n redis.call('DEL', prefix .. 'job:' .. oldId)\n redis.call('ZREM', failedKey, oldId)\n end\n end\n elseif removeMode == 'age_count' then\n if removeAge > 0 then\n local cutoff = timestamp - (removeAge * 1000)\n local old = redis.call('ZRANGEBYSCORE', failedKey, '0', tostring(cutoff))\n for i = 1, #old do\n local oldId = old[i]\n redis.call('DEL', prefix .. 'job:' .. oldId)\n redis.call('ZREM', failedKey, oldId)\n end\n end\n if removeCount > 0 then\n local total = redis.call('ZCARD', failedKey)\n if total > removeCount then\n local excess = redis.call('ZRANGE', failedKey, 0, total - removeCount - 1)\n for i = 1, #excess do\n local oldId = excess[i]\n redis.call('DEL', prefix .. 'job:' .. oldId)\n redis.call('ZREM', failedKey, oldId)\n end\n end\n end\n end\n return 'failed'\n end\nend)\n\nredis.register_function('glidemq_reclaimStalled', function(keys, args)\n local streamKey = keys[1]\n local eventsKey = keys[2]\n local group = args[1]\n local consumer = args[2]\n local minIdleMs = tonumber(args[3])\n local maxStalledCount = tonumber(args[4]) or 1\n local timestamp = tonumber(args[5])\n local failedKey = args[6]\n local result = redis.call('XAUTOCLAIM', streamKey, group, consumer, minIdleMs, '0-0')\n local entries = result[2]\n if not entries or #entries == 0 then\n return 0\n end\n local prefix = string.sub(streamKey, 1, #streamKey - 6)\n local count = 0\n for i = 1, #entries do\n local entry = entries[i]\n local entryId = entry[1]\n local fields = entry[2]\n local jobId = nil\n if type(fields) == 'table' then\n for j = 1, #fields, 2 do\n if fields[j] == 'jobId' then\n jobId = fields[j + 1]\n break\n end\n end\n end\n if jobId then\n local jobKey = prefix .. 'job:' .. jobId\n local lastActive = tonumber(redis.call('HGET', jobKey, 'lastActive'))\n if lastActive and (timestamp - lastActive) < minIdleMs then\n count = count + 1\n else\n local stalledCount = redis.call('HINCRBY', jobKey, 'stalledCount', 1)\n if stalledCount > maxStalledCount then\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n redis.call('ZADD', failedKey, timestamp, jobId)\n redis.call('HSET', jobKey,\n 'state', 'failed',\n 'failedReason', 'job stalled more than maxStalledCount',\n 'finishedOn', tostring(timestamp)\n )\n markOrderingDone(jobKey, jobId)\n releaseGroupSlotAndPromote(jobKey, jobId, timestamp)\n emitEvent(eventsKey, 'failed', jobId, {\n 'failedReason', 'job stalled more than maxStalledCount'\n })\n else\n redis.call('HSET', jobKey, 'state', 'active')\n emitEvent(eventsKey, 'stalled', jobId, nil)\n end\n count = count + 1\n end\n end\n end\n return count\nend)\n\nredis.register_function('glidemq_pause', function(keys, args)\n local metaKey = keys[1]\n local eventsKey = keys[2]\n redis.call('HSET', metaKey, 'paused', '1')\n emitEvent(eventsKey, 'paused', '0', nil)\n return 1\nend)\n\nredis.register_function('glidemq_resume', function(keys, args)\n local metaKey = keys[1]\n local eventsKey = keys[2]\n redis.call('HSET', metaKey, 'paused', '0')\n emitEvent(eventsKey, 'resumed', '0', nil)\n return 1\nend)\n\nredis.register_function('glidemq_dedup', function(keys, args)\n local dedupKey = keys[1]\n local idKey = keys[2]\n local streamKey = keys[3]\n local scheduledKey = keys[4]\n local eventsKey = keys[5]\n local dedupId = args[1]\n local ttlMs = tonumber(args[2]) or 0\n local mode = args[3]\n local jobName = args[4]\n local jobData = args[5]\n local jobOpts = args[6]\n local timestamp = tonumber(args[7])\n local delay = tonumber(args[8]) or 0\n local priority = tonumber(args[9]) or 0\n local parentId = args[10] or ''\n local maxAttempts = tonumber(args[11]) or 0\n local orderingKey = args[12] or ''\n local groupConcurrency = tonumber(args[13]) or 0\n local groupRateMax = tonumber(args[14]) or 0\n local groupRateDuration = tonumber(args[15]) or 0\n local tbCapacity = tonumber(args[16]) or 0\n local tbRefillRate = tonumber(args[17]) or 0\n local jobCost = tonumber(args[18]) or 0\n local prefix = string.sub(idKey, 1, #idKey - 2)\n local existing = redis.call('HGET', dedupKey, dedupId)\n if mode == 'simple' then\n if existing then\n local sep = string.find(existing, ':')\n if sep then\n local existingJobId = string.sub(existing, 1, sep - 1)\n local jobKey = prefix .. 'job:' .. existingJobId\n local state = redis.call('HGET', jobKey, 'state')\n if state and state ~= 'completed' and state ~= 'failed' then\n return 'skipped'\n end\n end\n end\n elseif mode == 'throttle' then\n if existing and ttlMs > 0 then\n local sep = string.find(existing, ':')\n if sep then\n local storedTs = tonumber(string.sub(existing, sep + 1))\n if storedTs and (timestamp - storedTs) < ttlMs then\n return 'skipped'\n end\n end\n end\n elseif mode == 'debounce' then\n if existing then\n local sep = string.find(existing, ':')\n if sep then\n local existingJobId = string.sub(existing, 1, sep - 1)\n local jobKey = prefix .. 'job:' .. existingJobId\n local state = redis.call('HGET', jobKey, 'state')\n if state == 'delayed' or state == 'prioritized' then\n redis.call('ZREM', scheduledKey, existingJobId)\n markOrderingDone(jobKey, existingJobId)\n redis.call('DEL', jobKey)\n emitEvent(eventsKey, 'removed', existingJobId, nil)\n elseif state and state ~= 'completed' and state ~= 'failed' then\n return 'skipped'\n end\n end\n end\n end\n local jobId = redis.call('INCR', idKey)\n local jobIdStr = tostring(jobId)\n local jobKey = prefix .. 'job:' .. jobIdStr\n local useGroupConcurrency = (orderingKey ~= '' and (groupConcurrency > 1 or groupRateMax > 0 or tbCapacity > 0))\n local orderingSeq = 0\n if orderingKey ~= '' and not useGroupConcurrency then\n local orderingMetaKey = prefix .. 'ordering'\n orderingSeq = redis.call('HINCRBY', orderingMetaKey, orderingKey, 1)\n end\n if useGroupConcurrency then\n local groupHashKey = prefix .. 'group:' .. orderingKey\n local curMax = tonumber(redis.call('HGET', groupHashKey, 'maxConcurrency')) or 0\n if curMax ~= groupConcurrency then\n redis.call('HSET', groupHashKey, 'maxConcurrency', tostring(groupConcurrency))\n end\n if curMax == 0 and groupConcurrency <= 1 then\n redis.call('HSET', groupHashKey, 'maxConcurrency', '1')\n end\n if groupRateMax > 0 then\n local curRateMax = tonumber(redis.call('HGET', groupHashKey, 'rateMax')) or 0\n if curRateMax ~= groupRateMax then\n redis.call('HSET', groupHashKey, 'rateMax', tostring(groupRateMax))\n end\n local curRateDuration = tonumber(redis.call('HGET', groupHashKey, 'rateDuration')) or 0\n if curRateDuration ~= groupRateDuration then\n redis.call('HSET', groupHashKey, 'rateDuration', tostring(groupRateDuration))\n end\n else\n local oldRateMax = tonumber(redis.call('HGET', groupHashKey, 'rateMax')) or 0\n if oldRateMax > 0 then\n redis.call('HDEL', groupHashKey, 'rateMax', 'rateDuration', 'rateWindowStart', 'rateCount')\n end\n end\n -- Upsert token bucket fields on group hash\n if tbCapacity > 0 then\n local curTbCap = tonumber(redis.call('HGET', groupHashKey, 'tbCapacity')) or 0\n if curTbCap ~= tbCapacity then\n redis.call('HSET', groupHashKey, 'tbCapacity', tostring(tbCapacity))\n end\n local curTbRate = tonumber(redis.call('HGET', groupHashKey, 'tbRefillRate')) or 0\n if curTbRate ~= tbRefillRate then\n redis.call('HSET', groupHashKey, 'tbRefillRate', tostring(tbRefillRate))\n end\n -- Initialize tokens on first setup\n if curTbCap == 0 then\n redis.call('HSET', groupHashKey,\n 'tbTokens', tostring(tbCapacity),\n 'tbLastRefill', tostring(timestamp),\n 'tbRefillRemainder', '0')\n end\n -- Validate cost <= capacity at enqueue\n -- Validate cost (explicit or default 1000 millitokens) against capacity\n local effectiveCost = (jobCost > 0) and jobCost or 1000\n if effectiveCost > tbCapacity then\n return 'ERR:COST_EXCEEDS_CAPACITY'\n end\n else\n -- Clear stale tb fields\n local oldTbCap = tonumber(redis.call('HGET', groupHashKey, 'tbCapacity')) or 0\n if oldTbCap > 0 then\n redis.call('HDEL', groupHashKey, 'tbCapacity', 'tbRefillRate', 'tbTokens', 'tbLastRefill', 'tbRefillRemainder')\n end\n end\n end\n local hashFields = {\n 'id', jobIdStr,\n 'name', jobName,\n 'data', jobData,\n 'opts', jobOpts,\n 'timestamp', tostring(timestamp),\n 'attemptsMade', '0',\n 'delay', tostring(delay),\n 'priority', tostring(priority),\n 'maxAttempts', tostring(maxAttempts)\n }\n if useGroupConcurrency then\n hashFields[#hashFields + 1] = 'groupKey'\n hashFields[#hashFields + 1] = orderingKey\n elseif orderingKey ~= '' then\n hashFields[#hashFields + 1] = 'orderingKey'\n hashFields[#hashFields + 1] = orderingKey\n hashFields[#hashFields + 1] = 'orderingSeq'\n hashFields[#hashFields + 1] = tostring(orderingSeq)\n end\n if jobCost > 0 then\n hashFields[#hashFields + 1] = 'cost'\n hashFields[#hashFields + 1] = tostring(jobCost)\n end\n if parentId ~= '' then\n hashFields[#hashFields + 1] = 'parentId'\n hashFields[#hashFields + 1] = parentId\n end\n if delay > 0 or priority > 0 then\n hashFields[#hashFields + 1] = 'state'\n hashFields[#hashFields + 1] = delay > 0 and 'delayed' or 'prioritized'\n else\n hashFields[#hashFields + 1] = 'state'\n hashFields[#hashFields + 1] = 'waiting'\n end\n redis.call('HSET', jobKey, unpack(hashFields))\n if delay > 0 then\n local score = priority * PRIORITY_SHIFT + (timestamp + delay)\n redis.call('ZADD', scheduledKey, score, jobIdStr)\n elseif priority > 0 then\n local score = priority * PRIORITY_SHIFT\n redis.call('ZADD', scheduledKey, score, jobIdStr)\n else\n redis.call('XADD', streamKey, '*', 'jobId', jobIdStr)\n end\n redis.call('HSET', dedupKey, dedupId, jobIdStr .. ':' .. tostring(timestamp))\n emitEvent(eventsKey, 'added', jobIdStr, {'name', jobName})\n return jobIdStr\nend)\n\nredis.register_function('glidemq_rateLimit', function(keys, args)\n local rateKey = keys[1]\n local metaKey = keys[2]\n local maxPerWindow = tonumber(args[1])\n local windowDuration = tonumber(args[2])\n local now = tonumber(args[3])\n -- Fallback: read rate limit config from meta if not provided inline\n if maxPerWindow <= 0 then\n maxPerWindow = tonumber(redis.call('HGET', metaKey, 'rateLimitMax')) or 0\n windowDuration = tonumber(redis.call('HGET', metaKey, 'rateLimitDuration')) or 0\n if maxPerWindow <= 0 then return 0 end\n end\n local windowStart = tonumber(redis.call('HGET', rateKey, 'windowStart')) or 0\n local count = tonumber(redis.call('HGET', rateKey, 'count')) or 0\n if now - windowStart >= windowDuration then\n redis.call('HSET', rateKey, 'windowStart', tostring(now), 'count', '1')\n return 0\n end\n if count >= maxPerWindow then\n local delayMs = windowDuration - (now - windowStart)\n return delayMs\n end\n redis.call('HSET', rateKey, 'count', tostring(count + 1))\n return 0\nend)\n\nredis.register_function('glidemq_promoteRateLimited', function(keys, args)\n local rateLimitedKey = keys[1]\n local streamKey = keys[2]\n local now = tonumber(args[1])\n -- Derive prefix from the server-validated key instead of caller-supplied arg\n local prefix = string.sub(rateLimitedKey, 1, #rateLimitedKey - #'ratelimited')\n local expired = redis.call('ZRANGEBYSCORE', rateLimitedKey, '0', string.format('%.0f', now), 'LIMIT', 0, 100)\n if not expired or #expired == 0 then return 0 end\n local promoted = 0\n for i = 1, #expired do\n local gk = expired[i]\n redis.call('ZREM', rateLimitedKey, gk)\n local groupHashKey = prefix .. 'group:' .. gk\n local waitListKey = prefix .. 'groupq:' .. gk\n -- Load all group fields in one call for rate limit + token bucket checks\n local prGrpFields = redis.call('HGETALL', groupHashKey)\n local prGrp = {}\n for pf = 1, #prGrpFields, 2 do prGrp[prGrpFields[pf]] = prGrpFields[pf + 1] end\n local rateMax = tonumber(prGrp.rateMax) or 0\n local maxConc = tonumber(prGrp.maxConcurrency) or 0\n local active = tonumber(prGrp.active) or 0\n -- Token bucket pre-check: peek head job cost before promoting\n local prTbCap = tonumber(prGrp.tbCapacity) or 0\n local tbCheckPassed = true\n if prTbCap > 0 then\n local prTbTokens = tbRefill(groupHashKey, prGrp, now)\n local headJobId = redis.call('LINDEX', waitListKey, 0)\n if headJobId then\n local headJobKey = prefix .. 'job:' .. headJobId\n -- Tombstone guard\n if redis.call('EXISTS', headJobKey) == 0 then\n redis.call('LPOP', waitListKey)\n tbCheckPassed = false\n end\n if tbCheckPassed then\n local headCost = tonumber(redis.call('HGET', headJobKey, 'cost')) or 1000\n -- DLQ guard: cost > capacity\n if headCost > prTbCap then\n redis.call('LPOP', waitListKey)\n redis.call('ZADD', prefix .. 'failed', now, headJobId)\n redis.call('HSET', headJobKey,\n 'state', 'failed',\n 'failedReason', 'cost exceeds token bucket capacity',\n 'finishedOn', tostring(now))\n emitEvent(prefix .. 'events', 'failed', headJobId, {'failedReason', 'cost exceeds token bucket capacity'})\n tbCheckPassed = false\n end\n if tbCheckPassed and prTbTokens < headCost then\n -- Not enough tokens: re-register with calculated delay\n local prTbRate = math.max(tonumber(prGrp.tbRefillRate) or 0, 1)\n local prTbDelay = math.ceil((headCost - prTbTokens) * 1000 / prTbRate)\n redis.call('ZADD', rateLimitedKey, now + prTbDelay, gk)\n tbCheckPassed = false\n end\n end\n end\n end\n if tbCheckPassed then\n -- Promote up to min(rateMax, available concurrency) jobs.\n -- Do NOT touch rateCount/rateWindowStart here - moveToActive handles\n -- window reset and counting when the worker picks up the promoted jobs.\n local canPromote = 1000\n if rateMax > 0 then\n canPromote = math.min(canPromote, rateMax)\n end\n if maxConc > 0 then\n canPromote = math.min(canPromote, math.max(0, maxConc - active))\n end\n for j = 1, canPromote do\n local nextJobId = redis.call('LPOP', waitListKey)\n if not nextJobId then break end\n redis.call('XADD', streamKey, '*', 'jobId', nextJobId)\n local nextJobKey = prefix .. 'job:' .. nextJobId\n redis.call('HSET', nextJobKey, 'state', 'waiting')\n promoted = promoted + 1\n end\n end\n end\n return promoted\nend)\n\nredis.register_function('glidemq_checkConcurrency', function(keys, args)\n local metaKey = keys[1]\n local streamKey = keys[2]\n local group = args[1]\n local gc = tonumber(redis.call('HGET', metaKey, 'globalConcurrency')) or 0\n if gc <= 0 then\n return -1\n end\n local pending = redis.call('XPENDING', streamKey, group)\n local pendingCount = tonumber(pending[1]) or 0\n local remaining = gc - pendingCount\n if remaining <= 0 then\n return 0\n end\n return remaining\nend)\n\nredis.register_function('glidemq_moveToActive', function(keys, args)\n local jobKey = keys[1]\n local streamKey = keys[2] or ''\n local timestamp = args[1]\n local entryId = args[2] or ''\n local group = args[3] or ''\n local jobId = args[4] or ''\n local exists = redis.call('EXISTS', jobKey)\n if exists == 0 then\n return ''\n end\n local revoked = redis.call('HGET', jobKey, 'revoked')\n if revoked == '1' then\n return 'REVOKED'\n end\n local groupKey = redis.call('HGET', jobKey, 'groupKey')\n if groupKey and groupKey ~= '' then\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n local groupHashKey = prefix .. 'group:' .. groupKey\n -- Load all group fields in one call\n local grpFields = redis.call('HGETALL', groupHashKey)\n local grp = {}\n for f = 1, #grpFields, 2 do grp[grpFields[f]] = grpFields[f + 1] end\n local maxConc = tonumber(grp.maxConcurrency) or 0\n local active = tonumber(grp.active) or 0\n -- Concurrency gate (checked first to avoid burning rate/token slots on parked jobs)\n if maxConc > 0 and active >= maxConc then\n if streamKey ~= '' and entryId ~= '' and group ~= '' then\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n end\n local waitListKey = prefix .. 'groupq:' .. groupKey\n redis.call('RPUSH', waitListKey, jobId)\n redis.call('HSET', jobKey, 'state', 'group-waiting')\n return 'GROUP_FULL'\n end\n -- Token bucket gate (read-only)\n local tbCapacity = tonumber(grp.tbCapacity) or 0\n local tbBlocked = false\n local tbDelay = 0\n local tbTokens = 0\n local jobCostVal = 0\n if tbCapacity > 0 then\n tbTokens = tbRefill(groupHashKey, grp, tonumber(timestamp))\n jobCostVal = tonumber(redis.call('HGET', jobKey, 'cost')) or 1000\n -- DLQ guard: cost > capacity\n if jobCostVal > tbCapacity then\n if streamKey ~= '' and entryId ~= '' and group ~= '' then\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n end\n redis.call('ZADD', prefix .. 'failed', tonumber(timestamp), jobId)\n redis.call('HSET', jobKey,\n 'state', 'failed',\n 'failedReason', 'cost exceeds token bucket capacity',\n 'finishedOn', timestamp)\n emitEvent(prefix .. 'events', 'failed', jobId, {'failedReason', 'cost exceeds token bucket capacity'})\n return 'ERR:COST_EXCEEDS_CAPACITY'\n end\n if tbTokens < jobCostVal then\n tbBlocked = true\n local tbRefillRateVal = tonumber(grp.tbRefillRate) or 0\n if tbRefillRateVal <= 0 then tbRefillRateVal = 1 end\n tbDelay = math.ceil((jobCostVal - tbTokens) * 1000 / tbRefillRateVal)\n end\n end\n -- Sliding window gate (read-only)\n local rateMax = tonumber(grp.rateMax) or 0\n local rlBlocked = false\n local rlDelay = 0\n if rateMax > 0 then\n local rateDuration = tonumber(grp.rateDuration) or 0\n local rateWindowStart = tonumber(grp.rateWindowStart) or 0\n local rateCount = tonumber(grp.rateCount) or 0\n local now = tonumber(timestamp)\n if rateDuration > 0 and now - rateWindowStart < rateDuration and rateCount >= rateMax then\n rlBlocked = true\n rlDelay = (rateWindowStart + rateDuration) - now\n end\n end\n -- If ANY gate blocked: park + register\n if tbBlocked or rlBlocked then\n if streamKey ~= '' and entryId ~= '' and group ~= '' then\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n end\n local waitListKey = prefix .. 'groupq:' .. groupKey\n redis.call('RPUSH', waitListKey, jobId)\n redis.call('HSET', jobKey, 'state', 'group-waiting')\n local maxDelay = math.max(tbDelay, rlDelay)\n local rateLimitedKey = prefix .. 'ratelimited'\n redis.call('ZADD', rateLimitedKey, tonumber(timestamp) + maxDelay, groupKey)\n if tbBlocked then return 'GROUP_TOKEN_LIMITED' end\n return 'GROUP_RATE_LIMITED'\n end\n -- All gates passed: mutate state\n if tbCapacity > 0 then\n redis.call('HINCRBY', groupHashKey, 'tbTokens', -jobCostVal)\n end\n if rateMax > 0 then\n local rateDuration = tonumber(grp.rateDuration) or 0\n if rateDuration > 0 then\n local rateWindowStart = tonumber(grp.rateWindowStart) or 0\n local now = tonumber(timestamp)\n if now - rateWindowStart >= rateDuration then\n redis.call('HSET', groupHashKey, 'rateWindowStart', tostring(now), 'rateCount', '1')\n else\n redis.call('HINCRBY', groupHashKey, 'rateCount', 1)\n end\n end\n end\n redis.call('HINCRBY', groupHashKey, 'active', 1)\n end\n redis.call('HSET', jobKey, 'state', 'active', 'processedOn', timestamp, 'lastActive', timestamp)\n local fields = redis.call('HGETALL', jobKey)\n return cjson.encode(fields)\nend)\n\nredis.register_function('glidemq_deferActive', function(keys, args)\n local streamKey = keys[1]\n local jobKey = keys[2]\n local jobId = args[1]\n local entryId = args[2]\n local group = args[3]\n local exists = redis.call('EXISTS', jobKey)\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n if exists == 0 then\n return 0\n end\n redis.call('XADD', streamKey, '*', 'jobId', jobId)\n redis.call('HSET', jobKey, 'state', 'waiting')\n return 1\nend)\n\nredis.register_function('glidemq_addFlow', function(keys, args)\n local parentIdKey = keys[1]\n local parentStreamKey = keys[2]\n local parentScheduledKey = keys[3]\n local parentEventsKey = keys[4]\n local parentName = args[1]\n local parentData = args[2]\n local parentOpts = args[3]\n local timestamp = tonumber(args[4])\n local parentDelay = tonumber(args[5]) or 0\n local parentPriority = tonumber(args[6]) or 0\n local parentMaxAttempts = tonumber(args[7]) or 0\n local numChildren = tonumber(args[8])\n local parentJobId = redis.call('INCR', parentIdKey)\n local parentJobIdStr = tostring(parentJobId)\n local parentPrefix = string.sub(parentIdKey, 1, #parentIdKey - 2)\n local parentJobKey = parentPrefix .. 'job:' .. parentJobIdStr\n local depsKey = parentPrefix .. 'deps:' .. parentJobIdStr\n local parentOrderingKey = extractOrderingKeyFromOpts(parentOpts)\n local parentGroupConc = extractGroupConcurrencyFromOpts(parentOpts)\n local parentRateMax, parentRateDuration = extractGroupRateLimitFromOpts(parentOpts)\n local parentTbCapacity, parentTbRefillRate = extractTokenBucketFromOpts(parentOpts)\n local parentCost = extractCostFromOpts(parentOpts)\n local parentUseGroup = (parentOrderingKey ~= '' and (parentGroupConc > 1 or parentRateMax > 0 or parentTbCapacity > 0))\n local parentOrderingSeq = 0\n if parentOrderingKey ~= '' and not parentUseGroup then\n local parentOrderingMetaKey = parentPrefix .. 'ordering'\n parentOrderingSeq = redis.call('HINCRBY', parentOrderingMetaKey, parentOrderingKey, 1)\n end\n local parentHash = {\n 'id', parentJobIdStr,\n 'name', parentName,\n 'data', parentData,\n 'opts', parentOpts,\n 'timestamp', tostring(timestamp),\n 'attemptsMade', '0',\n 'delay', tostring(parentDelay),\n 'priority', tostring(parentPriority),\n 'maxAttempts', tostring(parentMaxAttempts),\n 'state', 'waiting-children'\n }\n if parentUseGroup then\n parentHash[#parentHash + 1] = 'groupKey'\n parentHash[#parentHash + 1] = parentOrderingKey\n local groupHashKey = parentPrefix .. 'group:' .. parentOrderingKey\n redis.call('HSET', groupHashKey, 'maxConcurrency', tostring(parentGroupConc > 1 and parentGroupConc or 1))\n redis.call('HSETNX', groupHashKey, 'active', '0')\n if parentRateMax > 0 then\n redis.call('HSET', groupHashKey, 'rateMax', tostring(parentRateMax))\n redis.call('HSET', groupHashKey, 'rateDuration', tostring(parentRateDuration))\n end\n if parentTbCapacity > 0 then\n if parentCost > 0 and parentCost > parentTbCapacity then\n return 'ERR:COST_EXCEEDS_CAPACITY'\n end\n redis.call('HSET', groupHashKey, 'tbCapacity', tostring(parentTbCapacity), 'tbRefillRate', tostring(parentTbRefillRate))\n redis.call('HSETNX', groupHashKey, 'tbTokens', tostring(parentTbCapacity))\n redis.call('HSETNX', groupHashKey, 'tbLastRefill', tostring(timestamp))\n redis.call('HSETNX', groupHashKey, 'tbRefillRemainder', '0')\n end\n elseif parentOrderingKey ~= '' then\n parentHash[#parentHash + 1] = 'orderingKey'\n parentHash[#parentHash + 1] = parentOrderingKey\n parentHash[#parentHash + 1] = 'orderingSeq'\n parentHash[#parentHash + 1] = tostring(parentOrderingSeq)\n end\n if parentCost > 0 then\n parentHash[#parentHash + 1] = 'cost'\n parentHash[#parentHash + 1] = tostring(parentCost)\n end\n redis.call('HSET', parentJobKey, unpack(parentHash))\n -- Pre-validate all children's cost vs capacity before any child writes\n local childArgOffset = 8\n local childKeyOffset = 4\n for i = 1, numChildren do\n local base = childArgOffset + (i - 1) * 8\n local preChildOpts = args[base + 3]\n local preChildTbCap, _ = extractTokenBucketFromOpts(preChildOpts)\n if preChildTbCap > 0 then\n local preChildCost = extractCostFromOpts(preChildOpts)\n local preEffective = (preChildCost > 0) and preChildCost or 1000\n if preEffective > preChildTbCap then\n return 'ERR:COST_EXCEEDS_CAPACITY'\n end\n end\n end\n local childIds = {}\n for i = 1, numChildren do\n local base = childArgOffset + (i - 1) * 8\n local childName = args[base + 1]\n local childData = args[base + 2]\n local childOpts = args[base + 3]\n local childDelay = tonumber(args[base + 4]) or 0\n local childPriority = tonumber(args[base + 5]) or 0\n local childMaxAttempts = tonumber(args[base + 6]) or 0\n local childQueuePrefix = args[base + 7]\n local childParentQueue = args[base + 8]\n local ckBase = childKeyOffset + (i - 1) * 4\n local childIdKey = keys[ckBase + 1]\n local childStreamKey = keys[ckBase + 2]\n local childScheduledKey = keys[ckBase + 3]\n local childEventsKey = keys[ckBase + 4]\n local childJobId = redis.call('INCR', childIdKey)\n local childJobIdStr = tostring(childJobId)\n local childPrefix = string.sub(childIdKey, 1, #childIdKey - 2)\n local childJobKey = childPrefix .. 'job:' .. childJobIdStr\n local childOrderingKey = extractOrderingKeyFromOpts(childOpts)\n local childGroupConc = extractGroupConcurrencyFromOpts(childOpts)\n local childRateMax, childRateDuration = extractGroupRateLimitFromOpts(childOpts)\n local childTbCapacity, childTbRefillRate = extractTokenBucketFromOpts(childOpts)\n local childCost = extractCostFromOpts(childOpts)\n local childUseGroup = (childOrderingKey ~= '' and (childGroupConc > 1 or childRateMax > 0 or childTbCapacity > 0))\n local childOrderingSeq = 0\n if childOrderingKey ~= '' and not childUseGroup then\n local childOrderingMetaKey = childPrefix .. 'ordering'\n childOrderingSeq = redis.call('HINCRBY', childOrderingMetaKey, childOrderingKey, 1)\n end\n local childHash = {\n 'id', childJobIdStr,\n 'name', childName,\n 'data', childData,\n 'opts', childOpts,\n 'timestamp', tostring(timestamp),\n 'attemptsMade', '0',\n 'delay', tostring(childDelay),\n 'priority', tostring(childPriority),\n 'maxAttempts', tostring(childMaxAttempts),\n 'parentId', parentJobIdStr,\n 'parentQueue', childParentQueue\n }\n if childUseGroup then\n childHash[#childHash + 1] = 'groupKey'\n childHash[#childHash + 1] = childOrderingKey\n local childGroupHashKey = childPrefix .. 'group:' .. childOrderingKey\n redis.call('HSETNX', childGroupHashKey, 'maxConcurrency', tostring(childGroupConc > 1 and childGroupConc or 1))\n redis.call('HSETNX', childGroupHashKey, 'active', '0')\n if childRateMax > 0 then\n redis.call('HSET', childGroupHashKey, 'rateMax', tostring(childRateMax))\n redis.call('HSET', childGroupHashKey, 'rateDuration', tostring(childRateDuration))\n end\n if childTbCapacity > 0 then\n redis.call('HSET', childGroupHashKey, 'tbCapacity', tostring(childTbCapacity), 'tbRefillRate', tostring(childTbRefillRate))\n redis.call('HSETNX', childGroupHashKey, 'tbTokens', tostring(childTbCapacity))\n redis.call('HSETNX', childGroupHashKey, 'tbLastRefill', tostring(timestamp))\n redis.call('HSETNX', childGroupHashKey, 'tbRefillRemainder', '0')\n end\n elseif childOrderingKey ~= '' then\n childHash[#childHash + 1] = 'orderingKey'\n childHash[#childHash + 1] = childOrderingKey\n childHash[#childHash + 1] = 'orderingSeq'\n childHash[#childHash + 1] = tostring(childOrderingSeq)\n end\n if childCost > 0 then\n childHash[#childHash + 1] = 'cost'\n childHash[#childHash + 1] = tostring(childCost)\n end\n if childDelay > 0 or childPriority > 0 then\n childHash[#childHash + 1] = 'state'\n childHash[#childHash + 1] = childDelay > 0 and 'delayed' or 'prioritized'\n else\n childHash[#childHash + 1] = 'state'\n childHash[#childHash + 1] = 'waiting'\n end\n redis.call('HSET', childJobKey, unpack(childHash))\n local depsMember = childQueuePrefix .. ':' .. childJobIdStr\n redis.call('SADD', depsKey, depsMember)\n if childDelay > 0 then\n local score = childPriority * PRIORITY_SHIFT + (timestamp + childDelay)\n redis.call('ZADD', childScheduledKey, score, childJobIdStr)\n elseif childPriority > 0 then\n local score = childPriority * PRIORITY_SHIFT\n redis.call('ZADD', childScheduledKey, score, childJobIdStr)\n else\n redis.call('XADD', childStreamKey, '*', 'jobId', childJobIdStr)\n end\n emitEvent(childEventsKey, 'added', childJobIdStr, {'name', childName})\n childIds[#childIds + 1] = childJobIdStr\n end\n local extraDepsOffset = childArgOffset + numChildren * 8\n local numExtraDeps = tonumber(args[extraDepsOffset + 1]) or 0\n for i = 1, numExtraDeps do\n local extraMember = args[extraDepsOffset + 1 + i]\n redis.call('SADD', depsKey, extraMember)\n end\n emitEvent(parentEventsKey, 'added', parentJobIdStr, {'name', parentName})\n local result = {parentJobIdStr}\n for i = 1, #childIds do\n result[#result + 1] = childIds[i]\n end\n return cjson.encode(result)\nend)\n\nredis.register_function('glidemq_completeChild', function(keys, args)\n local depsKey = keys[1]\n local parentJobKey = keys[2]\n local parentStreamKey = keys[3]\n local parentEventsKey = keys[4]\n local depsMember = args[1]\n local parentId = args[2]\n local doneCount = redis.call('HINCRBY', parentJobKey, 'depsCompleted', 1)\n local totalDeps = redis.call('SCARD', depsKey)\n local remaining = totalDeps - doneCount\n if remaining <= 0 then\n redis.call('HSET', parentJobKey, 'state', 'waiting')\n redis.call('XADD', parentStreamKey, '*', 'jobId', parentId)\n emitEvent(parentEventsKey, 'active', parentId, nil)\n end\n return remaining\nend)\n\nredis.register_function('glidemq_removeJob', function(keys, args)\n local jobKey = keys[1]\n local streamKey = keys[2]\n local scheduledKey = keys[3]\n local completedKey = keys[4]\n local failedKey = keys[5]\n local eventsKey = keys[6]\n local logKey = keys[7]\n local jobId = args[1]\n local exists = redis.call('EXISTS', jobKey)\n if exists == 0 then\n return 0\n end\n local state = redis.call('HGET', jobKey, 'state')\n local groupKey = redis.call('HGET', jobKey, 'groupKey')\n if groupKey and groupKey ~= '' then\n if state == 'active' then\n releaseGroupSlotAndPromote(jobKey, jobId, 0)\n elseif state == 'group-waiting' then\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n local waitListKey = prefix .. 'groupq:' .. groupKey\n redis.call('LREM', waitListKey, 1, jobId)\n end\n end\n redis.call('ZREM', scheduledKey, jobId)\n redis.call('ZREM', completedKey, jobId)\n redis.call('ZREM', failedKey, jobId)\n markOrderingDone(jobKey, jobId)\n redis.call('DEL', jobKey)\n redis.call('DEL', logKey)\n emitEvent(eventsKey, 'removed', jobId, nil)\n return 1\nend)\n\nredis.register_function('glidemq_revoke', function(keys, args)\n local jobKey = keys[1]\n local streamKey = keys[2]\n local scheduledKey = keys[3]\n local failedKey = keys[4]\n local eventsKey = keys[5]\n local jobId = args[1]\n local timestamp = tonumber(args[2])\n local group = args[3]\n local exists = redis.call('EXISTS', jobKey)\n if exists == 0 then\n return 'not_found'\n end\n redis.call('HSET', jobKey, 'revoked', '1')\n local state = redis.call('HGET', jobKey, 'state')\n if state == 'group-waiting' then\n local gk = redis.call('HGET', jobKey, 'groupKey')\n if gk and gk ~= '' then\n local prefix = string.sub(jobKey, 1, #jobKey - #('job:' .. jobId))\n local waitListKey = prefix .. 'groupq:' .. gk\n redis.call('LREM', waitListKey, 1, jobId)\n end\n redis.call('ZADD', failedKey, timestamp, jobId)\n redis.call('HSET', jobKey,\n 'state', 'failed',\n 'failedReason', 'revoked',\n 'finishedOn', tostring(timestamp)\n )\n emitEvent(eventsKey, 'revoked', jobId, nil)\n return 'revoked'\n end\n if state == 'waiting' or state == 'delayed' or state == 'prioritized' then\n redis.call('ZREM', scheduledKey, jobId)\n local entries = redis.call('XRANGE', streamKey, '-', '+')\n for i = 1, #entries do\n local entryId = entries[i][1]\n local fields = entries[i][2]\n for j = 1, #fields, 2 do\n if fields[j] == 'jobId' and fields[j+1] == jobId then\n redis.call('XACK', streamKey, group, entryId)\n redis.call('XDEL', streamKey, entryId)\n break\n end\n end\n end\n redis.call('ZADD', failedKey, timestamp, jobId)\n redis.call('HSET', jobKey,\n 'state', 'failed',\n 'failedReason', 'revoked',\n 'finishedOn', tostring(timestamp)\n )\n markOrderingDone(jobKey, jobId)\n emitEvent(eventsKey, 'revoked', jobId, nil)\n return 'revoked'\n end\n emitEvent(eventsKey, 'revoked', jobId, nil)\n return 'flagged'\nend)\n\nredis.register_function('glidemq_searchByName', function(keys, args)\n local stateKey = keys[1]\n local stateType = args[1]\n local nameFilter = args[2]\n local limit = tonumber(args[3]) or 100\n local prefix = args[4]\n local matched = {}\n if stateType == 'zset' then\n local members = redis.call('ZRANGE', stateKey, 0, -1)\n for i = 1, #members do\n if #matched >= limit then break end\n local jobId = members[i]\n local jobKey = prefix .. 'job:' .. jobId\n local name = redis.call('HGET', jobKey, 'name')\n if name == nameFilter then\n matched[#matched + 1] = jobId\n end\n end\n elseif stateType == 'stream' then\n local entries = redis.call('XRANGE', stateKey, '-', '+')\n for i = 1, #entries do\n if #matched >= limit then break end\n local fields = entries[i][2]\n local jobId = nil\n for j = 1, #fields, 2 do\n if fields[j] == 'jobId' then\n jobId = fields[j + 1]\n break\n end\n end\n if jobId then\n local jobKey = prefix .. 'job:' .. jobId\n local name = redis.call('HGET', jobKey, 'name')\n if name == nameFilter then\n matched[#matched + 1] = jobId\n end\n end\n end\n end\n return matched\nend)\n";
|
|
7
7
|
export type QueueKeys = ReturnType<typeof import('../utils').buildKeys>;
|
|
8
8
|
/**
|
|
9
9
|
* Add a job to the queue atomically.
|
|
10
10
|
* Returns the new job ID (string).
|
|
11
11
|
*/
|
|
12
|
-
export declare function addJob(client: Client, k: QueueKeys, jobName: string, data: string, opts: string, timestamp: number, delay: number, priority: number, parentId: string, maxAttempts: number, orderingKey?: string, groupConcurrency?: number): Promise<string>;
|
|
12
|
+
export declare function addJob(client: Client, k: QueueKeys, jobName: string, data: string, opts: string, timestamp: number, delay: number, priority: number, parentId: string, maxAttempts: number, orderingKey?: string, groupConcurrency?: number, groupRateMax?: number, groupRateDuration?: number, tbCapacity?: number, tbRefillRate?: number, jobCost?: number): Promise<string>;
|
|
13
13
|
/**
|
|
14
14
|
* Add a job with deduplication. Checks the dedup hash and either skips or adds the job.
|
|
15
15
|
* Returns "skipped" if deduplicated, otherwise the new job ID (string).
|
|
16
16
|
*/
|
|
17
|
-
export declare function dedup(client: Client, k: QueueKeys, dedupId: string, ttlMs: number, mode: string, jobName: string, data: string, opts: string, timestamp: number, delay: number, priority: number, parentId: string, maxAttempts: number, orderingKey?: string, groupConcurrency?: number): Promise<string>;
|
|
17
|
+
export declare function dedup(client: Client, k: QueueKeys, dedupId: string, ttlMs: number, mode: string, jobName: string, data: string, opts: string, timestamp: number, delay: number, priority: number, parentId: string, maxAttempts: number, orderingKey?: string, groupConcurrency?: number, groupRateMax?: number, groupRateDuration?: number, tbCapacity?: number, tbRefillRate?: number, jobCost?: number): Promise<string>;
|
|
18
18
|
/**
|
|
19
19
|
* Promote delayed/prioritized jobs whose score <= now from scheduled ZSet to stream.
|
|
20
20
|
* Returns the number of jobs promoted.
|
|
@@ -95,13 +95,23 @@ export declare function checkConcurrency(client: Client, k: QueueKeys, group?: s
|
|
|
95
95
|
* Reads the full job hash, checks revoked flag, sets state=active + processedOn + lastActive.
|
|
96
96
|
* For group-concurrency jobs, checks if the group has capacity. If not, parks the job
|
|
97
97
|
* in the group wait list and returns 'GROUP_FULL'.
|
|
98
|
+
* For rate-limited groups, parks the job and returns 'GROUP_RATE_LIMITED'.
|
|
98
99
|
* Returns:
|
|
99
100
|
* - null if job hash doesn't exist
|
|
100
101
|
* - 'REVOKED' if the job's revoked flag is set
|
|
101
102
|
* - 'GROUP_FULL' if the job's group is at max concurrency (job was parked)
|
|
103
|
+
* - 'GROUP_RATE_LIMITED' if the job's group exceeded its rate limit (job was parked)
|
|
104
|
+
* - 'GROUP_TOKEN_LIMITED' if the job's group has insufficient tokens (job was parked)
|
|
105
|
+
* - 'ERR:COST_EXCEEDS_CAPACITY' if the job cost exceeds token bucket capacity (job was failed)
|
|
102
106
|
* - Record<string, string> with all job fields otherwise
|
|
103
107
|
*/
|
|
104
|
-
export declare function moveToActive(client: Client, k: QueueKeys, jobId: string, timestamp: number, streamKey?: string, entryId?: string, group?: string): Promise<Record<string, string> | 'REVOKED' | 'GROUP_FULL' | null>;
|
|
108
|
+
export declare function moveToActive(client: Client, k: QueueKeys, jobId: string, timestamp: number, streamKey?: string, entryId?: string, group?: string): Promise<Record<string, string> | 'REVOKED' | 'GROUP_FULL' | 'GROUP_RATE_LIMITED' | 'GROUP_TOKEN_LIMITED' | 'ERR:COST_EXCEEDS_CAPACITY' | null>;
|
|
109
|
+
/**
|
|
110
|
+
* Promote rate-limited groups whose window has expired.
|
|
111
|
+
* Moves waiting jobs from the group queue back into the stream.
|
|
112
|
+
* Returns the number of jobs promoted.
|
|
113
|
+
*/
|
|
114
|
+
export declare function promoteRateLimited(client: Client, k: QueueKeys, timestamp: number): Promise<number>;
|
|
105
115
|
/**
|
|
106
116
|
* Defers an active job back to waiting by acknowledging + deleting the current
|
|
107
117
|
* stream entry and re-enqueuing the same jobId to the stream tail.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/functions/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACvC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,eAAO,MAAM,YAAY,YAAY,CAAC;AACtC,eAAO,MAAM,eAAe,OAAO,CAAC;AAGpC,eAAO,MAAM,cAAc,YAAY,CAAC;AAIxC,eAAO,MAAM,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/functions/index.ts"],"names":[],"mappings":"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"}
|