Source code for litestar_queues.backends.redis.backend

"""Redis queue backend.

Stores queued task records in a Redis-protocol key-value server. The
implementation lives directly on ``RedisQueueBackend``; the Valkey
backend inherits from this class and only swaps the client factory and
``_backend_name`` ClassVar.
"""

import asyncio
import inspect
import json
from contextlib import suppress
from datetime import datetime, timedelta, timezone
from typing import TYPE_CHECKING, Any, ClassVar, cast
from uuid import UUID

from litestar_queues.backends._notification_wait import PendingNativeRead
from litestar_queues.backends.base import (
    EXTERNAL_DISPATCH_RESERVATION_PREFIX,
    STALE_HEARTBEAT_ERROR,
    BaseQueueBackend,
    attempts_consumed,
    interruption_count,
    is_external_dispatch_reservation,
    record_matches_filters,
    retry_schedule,
    stale_requeue_error,
    stale_requeue_priority,
)
from litestar_queues.backends.redis.config import RedisBackendConfig as _RedisBackendConfig
from litestar_queues.backends.redis.event_log import RedisQueueEventLog, hashed_index_value
from litestar_queues.exceptions import QueueConfigurationError
from litestar_queues.models import (
    HeartbeatTouchResult,
    QueueBackendCapabilities,
    QueuedTaskRecord,
    QueueStatistics,
    StaleTaskRecoveryResult,
    TaskReservation,
    TaskStatus,
)

if TYPE_CHECKING:
    from collections.abc import Iterable, Mapping, Sequence

    from litestar_queues.backends._protocol import ClientLike, PipelineLike, PubSubLike
    from litestar_queues.config import QueueConfig
    from litestar_queues.events import EventHistoryConfig
    from litestar_queues.models import HeartbeatTouch, TaskRequest

__all__ = ("RedisQueueBackend",)

_DUE_STATUSES = {"pending", "scheduled"}
_STATUS_VALUES = {"cancelled", "completed", "expired", "failed", "pending", "running", "scheduled"}
_TERMINAL_STATUSES = {"cancelled", "completed", "expired", "failed"}
_MAINTENANCE_INDEX_VERSION = "2"
_CLAIMED_OUTCOME = 1
_EXPIRED_OUTCOME = 2
_TOUCH_HEARTBEAT_SCRIPT = """
local status = redis.call('HGET', KEYS[1], 'status')
if status ~= 'running' then
    return 0
end

local expected_retry_count = ARGV[1]
if expected_retry_count ~= '' then
    local retry_count = redis.call('HGET', KEYS[1], 'retry_count')
    if retry_count ~= expected_retry_count then
        return 0
    end
end

local heartbeat_at = ARGV[2]
local metadata_patch_json = ARGV[3]
local heartbeat_score = ARGV[4]
local prefix = ARGV[5]
local task_id = ARGV[6]
if metadata_patch_json ~= '' then
    local metadata_json = redis.call('HGET', KEYS[1], 'metadata')
    local metadata = {}
    if metadata_json and metadata_json ~= '' then
        local ok, decoded = pcall(cjson.decode, metadata_json)
        if ok and type(decoded) == 'table' then
            metadata = decoded
        end
    end

    local ok_patch, metadata_patch = pcall(cjson.decode, metadata_patch_json)
    if ok_patch and type(metadata_patch) == 'table' then
        for key, value in pairs(metadata_patch) do
            metadata[key] = value
        end
    end
    redis.call('HSET', KEYS[1], 'heartbeat_at', heartbeat_at, 'heartbeat_score', heartbeat_score,
        'metadata', cjson.encode(metadata))
else
    redis.call('HSET', KEYS[1], 'heartbeat_at', heartbeat_at, 'heartbeat_score', heartbeat_score)
end
redis.call('ZADD', prefix .. ':maintenance:running', heartbeat_score, task_id)

return 1
"""
_CLAIM_SCRIPT = """
local ready = KEYS[1]
local scheduled = KEYS[2]
local prefix = ARGV[1]
local now_ms = tonumber(ARGV[2])
local now_iso = ARGV[3]
local limit = tonumber(ARGV[4])
local eb_filter = ARGV[5]
local window = tonumber(ARGV[6])

local reservation_prefix = ARGV[7]
local ncaps = tonumber(ARGV[8])
local caps = {}
local idx = 9
for _ = 1, ncaps do
    caps[ARGV[idx]] = tonumber(ARGV[idx + 1])
    idx = idx + 2
end
local queue_filter = {}
local has_queue_filter = false
for i = idx, #ARGV do
    queue_filter[ARGV[i]] = true
    has_queue_filter = true
end

local expired = {}
local due = redis.call('ZRANGEBYSCORE', scheduled, '-inf', now_ms)
for _, id in ipairs(due) do
    local hkey = prefix .. ':task:' .. id
    local status = redis.call('HGET', hkey, 'status')
    local eb = redis.call('HGET', hkey, 'execution_backend')
    local q = redis.call('HGET', hkey, 'queue')
    local execution_ref = redis.call('HGET', hkey, 'execution_ref')
    local eb_ok = (eb_filter == '' or eb == eb_filter)
    local q_ok = (not has_queue_filter or queue_filter[q] == true)
    local reservation_active = execution_ref and string.sub(execution_ref, 1, string.len(reservation_prefix)) == reservation_prefix
    local unreserved = not reservation_active
    if (status == 'scheduled' or status == 'pending') and eb_ok and q_ok and unreserved then
        local expires_score = tonumber(redis.call('HGET', hkey, 'expires_score')) or 0
        if expires_score > 0 and expires_score <= now_ms then
            redis.call('HSET', hkey, 'status', 'expired', 'completed_at', now_iso,
                'completed_score', now_ms, 'heartbeat_at', '', 'heartbeat_score', '0')
            redis.call('SREM', prefix .. ':status:' .. status, id)
            redis.call('SADD', prefix .. ':status:expired', id)
            redis.call('ZREM', ready, id)
            redis.call('ZREM', prefix .. ':maintenance:running', id)
            redis.call('ZREM', prefix .. ':maintenance:external', id)
            redis.call('ZREM', prefix .. ':maintenance:expiry', id)
            redis.call('ZADD', prefix .. ':maintenance:terminal', now_ms, id)
            redis.call('PUBLISH', prefix .. ':completions', id)
            expired[#expired + 1] = id
        else
            local ready_score = redis.call('HGET', hkey, 'ready_score')
            if ready_score then
                redis.call('ZADD', ready, ready_score, id)
            end
            if status == 'scheduled' then
                redis.call('SREM', prefix .. ':status:scheduled', id)
                redis.call('SADD', prefix .. ':status:pending', id)
                redis.call('HSET', hkey, 'status', 'pending')
            end
        end
        redis.call('ZREM', scheduled, id)
    elseif status ~= 'scheduled' and status ~= 'pending' then
        redis.call('ZREM', scheduled, id)
    end
end

local claimed = {}
local start = 0
while #claimed < limit do
    local candidates = redis.call('ZRANGE', ready, start, start + window - 1)
    if #candidates == 0 then break end
    local scanned = #candidates
    local removed = 0
    for _, id in ipairs(candidates) do
        if #claimed >= limit then break end
        local hkey = prefix .. ':task:' .. id
        local status = redis.call('HGET', hkey, 'status')
        if status ~= 'pending' then
            redis.call('ZREM', ready, id)
            removed = removed + 1
        else
            local eb = redis.call('HGET', hkey, 'execution_backend')
            local q = redis.call('HGET', hkey, 'queue')
            local execution_ref = redis.call('HGET', hkey, 'execution_ref')
            local eb_ok = (eb_filter == '' or eb == eb_filter)
            local q_ok = (not has_queue_filter or queue_filter[q] == true)
            local reservation_active = execution_ref and string.sub(execution_ref, 1, string.len(reservation_prefix)) == reservation_prefix
            local unreserved = not reservation_active
            if eb_ok and q_ok and unreserved then
                local expires_score = tonumber(redis.call('HGET', hkey, 'expires_score')) or 0
                if expires_score > 0 and expires_score <= now_ms then
                    redis.call('HSET', hkey, 'status', 'expired', 'completed_at', now_iso,
                        'completed_score', now_ms, 'heartbeat_at', '', 'heartbeat_score', '0')
                    redis.call('SREM', prefix .. ':status:pending', id)
                    redis.call('SADD', prefix .. ':status:expired', id)
                    redis.call('ZREM', ready, id)
                    removed = removed + 1
                    redis.call('ZREM', scheduled, id)
                    redis.call('ZREM', prefix .. ':maintenance:running', id)
                    redis.call('ZREM', prefix .. ':maintenance:external', id)
                    redis.call('ZREM', prefix .. ':maintenance:expiry', id)
                    redis.call('ZADD', prefix .. ':maintenance:terminal', now_ms, id)
                    redis.call('PUBLISH', prefix .. ':completions', id)
                    expired[#expired + 1] = id
                else
                    local cap = caps[q]
                    if cap == nil or cap > 0 then
                        redis.call('HSET', hkey, 'status', 'running', 'started_at', now_iso, 'heartbeat_at', now_iso,
                            'started_score', now_ms, 'heartbeat_score', now_ms)
                        redis.call('SREM', prefix .. ':status:pending', id)
                        redis.call('SADD', prefix .. ':status:running', id)
                        redis.call('ZREM', ready, id)
                        removed = removed + 1
                        redis.call('ZADD', prefix .. ':maintenance:running', now_ms, id)
                        redis.call('ZREM', prefix .. ':maintenance:terminal', id)
                        redis.call('ZREM', prefix .. ':maintenance:expiry', id)
                        redis.call('ZREM', prefix .. ':maintenance:external', id)
                        claimed[#claimed + 1] = id
                        if cap ~= nil then caps[q] = cap - 1 end
                    end
                end
            end
        end
    end
    start = start + scanned - removed
end
local outcome = {}
for _, id in ipairs(claimed) do
    outcome[#outcome + 1] = 'claimed:' .. id
end
for _, id in ipairs(expired) do
    outcome[#outcome + 1] = 'expired:' .. id
end
return outcome
"""
_CLAIM_TASK_SCRIPT = """
local hkey = KEYS[1]
local ready = KEYS[2]
local scheduled = KEYS[3]
local prefix = ARGV[1]
local task_id = ARGV[2]
local now_ms = tonumber(ARGV[3])
local now_iso = ARGV[4]
local reservation_prefix = ARGV[5]
local expected_retry = ARGV[6]
local expected_ref = ARGV[7]

local status = redis.call('HGET', hkey, 'status')
if status ~= 'pending' and status ~= 'scheduled' then
    return {0}
end
local execution_ref = redis.call('HGET', hkey, 'execution_ref')
if expected_retry ~= '' and redis.call('HGET', hkey, 'retry_count') ~= expected_retry then return {0} end
if expected_ref ~= '' and execution_ref ~= expected_ref then return {0} end
if execution_ref and string.sub(execution_ref, 1, string.len(reservation_prefix)) == reservation_prefix then
    return {0}
end
local scheduled_score = redis.call('ZSCORE', scheduled, task_id)
if scheduled_score and tonumber(scheduled_score) > now_ms then
    return {0}
end
local expires_score = tonumber(redis.call('HGET', hkey, 'expires_score')) or 0
if (not execution_ref or execution_ref == '') and expires_score > 0 and expires_score <= now_ms then
    redis.call('HSET', hkey, 'status', 'expired', 'completed_at', now_iso,
        'completed_score', now_ms, 'heartbeat_at', '', 'heartbeat_score', '0')
    redis.call('SREM', prefix .. ':status:' .. status, task_id)
    redis.call('SADD', prefix .. ':status:expired', task_id)
    redis.call('ZREM', ready, task_id)
    redis.call('ZREM', scheduled, task_id)
    redis.call('ZREM', prefix .. ':maintenance:running', task_id)
    redis.call('ZREM', prefix .. ':maintenance:external', task_id)
    redis.call('ZREM', prefix .. ':maintenance:expiry', task_id)
    redis.call('ZADD', prefix .. ':maintenance:terminal', now_ms, task_id)
    redis.call('PUBLISH', prefix .. ':completions', task_id)
    return {2}
end

redis.call('HSET', hkey, 'status', 'running', 'started_at', now_iso, 'heartbeat_at', now_iso,
    'started_score', now_ms, 'heartbeat_score', now_ms)
redis.call('SREM', prefix .. ':status:' .. status, task_id)
redis.call('SADD', prefix .. ':status:running', task_id)
redis.call('ZREM', ready, task_id)
redis.call('ZREM', scheduled, task_id)
redis.call('ZADD', prefix .. ':maintenance:running', now_ms, task_id)
redis.call('ZREM', prefix .. ':maintenance:terminal', task_id)
redis.call('ZREM', prefix .. ':maintenance:expiry', task_id)
local execution_ref = redis.call('HGET', hkey, 'execution_ref')
if execution_ref and execution_ref ~= '' then
    redis.call('ZADD', prefix .. ':maintenance:external', now_ms, task_id)
else
    redis.call('ZREM', prefix .. ':maintenance:external', task_id)
end
return {1}
"""

_CLEAR_EXECUTION_REF_SCRIPT = """
local status = redis.call('HGET', KEYS[1], 'status')
if status ~= 'pending' and status ~= 'scheduled' then return {0} end
if redis.call('HGET', KEYS[1], 'retry_count') ~= ARGV[1] then return {0} end
if redis.call('HGET', KEYS[1], 'execution_ref') ~= ARGV[2] then return {0} end
redis.call('HSET', KEYS[1], 'execution_ref', '')
return {1}
"""

_REPLACE_EXECUTION_REF_SCRIPT = """
local status = redis.call('HGET', KEYS[1], 'status')
if status ~= 'pending' and status ~= 'scheduled' then return {0} end
if redis.call('HGET', KEYS[1], 'retry_count') ~= ARGV[1] then return {0} end
if redis.call('HGET', KEYS[1], 'execution_ref') ~= ARGV[2] then return {0} end
redis.call('HSET', KEYS[1], 'execution_ref', ARGV[3])
return {1}
"""
_COMPLETE_SCRIPT = """
local hkey = KEYS[1]
local prefix = ARGV[1]
local task_id = ARGV[2]
local expected = ARGV[3]
local completed_at = ARGV[4]
local result_json = ARGV[5]
local channel = ARGV[6]
local completed_score = ARGV[7]

local status = redis.call('HGET', hkey, 'status')
if status ~= 'running' then
    return {0}
end
if expected ~= '' then
    local retry_count = redis.call('HGET', hkey, 'retry_count')
    if retry_count ~= expected then
        return {0}
    end
end
redis.call('HSET', hkey, 'status', 'completed', 'completed_at', completed_at,
    'completed_score', completed_score, 'heartbeat_at', '', 'heartbeat_score', '0',
    'result', result_json, 'error', '')
redis.call('SREM', prefix .. ':status:running', task_id)
redis.call('SADD', prefix .. ':status:completed', task_id)
redis.call('ZREM', prefix .. ':maintenance:running', task_id)
redis.call('ZREM', prefix .. ':maintenance:external', task_id)
redis.call('ZADD', prefix .. ':maintenance:terminal', completed_score, task_id)
redis.call('PUBLISH', channel, task_id)
return {1}
"""
_FAIL_SCRIPT = """
local hkey = KEYS[1]
local ready = KEYS[2]
local prefix = ARGV[1]
local task_id = ARGV[2]
local expected = ARGV[3]
local error = ARGV[4]
local retry = ARGV[5]
local completed_at = ARGV[6]
local channel = ARGV[7]
local completed_score = ARGV[8]
local retry_at = ARGV[9]
local retry_score = ARGV[10]
local queued_at = ARGV[11]
local ready_score = ARGV[12]

local status = redis.call('HGET', hkey, 'status')
if status ~= 'running' then
    return {0, ''}
end
local retry_count = tonumber(redis.call('HGET', hkey, 'retry_count')) or 0
if expected ~= '' and tostring(retry_count) ~= expected then
    return {0, ''}
end
redis.call('HSET', hkey, 'error', error)
local max_retries = tonumber(redis.call('HGET', hkey, 'max_retries')) or 0
local interruptions = 0
local metadata_json = redis.call('HGET', hkey, 'metadata')
if metadata_json and metadata_json ~= '' then
    local decoded_ok, decoded = pcall(cjson.decode, metadata_json)
    if decoded_ok and type(decoded) == 'table' and type(decoded.interruptions) == 'number' then
        interruptions = decoded.interruptions
    end
end
if retry == '1' and (retry_count - interruptions) < max_retries then
    local new_retry_count = retry_count + 1
    local retry_status = 'pending'
    if retry_at ~= '' then retry_status = 'scheduled' end
    redis.call('HSET', hkey, 'status', retry_status, 'retry_count', new_retry_count,
        'queued_at', queued_at, 'ready_score', ready_score, 'scheduled_at', retry_at,
        'started_at', '', 'started_score', '0', 'heartbeat_at', '', 'heartbeat_score', '0')
    redis.call('SREM', prefix .. ':status:running', task_id)
    redis.call('SADD', prefix .. ':status:' .. retry_status, task_id)
    redis.call('ZREM', prefix .. ':maintenance:running', task_id)
    redis.call('ZREM', prefix .. ':maintenance:terminal', task_id)
    local execution_ref = redis.call('HGET', hkey, 'execution_ref')
    if execution_ref and execution_ref ~= '' then
        local created_score = redis.call('HGET', hkey, 'created_score') or '0'
        redis.call('ZADD', prefix .. ':maintenance:external', created_score, task_id)
    else
        redis.call('ZREM', prefix .. ':maintenance:external', task_id)
    end
    if retry_at ~= '' then
        redis.call('ZREM', ready, task_id)
        redis.call('ZADD', KEYS[3], retry_score, task_id)
    else
        redis.call('ZREM', KEYS[3], task_id)
        redis.call('ZADD', ready, ready_score, task_id)
    end
    return {1, retry_status}
end
redis.call('HSET', hkey, 'status', 'failed', 'completed_at', completed_at,
    'completed_score', completed_score, 'heartbeat_at', '', 'heartbeat_score', '0')
redis.call('SREM', prefix .. ':status:running', task_id)
redis.call('SADD', prefix .. ':status:failed', task_id)
redis.call('ZREM', prefix .. ':maintenance:running', task_id)
redis.call('ZREM', prefix .. ':maintenance:external', task_id)
redis.call('ZADD', prefix .. ':maintenance:terminal', completed_score, task_id)
redis.call('PUBLISH', channel, task_id)
return {1, 'failed'}
"""
_ENQUEUE_SCRIPT = """
local ready = KEYS[1]
local scheduled = KEYS[2]
local prefix = ARGV[1]
local task_id = ARGV[2]
local status = ARGV[3]
local due = ARGV[4]
local score = ARGV[5]
local channel = ARGV[6]
local notify_payload = ARGV[7]
local publish = ARGV[8]
local expires_score = tonumber(ARGV[9]) or 0
local hkey = prefix .. ':task:' .. task_id
redis.call('HSET', hkey, unpack(ARGV, 10))
redis.call('SADD', prefix .. ':tasks', task_id)
redis.call('SADD', prefix .. ':status:' .. status, task_id)
redis.call('SADD', KEYS[3], task_id)
if expires_score > 0 then
    redis.call('ZADD', prefix .. ':maintenance:expiry', expires_score, task_id)
end
if due == '1' then
    redis.call('ZADD', ready, score, task_id)
    if publish == '1' then
        redis.call('PUBLISH', channel, notify_payload)
    end
else
    redis.call('ZADD', scheduled, score, task_id)
end
return {1}
"""
_ENQUEUE_KEYED_SCRIPT = """
local ready = KEYS[1]
local scheduled = KEYS[2]
local prefix = ARGV[1]
local task_id = ARGV[2]
local status = ARGV[3]
local due = ARGV[4]
local score = ARGV[5]
local channel = ARGV[6]
local notify_payload = ARGV[7]
local publish = ARGV[8]
local dedup_key = ARGV[9]
local expires_score = tonumber(ARGV[10]) or 0
local keys_hash = prefix .. ':keys'
local existing_id = redis.call('HGET', keys_hash, dedup_key)
if existing_id then
    local existing_status = redis.call('HGET', prefix .. ':task:' .. existing_id, 'status')
    if existing_status == 'pending' or existing_status == 'scheduled' or existing_status == 'running' then
        return {0, existing_id}
    end
end
redis.call('HSET', keys_hash, dedup_key, task_id)
local hkey = prefix .. ':task:' .. task_id
redis.call('HSET', hkey, unpack(ARGV, 11))
redis.call('SADD', prefix .. ':tasks', task_id)
redis.call('SADD', prefix .. ':status:' .. status, task_id)
redis.call('SADD', KEYS[3], task_id)
if expires_score > 0 then
    redis.call('ZADD', prefix .. ':maintenance:expiry', expires_score, task_id)
end
if due == '1' then
    redis.call('ZADD', ready, score, task_id)
    if publish == '1' then
        redis.call('PUBLISH', channel, notify_payload)
    end
else
    redis.call('ZADD', scheduled, score, task_id)
end
return {1, task_id}
"""
_TRANSITION_SCRIPT = """
local hkey = KEYS[1]
local ready = KEYS[2]
local scheduled = KEYS[3]
local prefix = ARGV[1]
local task_id = ARGV[2]
local expected_status = ARGV[3]
local expected_retry = ARGV[4]
local new_status = ARGV[5]
local zset_action = ARGV[6]
local score = ARGV[7]
local channel = ARGV[8]
local payload = ARGV[9]
local expected_worker = ARGV[10]

local status = redis.call('HGET', hkey, 'status')
if not status then
    return {0}
end
if expected_status ~= '' and status ~= expected_status then
    return {0}
end
if expected_retry ~= '' then
    local retry_count = redis.call('HGET', hkey, 'retry_count')
    if retry_count ~= expected_retry then
        return {0}
    end
end
if expected_worker ~= '' then
    local worker_id = redis.call('HGET', hkey, 'worker_id')
    if worker_id ~= expected_worker then
        return {0}
    end
end
if new_status ~= '' then
    redis.call('SREM', prefix .. ':status:' .. status, task_id)
    redis.call('SADD', prefix .. ':status:' .. new_status, task_id)
    redis.call('HSET', hkey, 'status', new_status)
end
if #ARGV >= 11 then
    redis.call('HSET', hkey, unpack(ARGV, 11))
end
if zset_action == 'ready' then
    redis.call('ZADD', ready, score, task_id)
    redis.call('ZREM', scheduled, task_id)
elseif zset_action == 'scheduled' then
    redis.call('ZADD', scheduled, score, task_id)
    redis.call('ZREM', ready, task_id)
elseif zset_action == 'remove' then
    redis.call('ZREM', ready, task_id)
    redis.call('ZREM', scheduled, task_id)
end
if channel ~= '' then
    redis.call('PUBLISH', channel, payload)
end
local final_status = new_status ~= '' and new_status or status
if final_status == 'running' then
    local heartbeat_score = redis.call('HGET', hkey, 'heartbeat_score')
        or redis.call('HGET', hkey, 'started_score') or '0'
    redis.call('ZADD', prefix .. ':maintenance:running', heartbeat_score, task_id)
else
    redis.call('ZREM', prefix .. ':maintenance:running', task_id)
end
if final_status == 'completed' or final_status == 'failed' or final_status == 'cancelled'
        or final_status == 'expired' then
    local completed_score = redis.call('HGET', hkey, 'completed_score') or '0'
    redis.call('ZADD', prefix .. ':maintenance:terminal', completed_score, task_id)
else
    redis.call('ZREM', prefix .. ':maintenance:terminal', task_id)
end
if final_status == 'pending' or final_status == 'scheduled' then
    local expires_score = tonumber(redis.call('HGET', hkey, 'expires_score')) or 0
    if expires_score > 0 then
        redis.call('ZADD', prefix .. ':maintenance:expiry', expires_score, task_id)
    else
        redis.call('ZREM', prefix .. ':maintenance:expiry', task_id)
    end
else
    redis.call('ZREM', prefix .. ':maintenance:expiry', task_id)
end
local execution_ref = redis.call('HGET', hkey, 'execution_ref')
if execution_ref and execution_ref ~= ''
        and (final_status == 'pending' or final_status == 'scheduled' or final_status == 'running') then
    local external_score
    if final_status == 'running' then
        external_score = redis.call('HGET', hkey, 'started_score') or redis.call('HGET', hkey, 'created_score') or '0'
    else
        external_score = redis.call('HGET', hkey, 'created_score') or '0'
    end
    redis.call('ZADD', prefix .. ':maintenance:external', external_score, task_id)
else
    redis.call('ZREM', prefix .. ':maintenance:external', task_id)
end
return {1}
"""
_DELETE_TERMINAL_SCRIPT = """
local hkey = KEYS[1]
local prefix = ARGV[1]
local task_id = ARGV[2]
local status = redis.call('HGET', hkey, 'status')
if status ~= 'completed' and status ~= 'failed' and status ~= 'cancelled' and status ~= 'expired' then
    return {0}
end
local dedup_key = redis.call('HGET', hkey, 'key')
local queue_index_key = redis.call('HGET', hkey, 'queue_index_key')
redis.call('DEL', hkey)
redis.call('SREM', prefix .. ':tasks', task_id)
redis.call('ZREM', prefix .. ':ready', task_id)
redis.call('ZREM', prefix .. ':scheduled', task_id)
redis.call('SREM', prefix .. ':status:' .. status, task_id)
redis.call('ZREM', prefix .. ':maintenance:running', task_id)
redis.call('ZREM', prefix .. ':maintenance:external', task_id)
redis.call('ZREM', prefix .. ':maintenance:terminal', task_id)
redis.call('ZREM', prefix .. ':maintenance:expiry', task_id)
if queue_index_key and queue_index_key ~= '' then
    redis.call('SREM', queue_index_key, task_id)
end
if dedup_key and dedup_key ~= '' then
    if redis.call('HGET', prefix .. ':keys', dedup_key) == task_id then
        redis.call('HDEL', prefix .. ':keys', dedup_key)
    end
end
return {1}
"""
_QUEUE_STATISTICS_SCRIPT = """
local counts = {}
for index = 2, #KEYS do
    counts[index - 1] = redis.call('SINTERCARD', 2, KEYS[1], KEYS[index])
end
return counts
"""
_RESERVE_IDENTITY_SCRIPT = """
local existing = redis.call('HGET', KEYS[1], ARGV[1])
if existing then
    return existing
end
redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
return false
"""


_RESET_IDENTITY_SCRIPT = """
local existing = redis.call('HGET', KEYS[1], ARGV[1])
if not existing then
    return {0}
end
if ARGV[2] ~= '' then
    local ok, owner = pcall(cjson.decode, existing)
    if not ok or tostring(owner.task_id) ~= ARGV[2] then
        return {0}
    end
end
return {redis.call('HDEL', KEYS[1], ARGV[1])}
"""


_RESERVE_EXTERNAL_DISPATCH_SCRIPT = """
local hkey = KEYS[1]
local status = redis.call('HGET', hkey, 'status')
if status ~= 'pending' and status ~= 'scheduled' then
    return {0}
end
local now = tonumber(ARGV[2])
local scheduled = tonumber(redis.call('HGET', hkey, 'scheduled_score')) or 0
local expires = tonumber(redis.call('HGET', hkey, 'expires_score')) or 0
local execution_ref = redis.call('HGET', hkey, 'execution_ref')
if ARGV[6] ~= '' and redis.call('HGET', hkey, 'retry_count') ~= ARGV[6] then
    return {0}
end
if scheduled > now or (expires > 0 and expires <= now)
        or (execution_ref and execution_ref ~= '') then
    return {0}
end
redis.call(
    'HSET',
    hkey,
    'execution_backend', ARGV[3],
    'execution_profile', ARGV[4],
    'execution_ref', ARGV[5]
)
redis.call('ZREM', KEYS[2], ARGV[1])
local created = tonumber(redis.call('HGET', hkey, 'created_score')) or 0
redis.call('ZADD', KEYS[3], created, ARGV[1])
return {1}
"""


_RELEASE_EXTERNAL_DISPATCH_SCRIPT = """
local hkey = KEYS[1]
if redis.call('HGET', hkey, 'execution_ref') ~= ARGV[2] then
    return {0}
end
redis.call(
    'HSET',
    hkey,
    'execution_backend', ARGV[3],
    'execution_profile', ARGV[4],
    'execution_ref', ''
)
redis.call('ZREM', KEYS[3], ARGV[1])
local status = redis.call('HGET', hkey, 'status')
local expires = tonumber(redis.call('HGET', hkey, 'expires_score')) or 0
if (status == 'pending' or status == 'scheduled') and expires > 0 then
    redis.call('ZADD', KEYS[2], expires, ARGV[1])
end
return {1}
"""

_FINALIZE_EXTERNAL_DISPATCH_SCRIPT = """
local hkey = KEYS[1]
if redis.call('HGET', hkey, 'execution_ref') ~= ARGV[1] then
    return {0}
end
local status = redis.call('HGET', hkey, 'status')
if status ~= 'pending' and status ~= 'scheduled' then
    return {0}
end
redis.call(
    'HSET',
    hkey,
    'execution_backend', ARGV[2],
    'execution_profile', ARGV[3],
    'execution_ref', ARGV[4]
)
return {1}
"""


_RELEASE_MAINTENANCE_SCRIPT = """
if redis.call('GET', KEYS[1]) == ARGV[1] then
    return {redis.call('DEL', KEYS[1])}
end
return {0}
"""


_CHECK_MAINTENANCE_INDEX_SCRIPT = """
local current = redis.call('GET', KEYS[1])
if current then
    return {current}
end
if redis.call('SCARD', KEYS[2]) == 0 then
    redis.call('SET', KEYS[1], ARGV[1])
    return {ARGV[1]}
end
return {''}
"""


[docs] class RedisQueueBackend(BaseQueueBackend): """Queue backend that stores records in a Redis-protocol key-value server. Ready work lives in one global ``{prefix}:ready`` sorted set scored priority-major / created_at-minor, so the claim pops the globally-correct next task with one ordered ``ZRANGE`` instead of a Python-side sort over an ``HGETALL`` of every due candidate. A separate ``{prefix}:scheduled`` sorted set scored by ``scheduled_at`` preserves exact delayed-promotion due-gating: the claim script promotes now-due scheduled ids into ``ready`` before scanning, so future-scheduled tasks are never claimable early. Keeping one global ``ready`` set rather than per-queue sets makes the claim a single ``EVAL`` with no queue enumeration; the queue and execution_backend filters skip non-matching top entries inside the script. Ready scores are IEEE-754 doubles, exact for integers up to 2^53. With stride ``1e13`` and ``created_ms`` near ``1.7e12`` the priority band ``(-priority) * 1e13`` stays exact for ``abs(priority) <= 450``, far beyond realistic priorities; ties break on ``created_ms`` ascending at millisecond resolution. All Lua scripts build their keys from a ``key_prefix`` ARG via string concatenation, which is single-node/replica only. Redis Cluster is out of scope: multi-key scripts on a cluster require same-slot hash-tagged keys and no hash-tag support is added. The composite-score ``ready``/``scheduled`` layout replaces the old ``{prefix}:pending`` zset outright with no data migration; records enqueued under the old layout are stranded (benchmark namespaces are ephemeral). """ _backend_name: "ClassVar[str]" = "redis" __slots__ = ( "_client", "_completion_lock", "_completion_pubsub", "_completion_reader_task", "_completion_waiters", "_control_channel", "_control_pending_read", "_control_pubsub", "_event_log", "_key_prefix", "_notifications", "_owns_client", "_pending_read", "_pubsub", "_url", "_wakeup_channel", )
[docs] def __init__( self, config: "QueueConfig | None" = None, *, backend_config: "_RedisBackendConfig | None" = None ) -> "None": super().__init__(config=config) backend_config = backend_config or _RedisBackendConfig() self._client: "ClientLike | None" = cast("ClientLike | None", backend_config.client) self._owns_client = self._client is None self._url = backend_config.url key_prefix = ( backend_config.key_prefix if backend_config.key_prefix is not None else config.names.root if config is not None else "litestar_queues" ) self._key_prefix = key_prefix.rstrip(":") self._notifications = backend_config.worker_wakeups self._wakeup_channel = ( backend_config.wakeup_channel if backend_config.wakeup_channel is not None else config.names.channel("worker_wakeups") if config is not None else "litestar_queues:worker_wakeups" ) self._control_channel = ( config.names.channel("worker_control") if config is not None else "litestar_queues:worker_control" ) self._pubsub: "PubSubLike | None" = None self._pending_read = PendingNativeRead() self._control_pubsub: "PubSubLike | None" = None self._control_pending_read = PendingNativeRead() self._completion_lock = asyncio.Lock() self._completion_pubsub: "PubSubLike | None" = None self._completion_reader_task: "asyncio.Task[None] | None" = None self._completion_waiters: "dict[str, set[asyncio.Future[bool]]]" = {} self._event_log: "RedisQueueEventLog | None" = None
@property def capabilities(self) -> "QueueBackendCapabilities": """Backend behavior capabilities.""" return QueueBackendCapabilities( supports_worker_wakeups=self._notifications, wakeup_backend=f"{self._backend_name}-pubsub" if self._notifications else None, wakeups_durable=False, supports_completion_events=self._notifications, supports_maintenance=True, )
[docs] async def open(self) -> "bool": """Open Redis-protocol client resources. Returns: True when the client is ready. """ if self._client is None: self._client = self._create_client(self._url) self._owns_client = True await self._require_maintenance_indexes() return True
[docs] async def close(self) -> "None": """Close owned Redis-protocol client resources.""" if self._event_log is not None: await self._event_log.flush_events() await self._pending_read.aclose() await self._control_pending_read.aclose() await self._close_completion_subscriber() if self._pubsub is not None: await _close_pubsub(self._pubsub, self._wakeup_channel) self._pubsub = None if self._control_pubsub is not None: await _close_pubsub(self._control_pubsub, self._control_channel) self._control_pubsub = None if self._owns_client and self._client is not None: close = getattr(self._client, "aclose", None) or getattr(self._client, "close", None) if close is not None: result = close() if inspect.isawaitable(result): await result self._client = None
[docs] def get_event_log(self, config: "EventHistoryConfig") -> "RedisQueueEventLog": if self._event_log is None: self._event_log = RedisQueueEventLog(backend=self, config=config) return self._event_log
[docs] async def enqueue( self, task_name: "str", *, args: "tuple[Any, ...]" = (), kwargs: "dict[str, Any] | None" = None, queue: "str" = "default", priority: "int" = 0, max_retries: "int" = 0, scheduled_at: "datetime | None" = None, expires_at: "datetime | None" = None, key: "str | None" = None, execution_backend: "str" = "local", execution_profile: "str | None" = None, metadata: "dict[str, Any] | None" = None, id: "UUID | None" = None, # noqa: A002 ) -> "QueuedTaskRecord": """Persist a queued task. Returns: The created or deduplicated queued task record. """ record = self._create_record( task_name, args=args, kwargs=kwargs, queue=queue, priority=priority, max_retries=max_retries, scheduled_at=scheduled_at, expires_at=expires_at, key=key, execution_backend=execution_backend, execution_profile=execution_profile, metadata=metadata, ) if id is not None: record.id = id if key is not None: return await self._enqueue_keyed(record, key, publish=True) await self._save_new_record(record, publish=True) return record
[docs] async def enqueue_many(self, requests: "Sequence[TaskRequest]") -> "list[QueuedTaskRecord]": """Persist a batch of Redis-backed tasks and coalesce worker wakeups. Returns: Queue task records in the same order as ``requests``. """ if not requests: return [] results: "list[QueuedTaskRecord]" = [] unkeyed_records: "list[QueuedTaskRecord]" = [] for request in requests: if request.key is not None: record = self._create_record( request.task_name, args=request.args, kwargs=request.kwargs, queue=request.queue, priority=request.priority, max_retries=request.max_retries, scheduled_at=request.scheduled_at, expires_at=request.expires_at, key=request.key, execution_backend=request.execution_backend, execution_profile=request.execution_profile, metadata=request.metadata, ) results.append(await self._enqueue_keyed(record, request.key, publish=False)) continue record = self._create_record( request.task_name, args=request.args, kwargs=request.kwargs, queue=request.queue, priority=request.priority, max_retries=request.max_retries, scheduled_at=request.scheduled_at, expires_at=request.expires_at, key=None, execution_backend=request.execution_backend, execution_profile=request.execution_profile, metadata=request.metadata, ) unkeyed_records.append(record) results.append(record) if unkeyed_records: await self._save_new_records(unkeyed_records, publish=False) await self.notify_new_tasks(results) self._record_enqueue_batch(len(requests)) return results
[docs] async def get_task(self, task_id: "UUID") -> "QueuedTaskRecord | None": """Return a queued task by ID.""" mapping = await self._client_hgetall(self._task_key(task_id)) if not mapping: return None return self._record_from_mapping(mapping)
[docs] async def get_task_by_key(self, key: "str") -> "QueuedTaskRecord | None": """Return a queued task by deduplication key.""" task_id = await self._client_hget(self._keys_key, key) if task_id is None: return None return await self.get_task(UUID(str(_decode(task_id))))
[docs] async def list_pending( self, *, limit: "int" = 1, queue: "str | None" = None, execution_backend: "str | None" = None ) -> "list[QueuedTaskRecord]": """Return due pending or scheduled tasks ordered for execution.""" client = await self._get_client() now_ms = _utc_now().timestamp() * 1000.0 ready_ids = await client.zrange(self._ready_key, 0, -1) scheduled_ids = await client.zrangebyscore(self._scheduled_key, "-inf", now_ms) candidate_ids = [*ready_ids, *scheduled_ids] due_records = [ record for record in await self._records_from_ids(candidate_ids) if record.status in _DUE_STATUSES and record.is_due and not record.is_expired and not is_external_dispatch_reservation(record.execution_ref) and (queue is None or record.queue == queue) and (execution_backend is None or record.execution_backend == execution_backend) ] due_records.sort(key=lambda record: (-record.priority, record.queued_at, record.created_at, record.id.int)) return due_records[:limit]
[docs] async def claim_task( self, task_id: "UUID", *, expected_retry_count: "int | None" = None, expected_execution_ref: "str | None" = None ) -> "QueuedTaskRecord | None": claimed, _ = await self.claim_task_with_expired( task_id, expected_retry_count=expected_retry_count, expected_execution_ref=expected_execution_ref ) return claimed
[docs] async def claim_task_with_expired( self, task_id: "UUID", *, expected_retry_count: "int | None" = None, expected_execution_ref: "str | None" = None ) -> "tuple[QueuedTaskRecord | None, QueuedTaskRecord | None]": """Atomically claim a pending task via a single fenced script. Returns: The claimed record and the expired record, at most one of which is set. """ client = await self._get_client() now = _utc_now() outcome = await _eval_script( client, _CLAIM_TASK_SCRIPT, [self._task_key(task_id), self._ready_key, self._scheduled_key], [ self._key_prefix, str(task_id), repr(_maintenance_score(now)), _serialize_datetime(now), EXTERNAL_DISPATCH_RESERVATION_PREFIX, "" if expected_retry_count is None else str(expected_retry_count), expected_execution_ref or "", ], ) if not outcome: return None, None result = int(outcome[0]) if result == _CLAIMED_OUTCOME: return await self.get_task(task_id), None if result == _EXPIRED_OUTCOME: return None, await self.get_task(task_id) return None, None
[docs] async def claim_many( self, *, limit: "int", queues: "tuple[str, ...]" = (), execution_backend: "str | None" = None, queue_limits: "Mapping[str, int] | None" = None, ) -> "list[QueuedTaskRecord]": """Claim up to ``limit`` due tasks in a single fenced ``EVAL``. Returns: Claimed task records in claim order. """ claimed, _ = await self.claim_many_with_expired( limit=limit, queues=queues, execution_backend=execution_backend, queue_limits=queue_limits ) return claimed
[docs] async def claim_many_with_expired( self, *, limit: "int", queues: "tuple[str, ...]" = (), execution_backend: "str | None" = None, queue_limits: "Mapping[str, int] | None" = None, ) -> "tuple[list[QueuedTaskRecord], list[QueuedTaskRecord]]": """Claim records and report expirations owned by the same Lua script.""" if limit <= 0: return [], [] client = await self._get_client() now = _utc_now() window = max(limit * 2, limit + 10) caps = sorted((queue_limits or {}).items()) args = [ self._key_prefix, repr(now.timestamp() * 1000.0), _serialize_datetime(now), str(limit), execution_backend or "", str(window), EXTERNAL_DISPATCH_RESERVATION_PREFIX, str(len(caps)), *(value for queue, cap in caps for value in (queue, str(cap))), *queues, ] outcome = await _eval_script(client, _CLAIM_SCRIPT, [self._ready_key, self._scheduled_key], args) claimed_ids: "list[str]" = [] expired_ids: "list[str]" = [] for value in outcome or (): kind, _, task_id = _decode(value).partition(":") if kind == "claimed": claimed_ids.append(task_id) elif kind == "expired": expired_ids.append(task_id) claimed = await self._records_from_ids(claimed_ids) if claimed_ids else [] expired = await self._records_from_ids(expired_ids) if expired_ids else [] return claimed, expired
[docs] async def complete_task( self, task_id: "UUID", *, result: "Any" = None, expected_retry_count: "int | None" = None ) -> "QueuedTaskRecord | None": """Mark a task as completed via a single fenced script. Returns: The completed record, if it exists. """ client = await self._get_client() now = _utc_now() outcome = await _eval_script( client, _COMPLETE_SCRIPT, [self._task_key(task_id)], [ self._key_prefix, str(task_id), "" if expected_retry_count is None else str(expected_retry_count), _serialize_datetime(now), _json_dumps(result), self._completion_channel, repr(_maintenance_score(now)), ], ) if not outcome or int(outcome[0]) != 1: return None return await self.get_task(task_id)
[docs] async def fail_task( self, task_id: "UUID", error: "str", *, retry: "bool" = True, expected_retry_count: "int | None" = None, retry_at: "datetime | None" = None, queued_at: "datetime | None" = None, ) -> "QueuedTaskRecord | None": """Mark a task as failed or retry it via a single fenced script. Returns: The updated record, if it exists. """ client = await self._get_client() now = _utc_now() queue_time = queued_at or now score_record = await self.get_task(task_id) if score_record is not None: score_record.queued_at = queue_time outcome = await _eval_script( client, _FAIL_SCRIPT, [self._task_key(task_id), self._ready_key, self._scheduled_key], [ self._key_prefix, str(task_id), "" if expected_retry_count is None else str(expected_retry_count), error, "1" if retry else "0", _serialize_datetime(now), self._completion_channel, repr(_maintenance_score(now)), _serialize_datetime(retry_at), repr(_scheduled_score(retry_at)), _serialize_datetime(queue_time), repr(_ready_score(score_record)) if score_record is not None else "0", ], ) if not outcome or int(outcome[0]) != 1: return None record = await self.get_task(task_id) if record is not None and _decode(outcome[1]) in {"pending", "scheduled"}: await self.notify_new_task(record) return record
[docs] async def assign_worker( self, task_id: "UUID", *, worker_id: "str", expected_retry_count: "int" ) -> "QueuedTaskRecord | None": """Persist running-record ownership through the fenced transition script. Returns: The owned record, or ``None`` when the fence was lost. """ committed = await self._commit_transition( task_id, expected_status="running", expected_retry_count=expected_retry_count, patch={"worker_id": worker_id}, ) return await self.get_task(task_id) if committed else None
[docs] async def interrupt_task( self, task_id: "UUID", *, expected_retry_count: "int", worker_id: "str", queued_at: "datetime" ) -> "QueuedTaskRecord | None": """Return an owned running attempt to pending through the fenced transition script. Returns: The requeued record, or ``None`` when the fence was lost. """ record = await self.get_task(task_id) if ( record is None or record.status != "running" or record.retry_count != expected_retry_count or record.worker_id != worker_id ): return None record.status = "pending" record.queued_at = queued_at record.scheduled_at = None record.started_at = None record.heartbeat_at = None record.completed_at = None record.execution_ref = None record.worker_id = None record.metadata["interruptions"] = interruption_count(record) + 1 record.retry_count += 1 zset_action, score = self._index_action(record) committed = await self._commit_transition( task_id, expected_status="running", expected_retry_count=expected_retry_count, expected_worker_id=worker_id, new_status="pending", patch={ "queued_at": _serialize_datetime(queued_at), "scheduled_at": "", "started_at": "", "started_score": "0", "heartbeat_at": "", "heartbeat_score": "0", "completed_at": "", "completed_score": "0", "execution_ref": "", "worker_id": "", "retry_count": str(record.retry_count), "metadata": _json_dumps(record.metadata), "ready_score": repr(_ready_score(record)), }, zset_action=zset_action, score=score, publish_channel=self._wakeup_channel if (self._notifications and zset_action == "ready") else "", publish_payload=_json_dumps({"event": "task_available"}), ) return await self.get_task(task_id) if committed else None
[docs] async def cancel_task( self, task_id: "UUID", *, include_running: "bool" = False, expected_retry_count: "int | None" = None ) -> "bool": """Cancel a task via a single fenced script. Returns: True when the task was cancelled. """ record = await self.get_task(task_id) cancellable_statuses = (*_DUE_STATUSES, "running") if include_running else _DUE_STATUSES if record is None or record.status not in cancellable_statuses: return False return await self._commit_cancel(record, expected_retry_count=expected_retry_count)
[docs] async def cancel_tasks( self, *, task_name: "str | None" = None, queue: "str | None" = None, kwargs: "Mapping[str, Any] | None" = None, metadata: "Mapping[str, Any] | None" = None, include_running: "bool" = False, ) -> "int": """Cancel tasks matching a domain predicate. Returns: Number of records cancelled. """ statuses = tuple(sorted((*_DUE_STATUSES, "running") if include_running else _DUE_STATUSES)) cancelled = 0 for record in await self._list_records_by_statuses(statuses): if not record_matches_filters(record, task_name=task_name, queue=queue, kwargs=kwargs, metadata=metadata): continue latest = await self.get_task(record.id) if latest is None or latest.status not in statuses: continue if not record_matches_filters(latest, task_name=task_name, queue=queue, kwargs=kwargs, metadata=metadata): continue if await self._commit_cancel(latest, expected_retry_count=None): cancelled += 1 return cancelled
async def _commit_cancel(self, record: "QueuedTaskRecord", expected_retry_count: "int | None" = None) -> "bool": now = _utc_now() return await self._commit_transition( record.id, expected_status=record.status, new_status="cancelled", expected_retry_count=expected_retry_count, patch={ "completed_at": _serialize_datetime(now), "completed_score": repr(_maintenance_score(now)), "heartbeat_at": "", "heartbeat_score": "0", }, zset_action="remove", publish_channel=self._completion_channel if self._notifications else "", publish_payload=str(record.id), )
[docs] async def touch_heartbeats(self, touches: "Sequence[HeartbeatTouch]") -> "HeartbeatTouchResult": """Update heartbeat timestamps for running tasks. Returns: The task IDs confirmed touched or missed by the backend. """ result = HeartbeatTouchResult() if not touches: return result client = await self._get_client() pipeline = _create_pipeline(client) now = _utc_now() heartbeat_at = _serialize_datetime(now) heartbeat_score = repr(_maintenance_score(now)) for touch in touches: expected_retry_count = "" if touch.expected_retry_count is None else str(touch.expected_retry_count) metadata_patch = _json_dumps(touch.metadata_patch) if touch.metadata_patch else "" pipeline.eval( _TOUCH_HEARTBEAT_SCRIPT, 1, self._task_key(touch.task_id), expected_retry_count, heartbeat_at, metadata_patch, heartbeat_score, self._key_prefix, str(touch.task_id), ) outcomes = await _execute_pipeline(pipeline) for touch, outcome in zip(touches, outcomes, strict=True): if int(outcome) == 1: result.touched_task_ids.add(touch.task_id) else: result.missed_task_ids.add(touch.task_id) return result
[docs] async def null_heartbeats(self, task_ids: "list[UUID]", *, expected_retry_count: "int | None" = None) -> "None": """Clear heartbeat timestamps for task IDs via a fenced script.""" for task_id in task_ids: await self._commit_transition( task_id, expected_status="", expected_retry_count=expected_retry_count, patch={"heartbeat_at": "", "heartbeat_score": "0"}, )
[docs] async def requeue_stale_running( self, *, stale_after: "timedelta", limit: "int | None" = None ) -> "StaleTaskRecoveryResult": """Requeue running tasks with stale heartbeats. Candidates are ordered oldest-heartbeat-first (then by id) and capped at ``limit`` before any mutation so one maintenance batch is bounded. Returns: Summary of recovered records. """ cutoff = _utc_now() - stale_after result = StaleTaskRecoveryResult() if limit is not None and limit <= 0: return result if limit is None: records = await self._list_records_by_statuses(("running",)) else: await self._require_maintenance_indexes() client = await self._get_client() task_ids = await client.zrangebyscore( self._maintenance_running_key, "-inf", f"({_maintenance_score(cutoff)}", start=0, num=limit ) records = await self._records_from_ids(task_ids) candidates = [ record for record in records if record.status == "running" and (record.heartbeat_at is None or record.heartbeat_at < cutoff) ] candidates.sort(key=_stale_sort_key) if limit is not None: candidates = candidates[:limit] for record in candidates: latest = await self.get_task(record.id) if latest is None or latest.status != "running": result.skipped += 1 continue if latest.heartbeat_at is not None and latest.heartbeat_at >= cutoff: result.skipped += 1 continue requeue_on_stale = latest.metadata.get("requeue_on_stale", True) is not False if requeue_on_stale and attempts_consumed(latest) < latest.max_retries: if await self._commit_stale_requeue(latest): result.requeued += 1 else: result.skipped += 1 elif await self._commit_stale_failure(latest): result.failed += 1 result.failed_task_ids.append(latest.id) if not requeue_on_stale: result.handler_needed += 1 result.handler_needed_task_ids.append(latest.id) else: result.skipped += 1 return result
async def _commit_stale_requeue(self, record: "QueuedTaskRecord") -> "bool": expected_retry = record.retry_count queued_at, retry_at = retry_schedule(record) record.status = "scheduled" if retry_at is not None else "pending" record.queued_at = queued_at record.scheduled_at = retry_at record.priority = stale_requeue_priority(record.priority, self._stale_requeue_priority_policy()) record.started_at = None record.heartbeat_at = None record.error = stale_requeue_error(record.error) record.retry_count += 1 zset_action, score = self._index_action(record) return await self._commit_transition( record.id, expected_status="running", expected_retry_count=expected_retry, new_status=record.status, patch={ "priority": str(record.priority), "started_at": "", "started_score": "0", "heartbeat_at": "", "heartbeat_score": "0", "error": record.error or "", "retry_count": str(record.retry_count), "queued_at": _serialize_datetime(queued_at), "scheduled_at": _serialize_datetime(retry_at), "ready_score": repr(_ready_score(record)), }, zset_action=zset_action, score=score, publish_channel=self._wakeup_channel if (self._notifications and zset_action == "ready") else "", publish_payload=_json_dumps({"event": "task_available"}), ) async def _commit_stale_failure(self, record: "QueuedTaskRecord") -> "bool": now = _utc_now() record.status = "failed" record.completed_at = now record.heartbeat_at = None record.error = STALE_HEARTBEAT_ERROR return await self._commit_transition( record.id, expected_status="running", new_status="failed", patch={ "completed_at": _serialize_datetime(now), "completed_score": repr(_maintenance_score(now)), "heartbeat_at": "", "heartbeat_score": "0", "error": STALE_HEARTBEAT_ERROR, }, zset_action="remove", )
[docs] async def set_execution_ref( self, task_id: "UUID", execution_backend: "str", execution_ref: "str", *, execution_profile: "str | None" = None ) -> "QueuedTaskRecord | None": """Persist an external execution reference for a running task via a fenced script. Returns: The updated record, if it exists. """ record = await self.get_task(task_id) if record is None: return None committed = await self._commit_transition( task_id, expected_status="", patch={ "execution_backend": execution_backend, "execution_profile": execution_profile or "", "execution_ref": execution_ref or "", }, ) if not committed: return None record.execution_backend = execution_backend record.execution_profile = execution_profile record.execution_ref = execution_ref return record
[docs] async def reserve_external_dispatch( self, task_id: "UUID", execution_backend: "str", reservation_ref: "str", *, execution_profile: "str | None" = None, expected_retry_count: "int | None" = None, ) -> "QueuedTaskRecord | None": client = await self._get_client() outcome = await _eval_script( client, _RESERVE_EXTERNAL_DISPATCH_SCRIPT, [self._task_key(task_id), self._maintenance_expiry_key, self._maintenance_external_key], [ str(task_id), repr(_maintenance_score(_utc_now())), execution_backend, execution_profile or "", reservation_ref, "" if expected_retry_count is None else str(expected_retry_count), ], ) if not outcome or int(outcome[0]) != 1: return None return await self.get_task(task_id)
[docs] async def clear_execution_ref( self, task_id: "UUID", expected_retry_count: "int", expected_execution_ref: "str" ) -> "QueuedTaskRecord | None": client = await self._get_client() outcome = await _eval_script( client, _CLEAR_EXECUTION_REF_SCRIPT, [self._task_key(task_id)], [str(expected_retry_count), expected_execution_ref], ) if not outcome or int(outcome[0]) != 1: return None record = await self.get_task(task_id) if record is not None: await self.notify_new_task(record) return record
[docs] async def replace_execution_ref( self, task_id: "UUID", expected_retry_count: "int", expected_execution_ref: "str", execution_ref: "str" ) -> "QueuedTaskRecord | None": client = await self._get_client() outcome = await _eval_script( client, _REPLACE_EXECUTION_REF_SCRIPT, [self._task_key(task_id)], [str(expected_retry_count), expected_execution_ref, execution_ref], ) if not outcome or int(outcome[0]) != 1: return None return await self.get_task(task_id)
[docs] async def release_external_dispatch( self, task_id: "UUID", reservation_ref: "str", execution_backend: "str", *, execution_profile: "str | None" = None, ) -> "QueuedTaskRecord | None": client = await self._get_client() outcome = await _eval_script( client, _RELEASE_EXTERNAL_DISPATCH_SCRIPT, [self._task_key(task_id), self._maintenance_expiry_key, self._maintenance_external_key], [str(task_id), reservation_ref, execution_backend, execution_profile or ""], ) if not outcome or int(outcome[0]) != 1: return None record = await self.get_task(task_id) if record is not None: await self.notify_new_task(record) return record
[docs] async def finalize_external_dispatch( self, task_id: "UUID", reservation_ref: "str", execution_backend: "str", execution_ref: "str", *, execution_profile: "str | None" = None, ) -> "QueuedTaskRecord | None": client = await self._get_client() outcome = await _eval_script( client, _FINALIZE_EXTERNAL_DISPATCH_SCRIPT, [self._task_key(task_id)], [reservation_ref, execution_backend, execution_profile or "", execution_ref], ) if not outcome or int(outcome[0]) != 1: return None return await self.get_task(task_id)
[docs] async def set_execution_backend( self, task_id: "UUID", execution_backend: "str", *, execution_profile: "str | None" = None ) -> "QueuedTaskRecord | None": """Persist an execution backend/profile change for a queued task via a fenced script. Returns: The updated record, if it exists. """ record = await self.get_task(task_id) if record is None: return None record.execution_backend = execution_backend record.execution_profile = execution_profile record.execution_ref = None due = record.status in _DUE_STATUSES and record.is_due await self._commit_transition( task_id, expected_status="", patch={ "execution_backend": execution_backend, "execution_profile": execution_profile or "", "execution_ref": "", }, publish_channel=self._wakeup_channel if (self._notifications and due) else "", publish_payload=_json_dumps({"event": "task_available"}), ) return record
[docs] async def list_running_external(self, *, limit: "int | None" = None) -> "list[QueuedTaskRecord]": """Return externally dispatched tasks with references to reconcile.""" if limit is not None and limit <= 0: return [] if limit is None: candidate_records = await self._list_records_by_statuses(("pending", "scheduled", "running")) else: await self._require_maintenance_indexes() client = await self._get_client() task_ids = await client.zrange(self._maintenance_external_key, 0, limit - 1) candidate_records = await self._records_from_ids(task_ids) records = [ record for record in candidate_records if record.status in {"pending", "scheduled", "running"} and record.execution_ref is not None ] records.sort(key=lambda record: (record.started_at or record.created_at, str(record.id))) return records[:limit] if limit is not None else records
[docs] async def get_statistics(self, *, queue: "str | None" = None) -> "QueueStatistics": """Return queue status counts.""" client = await self._get_client() statistics = QueueStatistics() statuses = sorted(_STATUS_VALUES) status_keys = [self._status_key(status) for status in statuses] counts = ( await _pipeline_scard(client, status_keys) if queue is None else await _eval_script(client, _QUEUE_STATISTICS_SCRIPT, [self._queue_index_key(queue), *status_keys], []) ) for status, count in zip(statuses, counts, strict=True): setattr(statistics, status, int(count)) return statistics
[docs] async def expire_overdue(self, *, limit: "int | None" = None) -> "list[QueuedTaskRecord]": """Transition overdue pending and scheduled records to ``expired``.""" if limit is not None and limit <= 0: return [] await self._require_maintenance_indexes() client = await self._get_client() now = _utc_now() task_ids = await client.zrangebyscore( self._maintenance_expiry_key, "-inf", _maintenance_score(now), start=0 if limit is not None else None, num=limit, ) records = await self._records_from_ids(task_ids) expired: "list[QueuedTaskRecord]" = [] for record in records: if record.status not in _DUE_STATUSES or not record.is_expired: await client.zrem(self._maintenance_expiry_key, str(record.id)) continue if await self._expire_record(record, now=now): record.status = "expired" record.completed_at = now record.heartbeat_at = None expired.append(record) return expired
[docs] async def list_completed_by_task( self, task_name: "str", *, since: "datetime | None" = None, limit: "int" = 10 ) -> "list[QueuedTaskRecord]": """Return recent completed records for a task name.""" records = [ record for record in await self._list_records_by_statuses(("completed",)) if record.task_name == task_name and record.status == "completed" and record.completed_at is not None and (since is None or record.completed_at >= since) ] records.sort(key=lambda record: record.completed_at or record.created_at, reverse=True) return records[:limit]
[docs] async def cleanup_terminal(self, before: "datetime", *, limit: "int | None" = None) -> "int": """Delete terminal records completed before a cutoff. Candidates are ordered oldest-completion-first (then by id) and capped at ``limit`` before any deletion so one maintenance batch is bounded. Returns: Number of deleted records. """ client = await self._get_client() if limit is not None and limit <= 0: return 0 if limit is None: records = await self._list_records_by_statuses(tuple(sorted(_TERMINAL_STATUSES))) else: await self._require_maintenance_indexes() task_ids = await client.zrangebyscore( self._maintenance_terminal_key, "-inf", f"({_maintenance_score(before)}", start=0, num=limit ) records = await self._records_from_ids(task_ids) candidates = [ record for record in records if record.status in _TERMINAL_STATUSES and record.completed_at is not None and record.completed_at < before ] candidates.sort(key=lambda record: (cast("datetime", record.completed_at), str(record.id))) if limit is not None: candidates = candidates[:limit] count = 0 for record in candidates: outcome = await _eval_script( client, _DELETE_TERMINAL_SCRIPT, [self._task_key(record.id)], [self._key_prefix, str(record.id)] ) if outcome and int(outcome[0]) == 1: count += 1 return count
[docs] async def rebuild_maintenance_indexes(self) -> "int": """Rebuild ordered maintenance indexes for a populated pre-index namespace. This is an intentionally unbounded, one-time upgrade operation. Stop all queue writers using this Redis/Valkey namespace before calling it. Interrupted calls are safe to retry because the version marker is written only after every task has been reindexed. Returns: Number of queue records indexed. """ client = await self._get_client() records = await self._list_records_by_statuses(tuple(sorted(_STATUS_VALUES))) pipeline = _create_pipeline(client) pipeline.delete( self._maintenance_running_key, self._maintenance_external_key, self._maintenance_terminal_key, self._maintenance_expiry_key, ) for record in records: task_id = str(record.id) timestamp_scores = { "created_score": repr(_maintenance_score(record.created_at)), "started_score": repr(_maintenance_score(record.started_at)), "completed_score": repr(_maintenance_score(record.completed_at)), "heartbeat_score": repr(_maintenance_score(record.heartbeat_at)), } pipeline.hset(self._task_key(record.id), mapping=timestamp_scores) if record.status == "running": pipeline.zadd(self._maintenance_running_key, {task_id: _maintenance_score(record.heartbeat_at)}) if record.status in _DUE_STATUSES and record.expires_at is not None and record.execution_ref is None: pipeline.zadd(self._maintenance_expiry_key, {task_id: _maintenance_score(record.expires_at)}) if record.execution_ref is not None and record.status in {"pending", "scheduled", "running"}: pipeline.zadd( self._maintenance_external_key, {task_id: _maintenance_score(record.started_at or record.created_at)}, ) if record.status in _TERMINAL_STATUSES and record.completed_at is not None: pipeline.zadd(self._maintenance_terminal_key, {task_id: _maintenance_score(record.completed_at)}) await _execute_pipeline(pipeline) marked = client.set(self._maintenance_index_version_key, _MAINTENANCE_INDEX_VERSION) if inspect.isawaitable(marked): await marked return len(records)
[docs] async def acquire_maintenance(self, name: "str", token: "str", *, ttl: "timedelta") -> "bool": """Acquire namespaced ``SET NX PX`` maintenance ownership. Returns: True when ownership was set for ``token``. """ client = await self._get_client() ttl_ms = max(1, int(ttl.total_seconds() * 1000)) result = client.set(self._maintenance_key(name), token, nx=True, px=ttl_ms) if inspect.isawaitable(result): result = await result return bool(result)
[docs] async def release_maintenance(self, name: "str", token: "str") -> "bool": """Release maintenance ownership via token-checked Lua compare-and-delete. Returns: True when ownership held under ``token`` was deleted. """ client = await self._get_client() outcome = await _eval_script(client, _RELEASE_MAINTENANCE_SCRIPT, [self._maintenance_key(name)], [token]) return bool(outcome and int(outcome[0]) == 1)
[docs] async def reserve_identity(self, key: "str", *, task_id: "UUID", task_name: "str") -> "TaskReservation | None": """Reserve a forever identity via an atomic HGET-or-HSET script. The task-reservation hash is separate from ``:task:``/``:keys`` and is never touched by terminal cleanup. Returns: ``None`` when this caller won the reservation; otherwise the existing owner reservation. """ client = await self._get_client() created_at = _utc_now() payload = _json_dumps({ "key": key, "task_id": str(task_id), "task_name": task_name, "created_at": _serialize_datetime(created_at), }) result = client.eval(_RESERVE_IDENTITY_SCRIPT, 1, self._task_reservation_key, key, payload) if inspect.isawaitable(result): result = await result if result is None or result is False: return None return _reservation_from_payload(_decode(result))
[docs] async def has_identity(self, key: "str") -> "TaskReservation | None": """Return the reservation owning a reserved forever identity, if any.""" raw = await self._client_hget(self._task_reservation_key, key) if raw is None: return None return _reservation_from_payload(_decode(raw))
[docs] async def reset_identity(self, key: "str", *, expected_task_id: "UUID | None" = None) -> "bool": """Delete a forever identity reservation via atomic compare-and-delete. Args: key: The exact effective identity key. expected_task_id: Optional task owner required for deletion. Returns: ``True`` when a reservation was removed. """ client = await self._get_client() outcome = await _eval_script( client, _RESET_IDENTITY_SCRIPT, [self._task_reservation_key], [key, str(expected_task_id) if expected_task_id is not None else ""], ) return bool(outcome and int(outcome[0]) == 1)
[docs] async def notify_new_task(self, record: "QueuedTaskRecord") -> "None": """Publish a Redis-protocol pub/sub message when work is available.""" if self._notifications and record.status in _DUE_STATUSES and record.is_due: payload = _json_dumps({"event": "task_available"}) client = await self._get_client() await client.publish(self._wakeup_channel, payload) self._record_wakeup_emitted()
[docs] async def wait_for_wakeups(self, timeout: "float | None" = None) -> "bool": """Wait for a Redis-protocol pub/sub message when notifications are enabled. A single pub/sub receive is retained across worker poll timeouts; only a real message, a read failure, or backend close ends it. Returns: True when a notification was observed. """ if not self._notifications: return await super().wait_for_wakeups(timeout=timeout) pubsub = await self._get_pubsub() task = await self._pending_read.race(lambda: _receive_pubsub_message(pubsub), timeout) if task is None: return False exc = task.exception() if exc is not None: await self._reset_pubsub() raise exc return bool(task.result())
[docs] async def notify_worker_control(self, worker_id: "str | None") -> "None": """Publish a worker-control hint on the Redis-protocol control channel. The hint is lossy by contract: it only shortens the wait before the owning worker reconciles durable status. """ if not self._notifications: return client = await self._get_client() await client.publish(self._control_channel, _json_dumps({"event": "worker_control", "worker_id": worker_id}))
[docs] async def wait_for_worker_control(self, *, worker_id: "str", timeout: "float | None" = None) -> "bool": """Wait for a Redis-protocol worker-control hint. The control subscription and its pending receive are retained across worker poll timeouts, exactly like the wakeup subscription, and are never shared with it: one in-flight read per subscription. Returns: True when a control hint was observed. Raises: Exception: Whatever the pub/sub receive raised, after the subscription is reset so the next wait reconnects. """ if not self._notifications: return await super().wait_for_worker_control(worker_id=worker_id, timeout=timeout) pubsub = await self._get_control_pubsub() task = await self._control_pending_read.race(lambda: _receive_pubsub_message(pubsub), timeout) if task is None: return False exc = task.exception() if exc is not None: await self._reset_control_pubsub() raise exc return bool(task.result())
[docs] async def time_until_next_due(self, *, queues: "tuple[str, ...]" = ()) -> "float | None": """Return seconds until the earliest not-yet-due scheduled record. Reads the lowest-scored member of the global ``scheduled`` sorted set (scored by ``scheduled_at``): an O(1) lookup independent of queue size. ``queues`` is not applied because the sorted set is not queue-scoped; an unfiltered bound is always safe here (it can only wake the worker sooner than strictly necessary, never later). Returns: Seconds until the next due record, or ``None`` when there is no upcoming scheduled work. """ del queues client = await self._get_client() member_ids = await client.zrange(self._scheduled_key, 0, 0) if not member_ids: return None records = await self._records_from_ids(member_ids) if not records or records[0].scheduled_at is None: return None return max((records[0].scheduled_at - _utc_now()).total_seconds(), 0.0)
async def _reset_pubsub(self) -> "None": """Drop the pub/sub subscription so the next wait re-establishes it.""" await self._pending_read.aclose() pubsub = self._pubsub self._pubsub = None if pubsub is not None: await _close_pubsub(pubsub, self._wakeup_channel) async def _reset_control_pubsub(self) -> "None": """Drop the control subscription so the next wait re-establishes it.""" await self._control_pending_read.aclose() pubsub = self._control_pubsub self._control_pubsub = None if pubsub is not None: await _close_pubsub(pubsub, self._control_channel)
[docs] async def wait_for_completion(self, task_id: "UUID", *, timeout: "float | None" = None) -> "bool": """Wait for a terminal completion message naming ``task_id``. Returns: True when a completion signal for ``task_id`` arrived before the deadline. """ if not self._notifications: return False loop = asyncio.get_running_loop() waiter: "asyncio.Future[bool]" = loop.create_future() target = str(task_id) async with self._completion_lock: await self._ensure_completion_subscriber() self._completion_waiters.setdefault(target, set()).add(waiter) try: record = await self.get_task(task_id) if record is not None and record.status in _TERMINAL_STATUSES: return True if timeout is None: return await waiter try: return await asyncio.wait_for(waiter, timeout=timeout) except asyncio.TimeoutError: return False finally: async with self._completion_lock: waiters = self._completion_waiters.get(target) if waiters is not None: waiters.discard(waiter) if not waiters: self._completion_waiters.pop(target, None)
async def _ensure_completion_subscriber(self) -> "None": """Create the subscriber while ``_completion_lock`` is held.""" if self._completion_reader_task is not None and not self._completion_reader_task.done(): return client = await self._get_client() pubsub = client.pubsub() subscribe = pubsub.subscribe(self._completion_channel) if inspect.isawaitable(subscribe): await subscribe self._completion_pubsub = pubsub self._completion_reader_task = asyncio.create_task( self._read_completion_messages(pubsub), name=f"{self._backend_name}-queue-completions" ) async def _read_completion_messages(self, pubsub: "PubSubLike") -> "None": try: while True: message = await pubsub.get_message(ignore_subscribe_messages=True, timeout=None) if message is None: continue target = str(_decode(message.get("data"))) for waiter in tuple(self._completion_waiters.get(target, ())): if not waiter.done(): waiter.set_result(True) finally: for waiters in tuple(self._completion_waiters.values()): for waiter in tuple(waiters): if not waiter.done(): waiter.set_result(False) async def _close_completion_subscriber(self) -> "None": async with self._completion_lock: reader, self._completion_reader_task = self._completion_reader_task, None pubsub, self._completion_pubsub = self._completion_pubsub, None await self._stop_completion_subscriber(reader, pubsub) async def _stop_completion_subscriber( self, reader: "asyncio.Task[None] | None", pubsub: "PubSubLike | None" ) -> "None": if reader is not None: reader.cancel() with suppress(asyncio.CancelledError): await reader if pubsub is not None: await _close_pubsub(pubsub, self._completion_channel) def _create_client(self, url: "str") -> "ClientLike": from redis import asyncio as redis_asyncio return cast("ClientLike", redis_asyncio.from_url(url, decode_responses=True)) # type: ignore[no-untyped-call] async def _get_client(self) -> "ClientLike": if self._client is None: await self.open() return cast("ClientLike", self._client) async def _get_pubsub(self) -> "PubSubLike": if self._pubsub is None: client = await self._get_client() self._pubsub = client.pubsub() subscribe = self._pubsub.subscribe(self._wakeup_channel) if inspect.isawaitable(subscribe): await subscribe return self._pubsub async def _get_control_pubsub(self) -> "PubSubLike": if self._control_pubsub is None: client = await self._get_client() self._control_pubsub = client.pubsub() subscribe = self._control_pubsub.subscribe(self._control_channel) if inspect.isawaitable(subscribe): await subscribe return self._control_pubsub async def _commit_transition( self, task_id: "UUID", *, expected_status: "str", new_status: "str" = "", patch: "Mapping[str, str] | None" = None, zset_action: "str" = "none", score: "str" = "", expected_retry_count: "int | None" = None, expected_worker_id: "str" = "", publish_channel: "str" = "", publish_payload: "str" = "", ) -> "bool": client = await self._get_client() args = [ self._key_prefix, str(task_id), expected_status, "" if expected_retry_count is None else str(expected_retry_count), new_status, zset_action, score, publish_channel, publish_payload, expected_worker_id, ] if patch: for field, value in patch.items(): args.append(field) args.append(value) outcome = await _eval_script( client, _TRANSITION_SCRIPT, [self._task_key(task_id), self._ready_key, self._scheduled_key], args ) return bool(outcome and int(outcome[0]) == 1) async def _expire_record(self, record: "QueuedTaskRecord", *, now: "datetime | None" = None) -> "bool": expired_at = now or _utc_now() return await self._commit_transition( record.id, expected_status=record.status, new_status="expired", patch={ "completed_at": _serialize_datetime(expired_at), "completed_score": repr(_maintenance_score(expired_at)), "heartbeat_at": "", "heartbeat_score": "0", }, zset_action="remove", publish_channel=self._completion_channel if self._notifications else "", publish_payload=str(record.id), ) async def _enqueue_keyed(self, record: "QueuedTaskRecord", key: "str", *, publish: "bool") -> "QueuedTaskRecord": client = await self._get_client() args = self._enqueue_args(record, publish=publish) args = [*args[:8], key, *args[8:]] outcome = await _eval_script( client, _ENQUEUE_KEYED_SCRIPT, [self._ready_key, self._scheduled_key, self._queue_index_key(record.queue)], args, ) if int(outcome[0]) == 1: return record existing = await self.get_task(UUID(str(_decode(outcome[1])))) return existing if existing is not None else record def _index_action(self, record: "QueuedTaskRecord") -> "tuple[str, str]": if record.status == "pending" and record.is_due: return "ready", repr(_ready_score(record)) if record.status in _DUE_STATUSES: return "scheduled", repr(_scheduled_score(record.scheduled_at)) return "remove", "" def _create_record( self, task_name: "str", *, args: "tuple[Any, ...]", kwargs: "dict[str, Any] | None", queue: "str", priority: "int", max_retries: "int", scheduled_at: "datetime | None", expires_at: "datetime | None", key: "str | None", execution_backend: "str", execution_profile: "str | None", metadata: "dict[str, Any] | None", ) -> "QueuedTaskRecord": now = _utc_now() return QueuedTaskRecord( task_name=task_name, args=args, kwargs=dict(kwargs or {}), queue=queue, execution_backend=execution_backend, execution_profile=execution_profile, status="scheduled" if scheduled_at is not None and scheduled_at > _utc_now() else "pending", priority=priority, max_retries=max_retries, scheduled_at=scheduled_at, expires_at=expires_at, key=key, metadata=dict(metadata or {}), created_at=now, queued_at=now, ) async def _save_new_record(self, record: "QueuedTaskRecord", *, publish: "bool") -> "None": client = await self._get_client() await _eval_script( client, _ENQUEUE_SCRIPT, [self._ready_key, self._scheduled_key, self._queue_index_key(record.queue)], self._enqueue_args(record, publish=publish), ) async def _save_new_records(self, records: "Sequence[QueuedTaskRecord]", *, publish: "bool") -> "None": if not records: return client = await self._get_client() pipeline = _create_pipeline(client) for record in records: keys = [self._ready_key, self._scheduled_key, self._queue_index_key(record.queue)] pipeline.eval(_ENQUEUE_SCRIPT, len(keys), *keys, *self._enqueue_args(record, publish=publish)) await _execute_pipeline(pipeline) def _enqueue_args(self, record: "QueuedTaskRecord", *, publish: "bool") -> "list[str]": due = record.status == "pending" and record.is_due score = _ready_score(record) if due else _scheduled_score(record.scheduled_at) args = [ self._key_prefix, str(record.id), record.status, "1" if due else "0", repr(score), self._wakeup_channel, _json_dumps({"event": "task_available"}), "1" if publish and self._notifications else "0", repr(_maintenance_score(record.expires_at)), ] for field, value in self._record_to_mapping(record).items(): args.append(field) args.append(value) return args async def _list_records_by_statuses(self, statuses: "tuple[str, ...]") -> "list[QueuedTaskRecord]": client = await self._get_client() member_sets = await _pipeline_smembers(client, [self._status_key(status) for status in statuses]) task_ids = {value for member_set in member_sets for value in member_set} return await self._records_from_ids(tuple(task_ids)) async def _require_maintenance_indexes(self) -> "None": client = await self._get_client() outcome = await _eval_script( client, _CHECK_MAINTENANCE_INDEX_SCRIPT, [self._maintenance_index_version_key, f"{self._key_prefix}:tasks"], [_MAINTENANCE_INDEX_VERSION], ) version = str(_decode(outcome[0])) if outcome else "" if version == _MAINTENANCE_INDEX_VERSION: return msg = ( f"{self._backend_name} maintenance indexes are missing for populated key prefix " f"{self._key_prefix!r}. Stop all queue writers using this namespace and run " "`await backend.rebuild_maintenance_indexes()` once before bounded maintenance." ) raise QueueConfigurationError(msg) async def _records_from_ids(self, task_ids: "Iterable[Any]") -> "list[QueuedTaskRecord]": task_keys = [self._task_key(UUID(str(_decode(value)))) for value in task_ids] mappings = await _pipeline_hgetall(await self._get_client(), task_keys) records: "list[QueuedTaskRecord]" = [] for mapping in mappings: decoded = _decode_mapping(mapping) if decoded: records.append(self._record_from_mapping(decoded)) return records async def _client_hget(self, name: "str", key: "str") -> "Any": client = await self._get_client() return await client.hget(name, key) async def _client_hgetall(self, name: "str") -> "dict[str, Any]": client = await self._get_client() return _decode_mapping(await client.hgetall(name)) @property def _keys_key(self) -> "str": return f"{self._key_prefix}:keys" @property def _task_reservation_key(self) -> "str": return f"{self._key_prefix}:task_reservations" @property def _ready_key(self) -> "str": return f"{self._key_prefix}:ready" @property def _scheduled_key(self) -> "str": return f"{self._key_prefix}:scheduled" @property def _maintenance_running_key(self) -> "str": return f"{self._key_prefix}:maintenance:running" @property def _maintenance_index_version_key(self) -> "str": return f"{self._key_prefix}:maintenance:index-version" @property def _maintenance_external_key(self) -> "str": return f"{self._key_prefix}:maintenance:external" @property def _maintenance_expiry_key(self) -> "str": return f"{self._key_prefix}:maintenance:expiry" @property def _maintenance_terminal_key(self) -> "str": return f"{self._key_prefix}:maintenance:terminal" @property def _completion_channel(self) -> "str": return f"{self._key_prefix}:completions" def _status_key(self, status: "str") -> "str": return f"{self._key_prefix}:status:{status}" def _queue_index_key(self, queue: "str") -> "str": return f"{self._key_prefix}:queue:{hashed_index_value(queue)}" def _task_key(self, task_id: "UUID") -> "str": return f"{self._key_prefix}:task:{task_id}" def _maintenance_key(self, name: "str") -> "str": return f"{self._key_prefix}:maintenance:{name}" def _event_log_global_key(self) -> "str": return f"{self._key_prefix}:events" def _event_log_event_key(self, event_id: "str") -> "str": return f"{self._key_prefix}:events:record:{event_id}" def _event_log_task_key(self, task_id: "str") -> "str": return f"{self._key_prefix}:events:task:{hashed_index_value(task_id)}" def _event_log_task_name_key(self, task_name: "str") -> "str": return f"{self._key_prefix}:events:task_name:{hashed_index_value(task_name)}" def _event_log_event_type_key(self, event_type: "str") -> "str": return f"{self._key_prefix}:events:event_type:{hashed_index_value(event_type)}" def _event_log_scope_key_key(self, scope_key: "str") -> "str": return f"{self._key_prefix}:events:scope_key:{hashed_index_value(scope_key)}" def _event_log_entity_key(self, entity: "str") -> "str": return f"{self._key_prefix}:events:entity:{hashed_index_value(entity)}" def _record_to_mapping(self, record: "QueuedTaskRecord") -> "dict[str, str]": return { "id": str(record.id), "task_name": record.task_name, "args": _json_dumps(list(record.args)), "kwargs": _json_dumps(record.kwargs), "queue": record.queue, "queue_index_key": self._queue_index_key(record.queue), "execution_backend": record.execution_backend, "execution_profile": record.execution_profile or "", "execution_ref": record.execution_ref or "", "worker_id": record.worker_id or "", "status": record.status, "priority": str(record.priority), "max_retries": str(record.max_retries), "retry_count": str(record.retry_count), "scheduled_at": _serialize_datetime(record.scheduled_at), "expires_at": _serialize_datetime(record.expires_at), "expires_score": repr(_maintenance_score(record.expires_at)), "created_at": _serialize_datetime(record.created_at), "created_score": repr(_maintenance_score(record.created_at)), "queued_at": _serialize_datetime(record.queued_at), "started_at": _serialize_datetime(record.started_at), "started_score": repr(_maintenance_score(record.started_at)), "completed_at": _serialize_datetime(record.completed_at), "completed_score": repr(_maintenance_score(record.completed_at)), "heartbeat_at": _serialize_datetime(record.heartbeat_at), "heartbeat_score": repr(_maintenance_score(record.heartbeat_at)), "result": _json_dumps(record.result), "error": record.error or "", "key": record.key or "", "metadata": _json_dumps(record.metadata), "ready_score": repr(_ready_score(record)), } def _record_from_mapping(self, mapping: "dict[str, Any]") -> "QueuedTaskRecord": return QueuedTaskRecord( id=UUID(str(mapping["id"])), task_name=str(mapping["task_name"]), args=tuple(_json_loads(mapping.get("args"), [])), kwargs=dict(_json_loads(mapping.get("kwargs"), {})), queue=str(mapping.get("queue") or "default"), execution_backend=str(mapping.get("execution_backend") or "local"), execution_profile=str(mapping["execution_profile"]) if mapping.get("execution_profile") else None, execution_ref=str(mapping["execution_ref"]) if mapping.get("execution_ref") else None, worker_id=str(mapping["worker_id"]) if mapping.get("worker_id") else None, status=_coerce_status(mapping.get("status")), priority=int(str(mapping.get("priority") or 0)), max_retries=int(str(mapping.get("max_retries") or 0)), retry_count=int(str(mapping.get("retry_count") or 0)), scheduled_at=_deserialize_datetime(mapping.get("scheduled_at")), expires_at=_deserialize_datetime(mapping.get("expires_at")), created_at=_deserialize_datetime(mapping.get("created_at")) or _utc_now(), queued_at=_deserialize_datetime(mapping.get("queued_at")) or _utc_now(), started_at=_deserialize_datetime(mapping.get("started_at")), completed_at=_deserialize_datetime(mapping.get("completed_at")), heartbeat_at=_deserialize_datetime(mapping.get("heartbeat_at")), result=_json_loads(mapping.get("result"), None), error=str(mapping["error"]) if mapping.get("error") else None, key=str(mapping["key"]) if mapping.get("key") else None, metadata=dict(_json_loads(mapping.get("metadata"), {})), )
def _create_pipeline(client: "ClientLike") -> "PipelineLike": try: return client.pipeline(transaction=False) except TypeError: return client.pipeline() async def _execute_pipeline(pipeline: "PipelineLike") -> "list[Any]": result = pipeline.execute() if inspect.isawaitable(result): return list(await result) return list(cast("list[Any]", result)) async def _eval_script(client: "ClientLike", script: "str", keys: "list[str]", args: "list[str]") -> "list[Any]": result = client.eval(script, len(keys), *keys, *args) if inspect.isawaitable(result): result = await result if result is None: return [] return list(cast("list[Any]", result)) async def _pipeline_hgetall(client: "ClientLike", keys: "list[str]") -> "list[dict[Any, Any]]": if not keys: return [] pipeline = _create_pipeline(client) for key in keys: pipeline.hgetall(key) return cast("list[dict[Any, Any]]", await _execute_pipeline(pipeline)) async def _pipeline_smembers(client: "ClientLike", keys: "list[str]") -> "list[set[Any]]": if not keys: return [] pipeline = _create_pipeline(client) for key in keys: pipeline.smembers(key) return [set(result) for result in await _execute_pipeline(pipeline)] async def _pipeline_scard(client: "ClientLike", keys: "list[str]") -> "list[int]": if not keys: return [] pipeline = _create_pipeline(client) for key in keys: pipeline.scard(key) return [int(result) for result in await _execute_pipeline(pipeline)] def _utc_now() -> "datetime": return datetime.now(timezone.utc) _MIN_DATETIME = datetime(1, 1, 1, tzinfo=timezone.utc) def _stale_sort_key(record: "QueuedTaskRecord") -> "tuple[datetime, str]": """Order stale candidates oldest-heartbeat-first, then by record id. Returns: A sort key of (effective heartbeat, record id). """ return (record.heartbeat_at or _MIN_DATETIME, str(record.id)) def _serialize_datetime(value: "datetime | None") -> "str": if value is None: return "" if value.tzinfo is None: value = value.replace(tzinfo=timezone.utc) return value.astimezone(timezone.utc).isoformat() def _deserialize_datetime(value: "Any") -> "datetime | None": value = _decode(value) if not value: return None parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00")) if parsed.tzinfo is None: return parsed.replace(tzinfo=timezone.utc) return parsed.astimezone(timezone.utc) _PRIORITY_STRIDE = 1e13 def _ready_score(record: "QueuedTaskRecord") -> "float": queued = record.queued_at if queued.tzinfo is None: queued = queued.replace(tzinfo=timezone.utc) queued_ms = queued.astimezone(timezone.utc).timestamp() * 1000.0 return (-record.priority) * _PRIORITY_STRIDE + queued_ms def _scheduled_score(value: "datetime | None") -> "float": if value is None: return 0.0 if value.tzinfo is None: value = value.replace(tzinfo=timezone.utc) return value.astimezone(timezone.utc).timestamp() * 1000.0 def _maintenance_score(value: "datetime | None") -> "float": """Return an ordered-set score for a maintenance timestamp.""" return _scheduled_score(value) def _decode(value: "Any") -> "Any": if isinstance(value, bytes): return value.decode() return value def _decode_mapping(mapping: "dict[Any, Any]") -> "dict[str, Any]": return {str(_decode(key)): _decode(value) for key, value in mapping.items()} def _json_default(value: "Any") -> "Any": if isinstance(value, datetime): return _serialize_datetime(value) msg = f"Object of type {type(value).__name__} is not JSON serializable" raise TypeError(msg) def _json_dumps(value: "Any") -> "str": return json.dumps(value, default=_json_default, separators=(",", ":"), sort_keys=True) def _json_loads(value: "Any", default: "Any") -> "Any": value = _decode(value) if value in {None, ""}: return default return json.loads(str(value)) def _reservation_from_payload(raw: "Any") -> "TaskReservation": data = json.loads(str(raw)) return TaskReservation( key=str(data["key"]), task_id=UUID(str(data["task_id"])), task_name=str(data["task_name"]), created_at=_deserialize_datetime(data.get("created_at")) or _utc_now(), ) def _coerce_status(value: "Any") -> "TaskStatus": status = str(_decode(value)) if status not in _STATUS_VALUES: msg = f"Unknown queued task status from Redis-protocol queue backend: {status!r}" raise ValueError(msg) return cast("TaskStatus", status) async def _receive_pubsub_message(pubsub: "PubSubLike") -> "bool": """Block until a real published ``message`` arrives on the subscription. ``get_message(timeout=None)`` blocks indefinitely; subscribe/unsubscribe confirmations are filtered to ``None`` by ``ignore_subscribe_messages``, so they are skipped without ending the retained read. This coroutine carries no deadline of its own — worker poll timeouts race it via :class:`PendingNativeRead`, leaving it pending until a message lands. Returns: True once a real published message is observed. """ while True: message = await pubsub.get_message(ignore_subscribe_messages=True, timeout=None) if message is not None: return True async def _close_pubsub(pubsub: "PubSubLike", channel: "str") -> "None": """Best-effort unsubscribe + close on a pubsub connection.""" unsubscribe = getattr(pubsub, "unsubscribe", None) if unsubscribe is not None: result = unsubscribe(channel) if inspect.isawaitable(result): with suppress(Exception): await result close = getattr(pubsub, "aclose", None) or getattr(pubsub, "close", None) if close is not None: result = close() if inspect.isawaitable(result): with suppress(Exception): await result