@falcondev-oss/workflow 0.8.8 → 0.8.9
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/dist/index.mjs +2 -2
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -7295,7 +7295,7 @@ var require_parser = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
7295
7295
|
}));
|
|
7296
7296
|
|
|
7297
7297
|
//#endregion
|
|
7298
|
-
//#region node_modules/.pnpm/groupmq@1.1.1-next.
|
|
7298
|
+
//#region node_modules/.pnpm/groupmq@1.1.1-next.2_patch_hash=0f055dcc6e397c02b849be5416debe7d948309b119c1f5282e0cfc6f2a063543_ioredis@5.9.2/node_modules/groupmq/dist/index.js
|
|
7299
7299
|
var import_parser = /* @__PURE__ */ __toESM$1(require_parser(), 1);
|
|
7300
7300
|
const __INLINED_LUA_SCRIPTS__ = {
|
|
7301
7301
|
"change-delay": "-- argv: ns, jobId, newDelayUntil, now\nlocal ns = KEYS[1]\nlocal jobId = ARGV[1]\nlocal newDelayUntil = tonumber(ARGV[2])\nlocal now = tonumber(ARGV[3])\n\nlocal jobKey = ns .. \":job:\" .. jobId\nlocal delayedKey = ns .. \":delayed\"\nlocal readyKey = ns .. \":ready\"\n\n-- Check if job exists\nlocal exists = redis.call(\"EXISTS\", jobKey)\nif exists == 0 then\n return 0\nend\n\nlocal groupId = redis.call(\"HGET\", jobKey, \"groupId\")\nif not groupId then\n return 0\nend\n\nlocal gZ = ns .. \":g:\" .. groupId\n\n-- Update job's delayUntil field\nredis.call(\"HSET\", jobKey, \"delayUntil\", tostring(newDelayUntil))\n\n-- Check if job is currently in delayed set\nlocal inDelayed = redis.call(\"ZSCORE\", delayedKey, jobId)\n\nif newDelayUntil > 0 and newDelayUntil > now then\n -- Job should be delayed\n redis.call(\"HSET\", jobKey, \"status\", \"delayed\")\n if inDelayed then\n -- Update existing delay\n redis.call(\"ZADD\", delayedKey, newDelayUntil, jobId)\n else\n -- Move to delayed\n redis.call(\"ZADD\", delayedKey, newDelayUntil, jobId)\n -- If this is the head job, remove group from ready\n local head = redis.call(\"ZRANGE\", gZ, 0, 0)\n if head and #head > 0 and head[1] == jobId then\n redis.call(\"ZREM\", readyKey, groupId)\n end\n end\nelse\n -- Job should be ready immediately\n redis.call(\"HSET\", jobKey, \"status\", \"waiting\")\n if inDelayed then\n -- Remove from delayed\n redis.call(\"ZREM\", delayedKey, jobId)\n -- If this is the head job, ensure group is in ready\n local head = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if head and #head >= 2 and head[1] == jobId then\n local headScore = tonumber(head[2])\n redis.call(\"ZADD\", readyKey, headScore, groupId)\n end\n end\nend\n\nreturn 1\n\n\n",
|
|
@@ -7325,7 +7325,7 @@ const __INLINED_LUA_SCRIPTS__ = {
|
|
|
7325
7325
|
"record-job-result": "-- Record job completion or failure with retention management\n-- argv: ns, jobId, status ('completed' | 'failed'), timestamp, result/error (JSON), \n-- keepCompleted, keepFailed, processedOn, finishedOn, attempts, maxAttempts\nlocal ns = KEYS[1]\nlocal jobId = ARGV[1]\nlocal status = ARGV[2]\nlocal timestamp = tonumber(ARGV[3])\nlocal resultOrError = ARGV[4]\nlocal keepCompleted = tonumber(ARGV[5])\nlocal keepFailed = tonumber(ARGV[6])\nlocal processedOn = ARGV[7]\nlocal finishedOn = ARGV[8]\nlocal attempts = ARGV[9]\nlocal maxAttempts = ARGV[10]\n\nlocal jobKey = ns .. \":job:\" .. jobId\n\n-- Verify job exists and check current status to prevent race conditions\nlocal currentStatus = redis.call(\"HGET\", jobKey, \"status\")\nif not currentStatus then\n -- Job doesn't exist, likely already cleaned up\n return 0\nend\n\n-- If job is in \"waiting\" state, this might be a late completion after stalled recovery\n-- In this case, we should not overwrite the status or delete the job\nif currentStatus == \"waiting\" then\n -- Job was recovered by stalled check and possibly being processed by another worker\n -- Ignore this late completion to prevent corruption\n return 0\nend\n\nif status == \"completed\" then\n local completedKey = ns .. \":completed\"\n \n if keepCompleted > 0 then\n -- Store job metadata and add to completed set\n redis.call(\"HSET\", jobKey, \n \"status\", \"completed\",\n \"processedOn\", processedOn,\n \"finishedOn\", finishedOn,\n \"attempts\", attempts,\n \"maxAttempts\", maxAttempts,\n \"returnvalue\", resultOrError\n )\n redis.call(\"ZADD\", completedKey, timestamp, jobId)\n -- Ensure idempotence mapping exists\n redis.call(\"SET\", ns .. \":unique:\" .. jobId, jobId)\n \n -- Trim old entries atomically\n local zcount = redis.call(\"ZCARD\", completedKey)\n local toRemove = zcount - keepCompleted\n if toRemove > 0 then\n local oldIds = redis.call(\"ZRANGE\", completedKey, 0, toRemove - 1)\n if #oldIds > 0 then\n redis.call(\"ZREMRANGEBYRANK\", completedKey, 0, toRemove - 1)\n -- Batch delete old jobs and unique keys\n local keysToDelete = {}\n for i = 1, #oldIds do\n local oldId = oldIds[i]\n table.insert(keysToDelete, ns .. \":job:\" .. oldId)\n table.insert(keysToDelete, ns .. \":unique:\" .. oldId)\n end\n if #keysToDelete > 0 then\n redis.call(\"DEL\", unpack(keysToDelete))\n end\n end\n end\n else\n -- keepCompleted == 0: Delete immediately (batch operation)\n redis.call(\"DEL\", jobKey, ns .. \":unique:\" .. jobId)\n end\n \nelseif status == \"failed\" then\n local failedKey = ns .. \":failed\"\n \n -- Parse error info from resultOrError JSON\n -- Expected format: {\"message\":\"...\", \"name\":\"...\", \"stack\":\"...\"}\n local errorInfo = cjson.decode(resultOrError)\n \n if keepFailed > 0 then\n -- Store failure metadata\n redis.call(\"HSET\", jobKey,\n \"status\", \"failed\",\n \"failedReason\", errorInfo.message or \"Error\",\n \"failedName\", errorInfo.name or \"Error\",\n \"stacktrace\", errorInfo.stack or \"\",\n \"processedOn\", processedOn,\n \"finishedOn\", finishedOn,\n \"attempts\", attempts,\n \"maxAttempts\", maxAttempts\n )\n redis.call(\"ZADD\", failedKey, timestamp, jobId)\n \n -- Note: No retention trimming for failed jobs (let clean() handle it)\n else\n -- keepFailed == 0: Delete immediately (batch operation)\n redis.call(\"DEL\", jobKey, ns .. \":unique:\" .. jobId)\n end\nend\n\nreturn 1\n\n",
|
|
7326
7326
|
"remove": "-- argv: ns, jobId\nlocal ns = KEYS[1]\nlocal jobId = ARGV[1]\n\nlocal jobKey = ns .. \":job:\" .. jobId\nlocal delayedKey = ns .. \":delayed\"\nlocal readyKey = ns .. \":ready\"\nlocal processingKey = ns .. \":processing\"\n\n-- If job does not exist, return 0\nif redis.call(\"EXISTS\", jobKey) == 0 then\n return 0\nend\n\nlocal groupId = redis.call(\"HGET\", jobKey, \"groupId\")\n\n-- Remove from delayed and processing structures\nredis.call(\"ZREM\", delayedKey, jobId)\nredis.call(\"DEL\", ns .. \":processing:\" .. jobId)\nredis.call(\"ZREM\", processingKey, jobId)\n\n-- Remove from completed/failed retention sets if present\nredis.call(\"ZREM\", ns .. \":completed\", jobId)\nredis.call(\"ZREM\", ns .. \":failed\", jobId)\n\n-- Delete idempotence mapping\nredis.call(\"DEL\", ns .. \":unique:\" .. jobId)\n\n-- If we have a group, update group zset and ready queue accordingly\nif groupId then\n local gZ = ns .. \":g:\" .. groupId\n redis.call(\"ZREM\", gZ, jobId)\n\n local jobCount = redis.call(\"ZCARD\", gZ)\n if jobCount == 0 then\n redis.call(\"ZREM\", readyKey, groupId)\n -- Clean up empty group\n redis.call(\"DEL\", gZ)\n redis.call(\"SREM\", ns .. \":groups\", groupId)\n else\n local head = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if head and #head >= 2 then\n local headScore = tonumber(head[2])\n redis.call(\"ZADD\", readyKey, headScore, groupId)\n end\n end\nend\n\n-- Finally, delete the job hash\nredis.call(\"DEL\", jobKey)\n\nreturn 1\n\n\n",
|
|
7327
7327
|
"reserve-atomic": "-- Atomic reserve operation that checks lock and reserves in one operation\n-- argv: ns, nowEpochMs, vtMs, targetGroupId, allowedJobId (optional)\nlocal ns = KEYS[1]\nlocal now = tonumber(ARGV[1])\nlocal vt = tonumber(ARGV[2])\nlocal targetGroupId = ARGV[3]\nlocal allowedJobId = ARGV[4] -- If provided, allow reserve if lock matches this job ID\n\nlocal readyKey = ns .. \":ready\"\nlocal gZ = ns .. \":g:\" .. targetGroupId\nlocal groupActiveKey = ns .. \":g:\" .. targetGroupId .. \":active\"\n\n-- Respect paused state\nif redis.call(\"GET\", ns .. \":paused\") then\n return nil\nend\n\n-- BullMQ-style: Check if group has active jobs\nlocal activeCount = redis.call(\"LLEN\", groupActiveKey)\n\nif activeCount > 0 then\n -- If allowedJobId is provided, check if it matches the active job (grace collection)\n if allowedJobId then\n local activeJobId = redis.call(\"LINDEX\", groupActiveKey, 0)\n if activeJobId == allowedJobId then\n -- This is grace collection - we're chaining from the same job\n -- Continue to reserve next job\n else\n -- Different job is active, re-add to ready and return\n local head = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if head and #head >= 2 then\n local headScore = tonumber(head[2])\n redis.call(\"ZADD\", readyKey, headScore, targetGroupId)\n end\n return nil\n end\n else\n -- Group has active job and this isn't grace collection, can't proceed\n local head = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if head and #head >= 2 then\n local headScore = tonumber(head[2])\n redis.call(\"ZADD\", readyKey, headScore, targetGroupId)\n end\n return nil\n end\nend\n\n-- Try to get a job from the group\n-- First check if head job is delayed\nlocal head = redis.call(\"ZRANGE\", gZ, 0, 0)\nif not head or #head == 0 then\n return nil\nend\nlocal headJobId = head[1]\nlocal jobKey = ns .. \":job:\" .. headJobId\n\n-- Skip if head job is delayed (will be promoted later)\nlocal jobStatus = redis.call(\"HGET\", jobKey, \"status\")\nif jobStatus == \"delayed\" then\n return nil\nend\n\n-- Pop the job\nlocal zpop = redis.call(\"ZPOPMIN\", gZ, 1)\nif not zpop or #zpop == 0 then\n -- No job available, return\n return nil\nend\nheadJobId = zpop[1]\n\nlocal job = redis.call(\"HMGET\", jobKey, \"id\",\"groupId\",\"data\",\"attempts\",\"maxAttempts\",\"seq\",\"timestamp\",\"orderMs\",\"score\")\nlocal id, groupId, payload, attempts, maxAttempts, seq, enq, orderMs, score = job[1], job[2], job[3], job[4], job[5], job[6], job[7], job[8], job[9]\n\n-- Validate job data exists (handle corrupted/missing job hash)\nif not id or id == false then\n -- Job hash is missing/corrupted, clean up if needed\n if not allowedJobId or activeCount == 0 then\n redis.call(\"LREM\", groupActiveKey, 1, headJobId)\n end\n \n -- Re-add next job to ready queue if exists\n local nextHead = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if nextHead and #nextHead >= 2 then\n local nextScore = tonumber(nextHead[2])\n redis.call(\"ZADD\", readyKey, nextScore, targetGroupId)\n end\n \n return nil\nend\n\n-- BullMQ-style: Push to group active list if not already there (not grace collection)\nif not allowedJobId or activeCount == 0 then\n -- Normal reserve: add this job to active list\n redis.call(\"LPUSH\", groupActiveKey, id)\nend\n-- If this is grace collection and activeCount > 0, the active list already has the job\n\nlocal procKey = ns .. \":processing:\" .. id\nlocal deadline = now + vt\nredis.call(\"HSET\", procKey, \"groupId\", groupId, \"deadlineAt\", tostring(deadline))\n\nlocal processingKey = ns .. \":processing\"\nredis.call(\"ZADD\", processingKey, deadline, id)\n\n-- Mark job as processing for accurate stalled detection and idempotency\nredis.call(\"HSET\", jobKey, \"status\", \"processing\")\n\nlocal nextHead = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\nif nextHead and #nextHead >= 2 then\n local nextScore = tonumber(nextHead[2])\n redis.call(\"ZADD\", readyKey, nextScore, groupId)\nend\n\nreturn id .. \"||GROUPMQ||\" .. groupId .. \"||GROUPMQ||\" .. payload .. \"||GROUPMQ||\" .. attempts .. \"||GROUPMQ||\" .. maxAttempts .. \"||GROUPMQ||\" .. seq .. \"||GROUPMQ||\" .. enq .. \"||GROUPMQ||\" .. orderMs .. \"||GROUPMQ||\" .. score .. \"||GROUPMQ||\" .. deadline\n",
|
|
7328
|
-
"reserve-batch": "-- argv: ns, nowEpochMs, vtMs, maxBatch\nlocal ns = KEYS[1]\nlocal now = tonumber(ARGV[1])\nlocal vt = tonumber(ARGV[2])\nlocal maxBatch = tonumber(ARGV[3]) or 16\n\nlocal readyKey = ns .. \":ready\"\nlocal processingKey = ns .. \":processing\"\n\n-- Early exit if paused\nif redis.call(\"GET\", ns .. \":paused\") then\n return {}\nend\n\nlocal out = {}\n\n-- STALLED JOB RECOVERY WITH THROTTLING\n-- Check for stalled jobs periodically to avoid overhead in hot path\n-- This ensures stalled jobs are recovered even in high-load systems where ready queue is never empty\n-- Check interval is adaptive: 1/4 of jobTimeout (to check 4x during visibility window), max 5s\nlocal stalledCheckKey = ns .. \":stalled:lastcheck\"\nlocal lastCheck = tonumber(redis.call(\"GET\", stalledCheckKey)) or 0\nlocal stalledCheckInterval = math.min(math.floor(vt / 4), 5000)\n\nif (now - lastCheck) >= stalledCheckInterval then\n -- Update last check timestamp\n redis.call(\"SET\", stalledCheckKey, tostring(now))\n \n -- Check for expired jobs and recover them\n local expiredJobs = redis.call(\"ZRANGEBYSCORE\", processingKey, 0, now)\n if #expiredJobs > 0 then\n for _, jobId in ipairs(expiredJobs) do\n local procKey = ns .. \":processing:\" .. jobId\n local procData = redis.call(\"HMGET\", procKey, \"groupId\", \"deadlineAt\")\n local gid = procData[1]\n local deadlineAt = tonumber(procData[2])\n if gid and deadlineAt and now > deadlineAt then\n local jobKey = ns .. \":job:\" .. jobId\n local jobScore = redis.call(\"HGET\", jobKey, \"score\")\n if jobScore then\n local gZ = ns .. \":g:\" .. gid\n redis.call(\"ZADD\", gZ, tonumber(jobScore), jobId)\n local head = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if head and #head >= 2 then\n local headScore = tonumber(head[2])\n redis.call(\"ZADD\", readyKey, headScore, gid)\n end\n redis.call(\"DEL\", ns .. \":lock:\" .. gid)\n redis.call(\"DEL\", procKey)\n redis.call(\"ZREM\", processingKey, jobId)\n end\n end\n end\n end\nend\n\n-- Pop up to maxBatch groups from ready set (lowest score first)\nlocal groups = redis.call(\"ZRANGE\", readyKey, 0, maxBatch - 1, \"WITHSCORES\")\nif not groups or #groups == 0 then\n return {}\nend\n\nlocal processedGroups = {}\n-- BullMQ-style: use per-group active list instead of group locks\nfor i = 1, #groups, 2 do\n local gid = groups[i]\n local gZ = ns .. \":g:\" .. gid\n local groupActiveKey = ns .. \":g:\" .. gid .. \":active\"\n\n -- Check if group has no active jobs (BullMQ-style gating)\n local activeCount = redis.call(\"LLEN\", groupActiveKey)\n if activeCount == 0 then\n local head = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if head and #head >= 2 then\n local headJobId = head[1]\n local headScore = tonumber(head[2])\n local headJobKey = ns .. \":job:\" .. headJobId\n \n -- Skip if head job is delayed (will be promoted later)\n local jobStatus = redis.call(\"HGET\", headJobKey, \"status\")\n if jobStatus ~= \"delayed\" then\n -- Pop the job and push to active list atomically\n local zpop = redis.call(\"ZPOPMIN\", gZ, 1)\n if zpop and #zpop > 0 then\n local jobId = zpop[1]\n \n local jobKey = ns .. \":job:\" .. jobId\n local job = redis.call(\"HMGET\", jobKey, \"id\",\"groupId\",\"data\",\"attempts\",\"maxAttempts\",\"seq\",\"timestamp\",\"orderMs\",\"score\")\n local id, groupId, payload, attempts, maxAttempts, seq, enq, orderMs, score = job[1], job[2], job[3], job[4], job[5], job[6], job[7], job[8], job[9]\n\n -- Validate job data exists (handle corrupted/missing job hash)\n if not id or id == false then\n -- Job hash is missing/corrupted, skip this job and continue\n -- Re-add next job to ready queue if exists\n local nextHead = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if nextHead and #nextHead >= 2 then\n local nextScore = tonumber(nextHead[2])\n redis.call(\"ZADD\", readyKey, nextScore, gid)\n end\n else\n -- Push to group active list (enforces 1-per-group)\n redis.call(\"LPUSH\", groupActiveKey, jobId)\n \n -- Mark job as processing\n redis.call(\"HSET\", jobKey, \"status\", \"processing\")\n \n local procKey = ns .. \":processing:\" .. id\n local deadline = now + vt\n redis.call(\"HSET\", procKey, \"groupId\", gid, \"deadlineAt\", tostring(deadline))\n redis.call(\"ZADD\", processingKey, deadline, id)\n\n -- Re-add group if there is a new head job (next oldest)\n local nextHead = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if nextHead and #nextHead >= 2 then\n local nextScore = tonumber(nextHead[2])\n redis.call(\"ZADD\", readyKey, nextScore, gid)\n end\n\n table.insert(out, id .. \"||GROUPMQ||\" .. groupId .. \"||GROUPMQ||\" .. payload .. \"||GROUPMQ||\" .. attempts .. \"||GROUPMQ||\" .. maxAttempts .. \"||GROUPMQ||\" .. seq .. \"||GROUPMQ||\" .. enq .. \"||GROUPMQ||\" .. orderMs .. \"||GROUPMQ||\" .. score .. \"||GROUPMQ||\" .. deadline)\n table.insert(processedGroups, gid)\n end\n end\n end\n end\n end\n -- Note: Groups with active jobs will be skipped\nend\n\n-- Remove only the groups that were actually processed from ready queue\nfor _, gid in ipairs(processedGroups) do\n redis.call(\"ZREM\", readyKey, gid)\nend\n\nreturn out\n\n\n",
|
|
7328
|
+
"reserve-batch": "-- argv: ns, nowEpochMs, vtMs, maxBatch\nlocal ns = KEYS[1]\nlocal now = tonumber(ARGV[1])\nlocal vt = tonumber(ARGV[2])\nlocal maxBatch = tonumber(ARGV[3]) or 16\n\nlocal readyKey = ns .. \":ready\"\nlocal processingKey = ns .. \":processing\"\n\n-- Early exit if paused\nif redis.call(\"GET\", ns .. \":paused\") then\n return {}\nend\n\nlocal out = {}\n\n-- STALLED JOB RECOVERY WITH THROTTLING\n-- Check for stalled jobs periodically to avoid overhead in hot path\n-- This ensures stalled jobs are recovered even in high-load systems where ready queue is never empty\n-- Check interval is adaptive: 1/4 of jobTimeout (to check 4x during visibility window), max 5s\nlocal stalledCheckKey = ns .. \":stalled:lastcheck\"\nlocal lastCheck = tonumber(redis.call(\"GET\", stalledCheckKey)) or 0\nlocal stalledCheckInterval = math.min(math.floor(vt / 4), 5000)\n\nif (now - lastCheck) >= stalledCheckInterval then\n -- Update last check timestamp\n redis.call(\"SET\", stalledCheckKey, tostring(now))\n \n -- Check for expired jobs and recover them\n local expiredJobs = redis.call(\"ZRANGEBYSCORE\", processingKey, 0, now)\n if #expiredJobs > 0 then\n for _, jobId in ipairs(expiredJobs) do\n local procKey = ns .. \":processing:\" .. jobId\n local procData = redis.call(\"HMGET\", procKey, \"groupId\", \"deadlineAt\")\n local gid = procData[1]\n local deadlineAt = tonumber(procData[2])\n if gid and deadlineAt and now > deadlineAt then\n local jobKey = ns .. \":job:\" .. jobId\n local jobScore = redis.call(\"HGET\", jobKey, \"score\")\n if jobScore then\n local gZ = ns .. \":g:\" .. gid\n redis.call(\"ZADD\", gZ, tonumber(jobScore), jobId)\n local head = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if head and #head >= 2 then\n local headScore = tonumber(head[2])\n redis.call(\"ZADD\", readyKey, headScore, gid)\n end\n -- Remove from group active list (BullMQ-style) - CRITICAL to unblock the group\n local groupActiveKey = ns .. \":g:\" .. gid .. \":active\"\n redis.call(\"LREM\", groupActiveKey, 1, jobId)\n redis.call(\"DEL\", ns .. \":lock:\" .. gid)\n redis.call(\"DEL\", procKey)\n redis.call(\"ZREM\", processingKey, jobId)\n end\n end\n end\n end\nend\n\n-- Pop up to maxBatch groups from ready set (lowest score first)\nlocal groups = redis.call(\"ZRANGE\", readyKey, 0, maxBatch - 1, \"WITHSCORES\")\nif not groups or #groups == 0 then\n return {}\nend\n\nlocal processedGroups = {}\n-- BullMQ-style: use per-group active list instead of group locks\nfor i = 1, #groups, 2 do\n local gid = groups[i]\n local gZ = ns .. \":g:\" .. gid\n local groupActiveKey = ns .. \":g:\" .. gid .. \":active\"\n\n -- Check if group has no active jobs (BullMQ-style gating)\n local activeCount = redis.call(\"LLEN\", groupActiveKey)\n if activeCount == 0 then\n local head = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if head and #head >= 2 then\n local headJobId = head[1]\n local headScore = tonumber(head[2])\n local headJobKey = ns .. \":job:\" .. headJobId\n \n -- Skip if head job is delayed (will be promoted later)\n local jobStatus = redis.call(\"HGET\", headJobKey, \"status\")\n if jobStatus ~= \"delayed\" then\n -- Pop the job and push to active list atomically\n local zpop = redis.call(\"ZPOPMIN\", gZ, 1)\n if zpop and #zpop > 0 then\n local jobId = zpop[1]\n \n local jobKey = ns .. \":job:\" .. jobId\n local job = redis.call(\"HMGET\", jobKey, \"id\",\"groupId\",\"data\",\"attempts\",\"maxAttempts\",\"seq\",\"timestamp\",\"orderMs\",\"score\")\n local id, groupId, payload, attempts, maxAttempts, seq, enq, orderMs, score = job[1], job[2], job[3], job[4], job[5], job[6], job[7], job[8], job[9]\n\n -- Validate job data exists (handle corrupted/missing job hash)\n if not id or id == false then\n -- Job hash is missing/corrupted, skip this job and continue\n -- Re-add next job to ready queue if exists\n local nextHead = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if nextHead and #nextHead >= 2 then\n local nextScore = tonumber(nextHead[2])\n redis.call(\"ZADD\", readyKey, nextScore, gid)\n end\n else\n -- Push to group active list (enforces 1-per-group)\n redis.call(\"LPUSH\", groupActiveKey, jobId)\n \n -- Mark job as processing\n redis.call(\"HSET\", jobKey, \"status\", \"processing\")\n \n local procKey = ns .. \":processing:\" .. id\n local deadline = now + vt\n redis.call(\"HSET\", procKey, \"groupId\", gid, \"deadlineAt\", tostring(deadline))\n redis.call(\"ZADD\", processingKey, deadline, id)\n\n -- Re-add group if there is a new head job (next oldest)\n local nextHead = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if nextHead and #nextHead >= 2 then\n local nextScore = tonumber(nextHead[2])\n redis.call(\"ZADD\", readyKey, nextScore, gid)\n end\n\n table.insert(out, id .. \"||GROUPMQ||\" .. groupId .. \"||GROUPMQ||\" .. payload .. \"||GROUPMQ||\" .. attempts .. \"||GROUPMQ||\" .. maxAttempts .. \"||GROUPMQ||\" .. seq .. \"||GROUPMQ||\" .. enq .. \"||GROUPMQ||\" .. orderMs .. \"||GROUPMQ||\" .. score .. \"||GROUPMQ||\" .. deadline)\n table.insert(processedGroups, gid)\n end\n end\n end\n end\n end\n -- Note: Groups with active jobs will be skipped\nend\n\n-- Remove only the groups that were actually processed from ready queue\nfor _, gid in ipairs(processedGroups) do\n redis.call(\"ZREM\", readyKey, gid)\nend\n\nreturn out\n\n\n",
|
|
7329
7329
|
"reserve": "-- argv: ns, nowEpochMs, vtMs, scanLimit\nlocal ns = KEYS[1]\nlocal now = tonumber(ARGV[1])\nlocal vt = tonumber(ARGV[2])\nlocal scanLimit = tonumber(ARGV[3]) or 20\n\nlocal readyKey = ns .. \":ready\"\n\n-- Respect paused state\nif redis.call(\"GET\", ns .. \":paused\") then\n return nil\nend\n\n-- STALLED JOB RECOVERY WITH THROTTLING\n-- Check for stalled jobs periodically to avoid overhead in hot path\n-- This ensures stalled jobs are recovered even in high-load systems\n-- Check interval is adaptive: 1/4 of jobTimeout (to check 4x during visibility window), max 5s\nlocal processingKey = ns .. \":processing\"\nlocal stalledCheckKey = ns .. \":stalled:lastcheck\"\nlocal lastCheck = tonumber(redis.call(\"GET\", stalledCheckKey)) or 0\nlocal stalledCheckInterval = math.min(math.floor(vt / 4), 5000)\n\nlocal shouldCheckStalled = (now - lastCheck) >= stalledCheckInterval\n\n-- Get available groups\nlocal groups = redis.call(\"ZRANGE\", readyKey, 0, scanLimit - 1, \"WITHSCORES\")\n\n-- Check for stalled jobs if: queue is empty OR it's time for periodic check\nif (not groups or #groups == 0) or shouldCheckStalled then\n if shouldCheckStalled then\n redis.call(\"SET\", stalledCheckKey, tostring(now))\n end\n \n local expiredJobs = redis.call(\"ZRANGEBYSCORE\", processingKey, 0, now)\n for _, jobId in ipairs(expiredJobs) do\n local procKey = ns .. \":processing:\" .. jobId\n local procData = redis.call(\"HMGET\", procKey, \"groupId\", \"deadlineAt\")\n local gid = procData[1]\n local deadlineAt = tonumber(procData[2])\n if gid and deadlineAt and now > deadlineAt then\n local jobKey = ns .. \":job:\" .. jobId\n local jobScore = redis.call(\"HGET\", jobKey, \"score\")\n if jobScore then\n local gZ = ns .. \":g:\" .. gid\n -- CRITICAL: Check if job is already in group set before re-adding\n -- This prevents duplicate entries and ensures we only re-add if truly needed\n local alreadyInGroup = redis.call(\"ZSCORE\", gZ, jobId)\n if not alreadyInGroup then\n redis.call(\"ZADD\", gZ, tonumber(jobScore), jobId)\n end\n -- CRITICAL: Reset status from \"processing\" to \"waiting\" when re-adding expired job\n -- This prevents jobs from being stuck with \"processing\" status in the group set\n redis.call(\"HSET\", jobKey, \"status\", \"waiting\")\n local head = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if head and #head >= 2 then\n local headScore = tonumber(head[2])\n redis.call(\"ZADD\", readyKey, headScore, gid)\n end\n -- Remove from group active list (BullMQ-style) - CRITICAL to unblock the group\n local groupActiveKey = ns .. \":g:\" .. gid .. \":active\"\n redis.call(\"LREM\", groupActiveKey, 1, jobId)\n redis.call(\"DEL\", ns .. \":lock:\" .. gid)\n redis.call(\"DEL\", procKey)\n redis.call(\"ZREM\", processingKey, jobId)\n end\n end\n end\n \n -- Refresh groups after recovery (only if we didn't have any before)\n if not groups or #groups == 0 then\n groups = redis.call(\"ZRANGE\", readyKey, 0, scanLimit - 1, \"WITHSCORES\")\n end\nend\n\nif not groups or #groups == 0 then\n return nil\nend\n\nlocal chosenGid = nil\nlocal chosenIndex = nil\nlocal headJobId = nil\nlocal job = nil\n\n-- Try to atomically acquire a group and its head job\n-- BullMQ-style: use per-group active list instead of group locks\nfor i = 1, #groups, 2 do\n local gid = groups[i]\n local gZ = ns .. \":g:\" .. gid\n local groupActiveKey = ns .. \":g:\" .. gid .. \":active\"\n \n -- Check if group has no active jobs (BullMQ-style gating)\n local activeCount = redis.call(\"LLEN\", groupActiveKey)\n if activeCount == 0 then\n -- Check if group has jobs\n local head = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if head and #head >= 2 then\n local headJobId = head[1]\n local headJobKey = ns .. \":job:\" .. headJobId\n \n -- Skip if head job is delayed (will be promoted later)\n local jobStatus = redis.call(\"HGET\", headJobKey, \"status\")\n if jobStatus ~= \"delayed\" then\n -- Pop the job and push to active list atomically\n local zpop = redis.call(\"ZPOPMIN\", gZ, 1)\n if zpop and #zpop > 0 then\n headJobId = zpop[1]\n -- Read the popped job (use headJobId to avoid races)\n headJobKey = ns .. \":job:\" .. headJobId\n job = redis.call(\"HMGET\", headJobKey, \"id\",\"groupId\",\"data\",\"attempts\",\"maxAttempts\",\"seq\",\"timestamp\",\"orderMs\",\"score\")\n \n -- Push to group active list (enforces 1-per-group)\n redis.call(\"LPUSH\", groupActiveKey, headJobId)\n \n chosenGid = gid\n chosenIndex = (i + 1) / 2 - 1\n -- Mark job as processing for accurate stalled detection and idempotency\n redis.call(\"HSET\", headJobKey, \"status\", \"processing\")\n \n -- CRITICAL: Ensure job is removed from group set (defensive check)\n -- This handles edge cases where job might still be in group set due to race conditions\n -- or if ZPOPMIN didn't fully remove it (shouldn't happen, but be safe)\n redis.call(\"ZREM\", gZ, headJobId)\n \n break\n end\n end\n end\n end\nend\n\nif not chosenGid or not job then\n return nil\nend\n\nlocal id, groupId, payload, attempts, maxAttempts, seq, enq, orderMs, score = job[1], job[2], job[3], job[4], job[5], job[6], job[7], job[8], job[9]\n\n-- Validate job data exists (handle corrupted/missing job hash)\nif not id or id == false then\n -- Job hash is missing/corrupted, clean up group active list\n local groupActiveKey = ns .. \":g:\" .. chosenGid .. \":active\"\n redis.call(\"LREM\", groupActiveKey, 1, headJobId)\n \n -- Re-add next job to ready queue if exists\n local gZ = ns .. \":g:\" .. chosenGid\n local nextHead = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if nextHead and #nextHead >= 2 then\n local nextScore = tonumber(nextHead[2])\n redis.call(\"ZADD\", readyKey, nextScore, chosenGid)\n end\n \n return nil\nend\n\n-- Remove the group from ready queue\nredis.call(\"ZREMRANGEBYRANK\", readyKey, chosenIndex, chosenIndex)\n\nlocal procKey = ns .. \":processing:\" .. id\nlocal deadline = now + vt\nredis.call(\"HSET\", procKey, \"groupId\", chosenGid, \"deadlineAt\", tostring(deadline))\n\nlocal processingKey2 = ns .. \":processing\"\nredis.call(\"ZADD\", processingKey2, deadline, id)\n\nlocal gZ = ns .. \":g:\" .. chosenGid\nlocal nextHead = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\nif nextHead and #nextHead >= 2 then\n local nextScore = tonumber(nextHead[2])\n redis.call(\"ZADD\", readyKey, nextScore, chosenGid)\nend\n\nreturn id .. \"||GROUPMQ||\" .. groupId .. \"||GROUPMQ||\" .. payload .. \"||GROUPMQ||\" .. attempts .. \"||GROUPMQ||\" .. maxAttempts .. \"||GROUPMQ||\" .. seq .. \"||GROUPMQ||\" .. enq .. \"||GROUPMQ||\" .. orderMs .. \"||GROUPMQ||\" .. score .. \"||GROUPMQ||\" .. deadline\n\n\n",
|
|
7330
7330
|
"retry": "-- argv: ns, jobId, backoffMs\nlocal ns = KEYS[1]\nlocal jobId = ARGV[1]\nlocal backoffMs = tonumber(ARGV[2]) or 0\n\nlocal jobKey = ns .. \":job:\" .. jobId\nlocal gid = redis.call(\"HGET\", jobKey, \"groupId\")\nlocal attempts = tonumber(redis.call(\"HINCRBY\", jobKey, \"attempts\", 1))\nlocal maxAttempts = tonumber(redis.call(\"HGET\", jobKey, \"maxAttempts\"))\n\nredis.call(\"DEL\", ns .. \":processing:\" .. jobId)\nredis.call(\"ZREM\", ns .. \":processing\", jobId)\n\n-- BullMQ-style: Remove from group active list\nlocal groupActiveKey = ns .. \":g:\" .. gid .. \":active\"\nredis.call(\"LREM\", groupActiveKey, 1, jobId)\n\nif attempts > maxAttempts then\n return -1\nend\n\nlocal score = tonumber(redis.call(\"HGET\", jobKey, \"score\"))\nlocal gZ = ns .. \":g:\" .. gid\n\n-- Re-add job to group\nredis.call(\"ZADD\", gZ, score, jobId)\n\n-- If backoffMs > 0, delay the retry\nif backoffMs > 0 then\n local now = tonumber(redis.call(\"TIME\")[1]) * 1000\n local delayUntil = now + backoffMs\n \n -- Move to delayed set\n local delayedKey = ns .. \":delayed\"\n redis.call(\"ZADD\", delayedKey, delayUntil, jobId)\n redis.call(\"HSET\", jobKey, \"runAt\", tostring(delayUntil), \"status\", \"delayed\")\n \n -- Don't add to ready yet - will be added when promoted\n -- (delayed jobs block their group)\nelse\n -- No backoff - immediate retry\n redis.call(\"HSET\", jobKey, \"status\", \"waiting\")\n \n -- Add group to ready queue\n local head = redis.call(\"ZRANGE\", gZ, 0, 0, \"WITHSCORES\")\n if head and #head >= 2 then\n local headScore = tonumber(head[2])\n local readyKey = ns .. \":ready\"\n redis.call(\"ZADD\", readyKey, headScore, gid)\n end\nend\n\nreturn attempts\n"
|
|
7331
7331
|
};
|
package/package.json
CHANGED