bullmq
Version:
Queue for messages and jobs based on Redis
529 lines • 19.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.moveToWaitingChildren = void 0;
const content = `--[[
Moves job from active to waiting children set.
Input:
KEYS[1] active key
KEYS[2] wait-children key
KEYS[3] job key
KEYS[4] job dependencies key
KEYS[5] job unsuccessful key
KEYS[6] stalled key
KEYS[7] failed key
KEYS[8] events key
ARGV[1] token
ARGV[2] child key
ARGV[3] timestamp
ARGV[4] jobId
ARGV[5] prefix
Output:
0 - OK
1 - There are not pending dependencies.
-1 - Missing job.
-2 - Missing lock
-3 - Job not in active set
]]
local rcall = redis.call
local activeKey = KEYS[1]
local waitingChildrenKey = KEYS[2]
local jobKey = KEYS[3]
local jobDependenciesKey = KEYS[4]
local jobUnsuccessfulKey = KEYS[5]
local stalledKey = KEYS[6]
local failedKey = KEYS[7]
local timestamp = ARGV[3]
local jobId = ARGV[4]
--- Includes
--[[
Function to recursively move from waitingChildren to failed.
]]
-- Includes
--[[
Validate and move parent to a wait status (waiting, delayed or prioritized)
if no pending dependencies.
]]
-- Includes
--[[
Validate and move parent to a wait status (waiting, delayed or prioritized) if needed.
]]
-- Includes
--[[
Move parent to a wait status (wait, prioritized or delayed)
]]
-- Includes
--[[
Add delay marker if needed.
]]
-- Includes
--[[
Function to return the next delayed job timestamp.
]]
local function getNextDelayedTimestamp(delayedKey)
local result = rcall("ZRANGE", delayedKey, 0, 0, "WITHSCORES")
if #result then
local nextTimestamp = tonumber(result[2])
if nextTimestamp ~= nil then
return nextTimestamp / 0x1000
end
end
end
local function addDelayMarkerIfNeeded(markerKey, delayedKey)
local nextTimestamp = getNextDelayedTimestamp(delayedKey)
if nextTimestamp ~= nil then
-- Replace the score of the marker with the newest known
-- next timestamp.
rcall("ZADD", markerKey, nextTimestamp, "1")
end
end
--[[
Function to add job in target list and add marker if needed.
]]
-- Includes
--[[
Add marker if needed when a job is available.
]]
local function addBaseMarkerIfNeeded(markerKey, isPausedOrMaxed)
if not isPausedOrMaxed then
rcall("ZADD", markerKey, 0, "0")
end
end
local function addJobInTargetList(targetKey, markerKey, pushCmd, isPausedOrMaxed, jobId)
rcall(pushCmd, targetKey, jobId)
addBaseMarkerIfNeeded(markerKey, isPausedOrMaxed)
end
--[[
Function to add job considering priority.
]]
-- Includes
--[[
Function to get priority score.
]]
local function getPriorityScore(priority, priorityCounterKey)
local prioCounter = rcall("INCR", priorityCounterKey)
return priority * 0x100000000 + prioCounter % 0x100000000
end
local function addJobWithPriority(markerKey, prioritizedKey, priority, jobId, priorityCounterKey,
isPausedOrMaxed)
local score = getPriorityScore(priority, priorityCounterKey)
rcall("ZADD", prioritizedKey, score, jobId)
addBaseMarkerIfNeeded(markerKey, isPausedOrMaxed)
end
--[[
Function to check if queue is paused or maxed
(since an empty list and !EXISTS are not really the same).
]]
local function isQueuePausedOrMaxed(queueMetaKey, activeKey)
local queueAttributes = rcall("HMGET", queueMetaKey, "paused", "concurrency")
if queueAttributes[1] then
return true
else
if queueAttributes[2] then
local activeCount = rcall("LLEN", activeKey)
return activeCount >= tonumber(queueAttributes[2])
end
end
return false
end
--[[
Function to check for the meta.paused key to decide if we are paused or not
(since an empty list and !EXISTS are not really the same).
]]
local function getTargetQueueList(queueMetaKey, activeKey, waitKey, pausedKey)
local queueAttributes = rcall("HMGET", queueMetaKey, "paused", "concurrency")
if queueAttributes[1] then
return pausedKey, true
else
if queueAttributes[2] then
local activeCount = rcall("LLEN", activeKey)
if activeCount >= tonumber(queueAttributes[2]) then
return waitKey, true
else
return waitKey, false
end
end
end
return waitKey, false
end
local function moveParentToWait(parentQueueKey, parentKey, parentId, timestamp)
local parentWaitKey = parentQueueKey .. ":wait"
local parentPausedKey = parentQueueKey .. ":paused"
local parentActiveKey = parentQueueKey .. ":active"
local parentMetaKey = parentQueueKey .. ":meta"
local parentMarkerKey = parentQueueKey .. ":marker"
local jobAttributes = rcall("HMGET", parentKey, "priority", "delay")
local priority = tonumber(jobAttributes[1]) or 0
local delay = tonumber(jobAttributes[2]) or 0
if delay > 0 then
local delayedTimestamp = tonumber(timestamp) + delay
local score = delayedTimestamp * 0x1000
local parentDelayedKey = parentQueueKey .. ":delayed"
rcall("ZADD", parentDelayedKey, score, parentId)
rcall("XADD", parentQueueKey .. ":events", "*", "event", "delayed", "jobId", parentId, "delay",
delayedTimestamp)
addDelayMarkerIfNeeded(parentMarkerKey, parentDelayedKey)
else
if priority == 0 then
local parentTarget, isParentPausedOrMaxed = getTargetQueueList(parentMetaKey, parentActiveKey,
parentWaitKey, parentPausedKey)
addJobInTargetList(parentTarget, parentMarkerKey, "RPUSH", isParentPausedOrMaxed, parentId)
else
local isPausedOrMaxed = isQueuePausedOrMaxed(parentMetaKey, parentActiveKey)
addJobWithPriority(parentMarkerKey, parentQueueKey .. ":prioritized", priority, parentId,
parentQueueKey .. ":pc", isPausedOrMaxed)
end
rcall("XADD", parentQueueKey .. ":events", "*", "event", "waiting", "jobId", parentId, "prev",
"waiting-children")
end
end
local function moveParentToWaitIfNeeded(parentQueueKey, parentKey, parentId, timestamp)
if rcall("EXISTS", parentKey) == 1 then
local parentWaitingChildrenKey = parentQueueKey .. ":waiting-children"
if rcall("ZSCORE", parentWaitingChildrenKey, parentId) then
rcall("ZREM", parentWaitingChildrenKey, parentId)
moveParentToWait(parentQueueKey, parentKey, parentId, timestamp)
end
end
end
local function moveParentToWaitIfNoPendingDependencies(parentQueueKey, parentDependenciesKey, parentKey,
parentId, timestamp)
local doNotHavePendingDependencies = rcall("SCARD", parentDependenciesKey) == 0
if doNotHavePendingDependencies then
moveParentToWaitIfNeeded(parentQueueKey, parentKey, parentId, timestamp)
end
end
--[[
Functions to remove jobs when removeOnFail option is provided.
]]
-- Includes
--[[
Function to remove job.
]]
-- Includes
--[[
Function to remove deduplication key if needed
when a job is being removed.
]]
local function removeDeduplicationKeyIfNeededOnRemoval(prefixKey,
jobKey, jobId)
local deduplicationId = rcall("HGET", jobKey, "deid")
if deduplicationId then
local deduplicationKey = prefixKey .. "de:" .. deduplicationId
local currentJobId = rcall('GET', deduplicationKey)
if currentJobId and currentJobId == jobId then
return rcall("DEL", deduplicationKey)
end
end
end
--[[
Function to remove job keys.
]]
local function removeJobKeys(jobKey)
return rcall("DEL", jobKey, jobKey .. ':logs', jobKey .. ':dependencies',
jobKey .. ':processed', jobKey .. ':failed', jobKey .. ':unsuccessful')
end
--[[
Check if this job has a parent. If so we will just remove it from
the parent child list, but if it is the last child we should move the parent to "wait/paused"
which requires code from "moveToFinished"
]]
-- Includes
--[[
Functions to destructure job key.
Just a bit of warning, these functions may be a bit slow and affect performance significantly.
]]
local getJobIdFromKey = function (jobKey)
return string.match(jobKey, ".*:(.*)")
end
local getJobKeyPrefix = function (jobKey, jobId)
return string.sub(jobKey, 0, #jobKey - #jobId)
end
local function _moveParentToWait(parentPrefix, parentId, emitEvent)
local parentTarget, isPausedOrMaxed = getTargetQueueList(parentPrefix .. "meta", parentPrefix .. "active",
parentPrefix .. "wait", parentPrefix .. "paused")
addJobInTargetList(parentTarget, parentPrefix .. "marker", "RPUSH", isPausedOrMaxed, parentId)
if emitEvent then
local parentEventStream = parentPrefix .. "events"
rcall("XADD", parentEventStream, "*", "event", "waiting", "jobId", parentId, "prev", "waiting-children")
end
end
local function removeParentDependencyKey(jobKey, hard, parentKey, baseKey, debounceId)
if parentKey then
local parentDependenciesKey = parentKey .. ":dependencies"
local result = rcall("SREM", parentDependenciesKey, jobKey)
if result > 0 then
local pendingDependencies = rcall("SCARD", parentDependenciesKey)
if pendingDependencies == 0 then
local parentId = getJobIdFromKey(parentKey)
local parentPrefix = getJobKeyPrefix(parentKey, parentId)
local numRemovedElements = rcall("ZREM", parentPrefix .. "waiting-children", parentId)
if numRemovedElements == 1 then
if hard then -- remove parent in same queue
if parentPrefix == baseKey then
removeParentDependencyKey(parentKey, hard, nil, baseKey, nil)
removeJobKeys(parentKey)
if debounceId then
rcall("DEL", parentPrefix .. "de:" .. debounceId)
end
else
_moveParentToWait(parentPrefix, parentId)
end
else
_moveParentToWait(parentPrefix, parentId, true)
end
end
end
return true
end
else
local parentAttributes = rcall("HMGET", jobKey, "parentKey", "deid")
local missedParentKey = parentAttributes[1]
if( (type(missedParentKey) == "string") and missedParentKey ~= ""
and (rcall("EXISTS", missedParentKey) == 1)) then
local parentDependenciesKey = missedParentKey .. ":dependencies"
local result = rcall("SREM", parentDependenciesKey, jobKey)
if result > 0 then
local pendingDependencies = rcall("SCARD", parentDependenciesKey)
if pendingDependencies == 0 then
local parentId = getJobIdFromKey(missedParentKey)
local parentPrefix = getJobKeyPrefix(missedParentKey, parentId)
local numRemovedElements = rcall("ZREM", parentPrefix .. "waiting-children", parentId)
if numRemovedElements == 1 then
if hard then
if parentPrefix == baseKey then
removeParentDependencyKey(missedParentKey, hard, nil, baseKey, nil)
removeJobKeys(missedParentKey)
if parentAttributes[2] then
rcall("DEL", parentPrefix .. "de:" .. parentAttributes[2])
end
else
_moveParentToWait(parentPrefix, parentId)
end
else
_moveParentToWait(parentPrefix, parentId, true)
end
end
end
return true
end
end
end
return false
end
local function removeJob(jobId, hard, baseKey, shouldRemoveDeduplicationKey)
local jobKey = baseKey .. jobId
removeParentDependencyKey(jobKey, hard, nil, baseKey)
if shouldRemoveDeduplicationKey then
removeDeduplicationKeyIfNeededOnRemoval(baseKey, jobKey, jobId)
end
removeJobKeys(jobKey)
end
--[[
Functions to remove jobs by max age.
]]
-- Includes
local function removeJobsByMaxAge(timestamp, maxAge, targetSet, prefix,
shouldRemoveDebounceKey)
local start = timestamp - maxAge * 1000
local jobIds = rcall("ZREVRANGEBYSCORE", targetSet, start, "-inf")
for i, jobId in ipairs(jobIds) do
removeJob(jobId, false, prefix, false --[[remove debounce key]])
end
rcall("ZREMRANGEBYSCORE", targetSet, "-inf", start)
end
--[[
Functions to remove jobs by max count.
]]
-- Includes
local function removeJobsByMaxCount(maxCount, targetSet, prefix)
local start = maxCount
local jobIds = rcall("ZREVRANGE", targetSet, start, -1)
for i, jobId in ipairs(jobIds) do
removeJob(jobId, false, prefix, false --[[remove debounce key]])
end
rcall("ZREMRANGEBYRANK", targetSet, 0, -(maxCount + 1))
end
local function removeJobsOnFail(queueKeyPrefix, failedKey, jobId, opts, timestamp)
local removeOnFailType = type(opts["removeOnFail"])
if removeOnFailType == "number" then
removeJobsByMaxCount(opts["removeOnFail"],
failedKey, queueKeyPrefix)
elseif removeOnFailType == "boolean" then
if opts["removeOnFail"] then
removeJob(jobId, false, queueKeyPrefix,
false --[[remove debounce key]])
rcall("ZREM", failedKey, jobId)
end
elseif removeOnFailType ~= "nil" then
local maxAge = opts["removeOnFail"]["age"]
local maxCount = opts["removeOnFail"]["count"]
if maxAge ~= nil then
removeJobsByMaxAge(timestamp, maxAge,
failedKey, queueKeyPrefix)
end
if maxCount ~= nil and maxCount > 0 then
removeJobsByMaxCount(maxCount, failedKey,
queueKeyPrefix)
end
end
end
local moveParentToFailedIfNeeded = function (parentQueueKey, parentKey, parentId, jobIdKey, timestamp)
if rcall("EXISTS", parentKey) == 1 then
local parentWaitingChildrenKey = parentQueueKey .. ":waiting-children"
local parentDelayedKey = parentQueueKey .. ":delayed"
local parentPrioritizedKey = parentQueueKey .. ":prioritized"
local parentWaitingChildrenOrDelayedKey
local prevState
if rcall("ZSCORE", parentWaitingChildrenKey, parentId) then
parentWaitingChildrenOrDelayedKey = parentWaitingChildrenKey
prevState = "waiting-children"
elseif rcall("ZSCORE", parentDelayedKey, parentId) then
parentWaitingChildrenOrDelayedKey = parentDelayedKey
prevState = "delayed"
rcall("HSET", parentKey, "delay", 0)
end
if parentWaitingChildrenOrDelayedKey then
rcall("ZREM", parentWaitingChildrenOrDelayedKey, parentId)
local parentQueuePrefix = parentQueueKey .. ":"
local parentFailedKey = parentQueueKey .. ":failed"
local deferredFailure = "child " .. jobIdKey .. " failed"
rcall("HSET", parentKey, "defa", deferredFailure)
moveParentToWait(parentQueueKey, parentKey, parentId, timestamp)
else
if not rcall("ZSCORE", parentQueueKey .. ":failed", parentId) then
local deferredFailure = "child " .. jobIdKey .. " failed"
rcall("HSET", parentKey, "defa", deferredFailure)
end
end
end
end
local moveChildFromDependenciesIfNeeded = function (rawParentData, childKey, failedReason, timestamp)
if rawParentData then
local parentData = cjson.decode(rawParentData)
local parentKey = parentData['queueKey'] .. ':' .. parentData['id']
local parentDependenciesChildrenKey = parentKey .. ":dependencies"
if parentData['fpof'] then
if rcall("SREM", parentDependenciesChildrenKey, childKey) == 1 then
local parentUnsuccesssfulChildrenKey = parentKey .. ":unsuccessful"
rcall("ZADD", parentUnsuccesssfulChildrenKey, timestamp, childKey)
moveParentToFailedIfNeeded(
parentData['queueKey'],
parentKey,
parentData['id'],
childKey,
timestamp
)
end
elseif parentData['cpof'] then
if rcall("SREM", parentDependenciesChildrenKey, childKey) == 1 then
local parentFailedChildrenKey = parentKey .. ":failed"
rcall("HSET", parentFailedChildrenKey, childKey, failedReason)
moveParentToWaitIfNeeded(parentData['queueKey'], parentKey, parentData['id'], timestamp)
end
elseif parentData['idof'] or parentData['rdof'] then
if rcall("SREM", parentDependenciesChildrenKey, childKey) == 1 then
moveParentToWaitIfNoPendingDependencies(parentData['queueKey'], parentDependenciesChildrenKey,
parentKey, parentData['id'], timestamp)
if parentData['idof'] then
local parentFailedChildrenKey = parentKey .. ":failed"
rcall("HSET", parentFailedChildrenKey, childKey, failedReason)
end
end
end
end
end
--[[
Function to remove deduplication key if needed
when a job is moved to completed or failed states.
]]
local function removeDeduplicationKeyIfNeededOnFinalization(prefixKey,
deduplicationId, jobId)
if deduplicationId then
local deduplicationKey = prefixKey .. "de:" .. deduplicationId
local pttl = rcall("PTTL", deduplicationKey)
if pttl == 0 then
return rcall("DEL", deduplicationKey)
end
if pttl == -1 then
local currentJobId = rcall('GET', deduplicationKey)
if currentJobId and currentJobId == jobId then
return rcall("DEL", deduplicationKey)
end
end
end
end
local function removeLock(jobKey, stalledKey, token, jobId)
if token ~= "0" then
local lockKey = jobKey .. ':lock'
local lockToken = rcall("GET", lockKey)
if lockToken == token then
rcall("DEL", lockKey)
rcall("SREM", stalledKey, jobId)
else
if lockToken then
-- Lock exists but token does not match
return -6
else
-- Lock is missing completely
return -2
end
end
end
return 0
end
local function moveToWaitingChildren(activeKey, waitingChildrenKey, jobId,
timestamp)
local score = tonumber(timestamp)
local numRemovedElements = rcall("LREM", activeKey, -1, jobId)
if(numRemovedElements < 1) then
return -3
end
rcall("ZADD", waitingChildrenKey, score, jobId)
return 0
end
if rcall("EXISTS", jobKey) == 1 then
if rcall("ZCARD", jobUnsuccessfulKey) ~= 0 then
-- TODO: refactor this logic in an include later
local jobAttributes = rcall("HMGET", jobKey, "parent", "deid", "opts")
removeDeduplicationKeyIfNeededOnFinalization(ARGV[5], jobAttributes[2], jobId)
local failedReason = "children are failed"
rcall("ZADD", failedKey, timestamp, jobId)
rcall("HSET", jobKey, "finishedOn", timestamp)
rcall("XADD", KEYS[8], "*", "event", "failed", "jobId", jobId, "failedReason",
failedReason, "prev", "active")
local rawParentData = jobAttributes[1]
local rawOpts = jobAttributes[3]
local opts = cjson.decode(rawOpts)
moveChildFromDependenciesIfNeeded(rawParentData, jobKey, failedReason, timestamp)
removeJobsOnFail(ARGV[5], failedKey, jobId, opts, timestamp)
return 0
else
if ARGV[2] ~= "" then
if rcall("SISMEMBER", jobDependenciesKey, ARGV[2]) ~= 0 then
local errorCode = removeLock(jobKey, stalledKey, ARGV[1], jobId)
if errorCode < 0 then
return errorCode
end
return moveToWaitingChildren(activeKey, waitingChildrenKey, jobId, timestamp)
end
return 1
else
if rcall("SCARD", jobDependenciesKey) ~= 0 then
local errorCode = removeLock(jobKey, stalledKey, ARGV[1], jobId)
if errorCode < 0 then
return errorCode
end
return moveToWaitingChildren(activeKey, waitingChildrenKey, jobId, timestamp)
end
return 1
end
end
end
return -1
`;
exports.moveToWaitingChildren = {
name: 'moveToWaitingChildren',
content,
keys: 8,
};
//# sourceMappingURL=moveToWaitingChildren-8.js.map