import { type RedisOptions } from "@internal/redis"; import type { Span, SpanOptions, Tracer } from "@opentelemetry/api"; import { context, propagation, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api"; import { SEMATTRS_MESSAGE_ID, SEMATTRS_MESSAGING_OPERATION, SEMATTRS_MESSAGING_SYSTEM, } from "@opentelemetry/semantic-conventions"; import { Logger } from "@trigger.dev/core/logger"; import { tryCatch } from "@trigger.dev/core/utils"; import { flattenAttributes } from "@trigger.dev/core/v3"; import { Worker, type WorkerConcurrencyOptions } from "@trigger.dev/redis-worker"; import Redis, { type Callback, type Result } from "ioredis"; import { setInterval as setIntervalAsync } from "node:timers/promises"; import z from "zod"; import { env } from "~/env.server"; import type { AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { logger } from "~/services/logger.server"; import { signalsEmitter } from "~/services/signals.server"; import { singleton } from "~/utils/singleton"; import { legacyRunEngineWorker } from "../legacyRunEngineWorker.server"; import { concurrencyTracker } from "../services/taskRunConcurrencyTracker.server"; import { attributesFromAuthenticatedEnv, tracer } from "../tracer.server"; import { AsyncWorker } from "./asyncWorker.server"; import { MARQS_DELAYED_REQUEUE_THRESHOLD_IN_MS, MARQS_RESUME_PRIORITY_TIMESTAMP_OFFSET, MARQS_RETRY_PRIORITY_TIMESTAMP_OFFSET, MARQS_SCHEDULED_REQUEUE_AVAILABLE_AT_THRESHOLD_IN_MS, } from "./constants.server"; import { FairDequeuingStrategy } from "./fairDequeuingStrategy.server"; import { MarQSShortKeyProducer } from "./marqsKeyProducer"; import type { EnqueueMessageReserveConcurrencyOptions, MarQSFairDequeueStrategy, MarQSKeyProducer, MarQSKeyProducerEnv, MarQSPriorityLevel, MessageQueueSubscriber, VisibilityTimeoutStrategy, } from "./types"; import { MessagePayload } from "./types"; import { V3LegacyRunEngineWorkerVisibilityTimeout } from "./v3VisibilityTimeout.server"; const KEY_PREFIX = "marqs:"; const SemanticAttributes = { CONSUMER_ID: "consumer_id", QUEUE: "queue", PARENT_QUEUE: "parent_queue", MESSAGE_ID: "message_id", CONCURRENCY_KEY: "concurrency_key", }; export type MarQSOptions = { name: string; tracer: Tracer; redis: Redis; defaultEnvConcurrency: number; defaultOrgConcurrency: number; windowSize?: number; visibilityTimeoutInMs?: number; workers: number; keysProducer: MarQSKeyProducer; queuePriorityStrategy: MarQSFairDequeueStrategy; envQueuePriorityStrategy: MarQSFairDequeueStrategy; visibilityTimeoutStrategy: VisibilityTimeoutStrategy; maximumNackCount: number; enableRebalancing?: boolean; verbose?: boolean; subscriber?: MessageQueueSubscriber; sharedWorkerQueueConsumerIntervalMs?: number; sharedWorkerQueueMaxMessageCount?: number; sharedWorkerQueueCooloffPeriodMs?: number; sharedWorkerQueueCooloffCountThreshold?: number; eagerDequeuingEnabled?: boolean; workerOptions: { pollIntervalMs?: number; immediatePollIntervalMs?: number; shutdownTimeoutMs?: number; concurrency?: WorkerConcurrencyOptions; enabled?: boolean; redisOptions: RedisOptions; }; }; const workerCatalog = { processQueueForWorkerQueue: { schema: z.object({ queueKey: z.string(), parentQueueKey: z.string(), }), visibilityTimeoutMs: 30_000, }, }; /** * MarQS - Multitenant Asynchronous Reliable Queueing System (pronounced "markus") */ export class MarQS { private redis: Redis; public keys: MarQSKeyProducer; #rebalanceWorkers: Array = []; private worker: Worker; private queueDequeueCooloffPeriod: Map = new Map(); private queueDequeueCooloffCounts: Map = new Map(); private clearCooloffPeriodInterval: NodeJS.Timeout; isShuttingDown: boolean = false; constructor(private readonly options: MarQSOptions) { this.redis = options.redis; this.keys = options.keysProducer; this.#startRebalanceWorkers(); this.#registerCommands(); // This will prevent these cooloff maps from growing indefinitely this.clearCooloffPeriodInterval = setInterval(() => { this.queueDequeueCooloffCounts.clear(); this.queueDequeueCooloffPeriod.clear(); }, 60_000 * 10); // 10 minutes this.worker = new Worker({ name: "marqs-worker", redisOptions: options.workerOptions.redisOptions, catalog: workerCatalog, concurrency: options.workerOptions?.concurrency, pollIntervalMs: options.workerOptions?.pollIntervalMs ?? 1000, immediatePollIntervalMs: options.workerOptions?.immediatePollIntervalMs ?? 100, shutdownTimeoutMs: options.workerOptions?.shutdownTimeoutMs ?? 10_000, logger: new Logger("MarQSWorker", "info"), jobs: { processQueueForWorkerQueue: async (job) => { await this.#processQueueForWorkerQueue(job.payload.queueKey, job.payload.parentQueueKey); }, }, }); if (options.workerOptions?.enabled) { this.worker.start(); } this.#setupShutdownHandlers(); } #setupShutdownHandlers() { signalsEmitter.on("SIGTERM", () => this.shutdown("SIGTERM")); signalsEmitter.on("SIGINT", () => this.shutdown("SIGINT")); } async shutdown(signal: NodeJS.Signals) { if (this.isShuttingDown) return; this.isShuttingDown = true; console.log("👇 Shutting down marqs", this.name, signal); clearInterval(this.clearCooloffPeriodInterval); this.#rebalanceWorkers.forEach((worker) => worker.stop()); } get name() { return this.options.name; } get tracer() { return this.options.tracer; } public async updateQueueConcurrencyLimits( env: AuthenticatedEnvironment, queue: string, concurrency: number ) { return this.redis.set(this.keys.queueConcurrencyLimitKey(env, queue), concurrency); } public async removeQueueConcurrencyLimits(env: AuthenticatedEnvironment, queue: string) { return this.redis.del(this.keys.queueConcurrencyLimitKey(env, queue)); } public async updateEnvConcurrencyLimits(env: AuthenticatedEnvironment) { const envConcurrencyLimitKey = this.keys.envConcurrencyLimitKey(env); logger.debug("Updating env concurrency limits", { envConcurrencyLimitKey, service: this.name, }); await this.#callUpdateGlobalConcurrencyLimits({ envConcurrencyLimitKey, envConcurrencyLimit: env.maximumConcurrencyLimit, }); } public async getQueueConcurrencyLimit(env: MarQSKeyProducerEnv, queue: string) { const result = await this.redis.get(this.keys.queueConcurrencyLimitKey(env, queue)); return result ? Number(result) : undefined; } public async getEnvConcurrencyLimit(env: MarQSKeyProducerEnv) { const result = await this.redis.get(this.keys.envConcurrencyLimitKey(env)); return result ? Number(result) : this.options.defaultEnvConcurrency; } public async lengthOfQueue( env: AuthenticatedEnvironment, queue: string, concurrencyKey?: string ) { return this.redis.zcard(this.keys.queueKey(env, queue, concurrencyKey)); } public async lengthOfEnvQueue(env: MarQSKeyProducerEnv) { return this.redis.zcard(this.keys.envQueueKey(env)); } public async oldestMessageInQueue( env: AuthenticatedEnvironment, queue: string, concurrencyKey?: string ) { // Get the "score" of the sorted set to get the oldest message score const result = await this.redis.zrange( this.keys.queueKey(env, queue, concurrencyKey), 0, 0, "WITHSCORES" ); if (result.length === 0) { return; } return Number(result[1]); } public async currentConcurrencyOfQueue( env: MarQSKeyProducerEnv, queue: string, concurrencyKey?: string ) { return this.redis.scard(this.keys.queueCurrentConcurrencyKey(env, queue, concurrencyKey)); } public async reserveConcurrencyOfQueue( env: MarQSKeyProducerEnv, queue: string, concurrencyKey?: string ) { return this.redis.scard( this.keys.queueReserveConcurrencyKeyFromQueue(this.keys.queueKey(env, queue, concurrencyKey)) ); } public async currentConcurrencyOfEnvironment(env: MarQSKeyProducerEnv) { return this.redis.scard(this.keys.envCurrentConcurrencyKey(env)); } public async reserveConcurrencyOfEnvironment(env: MarQSKeyProducerEnv) { return this.redis.scard(this.keys.envReserveConcurrencyKey(env.id)); } public async removeEnvironmentQueuesFromMasterQueue(orgId: string, environmentId: string) { const sharedQueue = this.keys.sharedQueueKey(); const queuePattern = this.keys.queueKey(orgId, environmentId, "*"); // Use scanStream to find all matching members const stream = this.redis.zscanStream(sharedQueue, { match: queuePattern, count: 100, }); return new Promise((resolve, reject) => { const matchingQueues: string[] = []; stream.on("data", (resultKeys) => { // zscanStream returns [member1, score1, member2, score2, ...] // We only want the members (even indices) for (let i = 0; i < resultKeys.length; i += 2) { matchingQueues.push(resultKeys[i]); } }); stream.on("end", async () => { if (matchingQueues.length > 0) { await this.redis.zrem(sharedQueue, matchingQueues); } resolve(); }); stream.on("error", (err) => reject(err)); }); } public async enqueueMessage( env: AuthenticatedEnvironment, queue: string, messageId: string, messageData: Record, concurrencyKey?: string, timestamp?: number | Date, reserve?: EnqueueMessageReserveConcurrencyOptions, priority?: MarQSPriorityLevel ) { return await this.#trace( "enqueueMessage", async (span) => { const messageQueue = this.keys.queueKey(env, queue, concurrencyKey); const parentQueue = this.keys.envSharedQueueKey(env); propagation.inject(context.active(), messageData); const $timestamp = typeof timestamp === "undefined" ? Date.now() : typeof timestamp === "number" ? timestamp : timestamp.getTime(); const messagePayload: MessagePayload = { version: "1", data: messageData, queue: messageQueue, concurrencyKey, timestamp: $timestamp, messageId, parentQueue, priority, availableAt: Date.now(), enqueueMethod: "enqueue", }; span.setAttributes({ [SemanticAttributes.QUEUE]: queue, [SemanticAttributes.MESSAGE_ID]: messageId, [SemanticAttributes.CONCURRENCY_KEY]: concurrencyKey, [SemanticAttributes.PARENT_QUEUE]: parentQueue, }); if (reserve) { span.setAttribute("reserve_message_id", reserve.messageId); span.setAttribute("reserve_recursive_queue", reserve.recursiveQueue); } if (env.type !== "DEVELOPMENT" && this.options.eagerDequeuingEnabled) { // This will move the message to the worker queue so it can be dequeued await this.worker.enqueueOnce({ id: messageQueue, // dedupe by environment, queue, and concurrency key job: "processQueueForWorkerQueue", payload: { queueKey: messageQueue, parentQueueKey: parentQueue, }, // Add a small delay to dedupe messages so at most one of these will processed, // every 500ms per queue, concurrency key, and environment availableAt: new Date(Date.now() + 500), // 500ms from now }); } const result = await this.#callEnqueueMessage(messagePayload, reserve); if (result) { await this.options.subscriber?.messageEnqueued(messagePayload); } return result; }, { kind: SpanKind.PRODUCER, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "publish", [SEMATTRS_MESSAGE_ID]: messageId, [SEMATTRS_MESSAGING_SYSTEM]: "marqs", ...attributesFromAuthenticatedEnv(env), }, } ); } public async replaceMessage( messageId: string, messageData: Record, timestamp?: number ) { return this.#trace( "replaceMessage", async (span) => { const oldMessage = await this.readMessage(messageId); if (!oldMessage) { return; } span.setAttributes({ [SemanticAttributes.QUEUE]: oldMessage.queue, [SemanticAttributes.MESSAGE_ID]: oldMessage.messageId, [SemanticAttributes.CONCURRENCY_KEY]: oldMessage.concurrencyKey, [SemanticAttributes.PARENT_QUEUE]: oldMessage.parentQueue, }); const traceContext = { traceparent: oldMessage.data.traceparent, tracestate: oldMessage.data.tracestate, }; const newMessage: MessagePayload = { version: "1", // preserve original trace context data: { ...oldMessage.data, ...messageData, ...traceContext, queue: oldMessage.queue }, queue: oldMessage.queue, concurrencyKey: oldMessage.concurrencyKey, timestamp: timestamp ?? Date.now(), messageId, parentQueue: oldMessage.parentQueue, priority: oldMessage.priority, enqueueMethod: "replace", }; await this.#saveMessageIfExists(newMessage); }, { kind: SpanKind.CONSUMER, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "replace", [SEMATTRS_MESSAGE_ID]: messageId, [SEMATTRS_MESSAGING_SYSTEM]: "marqs", }, } ); } public async requeueMessage( messageId: string, messageData: Record, timestamp?: number, priority?: MarQSPriorityLevel ) { return this.#trace( "requeueMessage", async (span) => { const oldMessage = await this.readMessage(messageId); if (!oldMessage) { return; } span.setAttributes({ [SemanticAttributes.QUEUE]: oldMessage.queue, [SemanticAttributes.MESSAGE_ID]: oldMessage.messageId, [SemanticAttributes.CONCURRENCY_KEY]: oldMessage.concurrencyKey, [SemanticAttributes.PARENT_QUEUE]: oldMessage.parentQueue, }); const traceContext = { traceparent: oldMessage.data.traceparent, tracestate: oldMessage.data.tracestate, }; const $timestamp = timestamp ?? Date.now(); const newMessage: MessagePayload = { version: "1", // preserve original trace context data: { ...oldMessage.data, ...messageData, ...traceContext, queue: oldMessage.queue, }, queue: oldMessage.queue, concurrencyKey: oldMessage.concurrencyKey, timestamp: $timestamp, messageId, parentQueue: oldMessage.parentQueue, priority: priority ?? oldMessage.priority, availableAt: $timestamp, enqueueMethod: "requeue", }; await this.options.visibilityTimeoutStrategy.cancelHeartbeat(messageId); // If the message timestamp is enough in the future (e.g. more than 500ms from now), // we will schedule it to be requeued in the future using the legacy run engine redis worker // If not, we just requeue it immediately if ($timestamp > Date.now() + MARQS_DELAYED_REQUEUE_THRESHOLD_IN_MS) { await this.#callDelayedRequeueMessage(newMessage); } else { await this.#callRequeueMessage(newMessage); } }, { kind: SpanKind.CONSUMER, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "requeue", [SEMATTRS_MESSAGE_ID]: messageId, [SEMATTRS_MESSAGING_SYSTEM]: "marqs", }, } ); } public async requeueMessageById(messageId: string) { return this.#trace( "requeueMessageById", async (span) => { const message = await this.readMessage(messageId); if (!message) { return; } span.setAttributes({ [SemanticAttributes.QUEUE]: message.queue, [SemanticAttributes.MESSAGE_ID]: message.messageId, [SemanticAttributes.CONCURRENCY_KEY]: message.concurrencyKey, [SemanticAttributes.PARENT_QUEUE]: message.parentQueue, }); logger.debug(`Requeueing message by id`, { messageId, message, service: this.name }); await this.#callRequeueMessage(message); }, { kind: SpanKind.CONSUMER, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "requeue_by_id", [SEMATTRS_MESSAGE_ID]: messageId, [SEMATTRS_MESSAGING_SYSTEM]: "marqs", }, } ); } async #saveMessageIfExists(message: MessagePayload) { logger.debug(`Saving message if exists`, { message, service: this.name }); const messageKey = this.keys.messageKey(message.messageId); await this.redis.set(messageKey, JSON.stringify(message), "XX"); // XX means only set if key exists } public async dequeueMessageInEnv(env: AuthenticatedEnvironment) { return this.#trace( "dequeueMessageInEnv", async (span) => { const parentQueue = this.keys.envSharedQueueKey(env); span.setAttribute(SemanticAttributes.PARENT_QUEUE, parentQueue); span.setAttribute(SemanticAttributes.CONSUMER_ID, env.id); // Get prioritized list of queues to try const environments = await this.options.envQueuePriorityStrategy.distributeFairQueuesFromParentQueue( parentQueue, env.id ); const queues = environments.flatMap((e) => e.queues); span.setAttribute("env_count", environments.length); span.setAttribute("queue_count", queues.length); for (const messageQueue of queues) { const messages = await this.#callDequeueMessages({ messageQueue, parentQueue, maxCount: 1, }); if (!messages || messages.length === 0) { return; } const messageData = messages[0]; const message = await this.readMessage(messageData.messageId); if (message) { span.setAttributes({ [SEMATTRS_MESSAGE_ID]: message.messageId, [SemanticAttributes.QUEUE]: message.queue, [SemanticAttributes.MESSAGE_ID]: message.messageId, [SemanticAttributes.CONCURRENCY_KEY]: message.concurrencyKey, [SemanticAttributes.PARENT_QUEUE]: message.parentQueue, attempted_queues: queues.indexOf(messageQueue) + 1, // How many queues we tried before success message_timestamp: message.timestamp, message_age: this.#calculateMessageAge(message), message_priority: message.priority, message_enqueue_method: message.enqueueMethod, message_available_at: message.availableAt, ...flattenAttributes(message.data, "message.data"), }); await this.options.subscriber?.messageDequeued(message); } else { logger.error(`Failed to read message, undoing the dequeueing of the message`, { messageData, service: this.name, }); await this.#callAcknowledgeMessage({ parentQueue, messageQueue: messageQueue, messageId: messageData.messageId, }); return; } await this.options.visibilityTimeoutStrategy.startHeartbeat( messageData.messageId, this.visibilityTimeoutInMs ); return message; } span.setAttribute("attempted_queues", queues.length); return; }, { kind: SpanKind.CONSUMER, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "receive", [SEMATTRS_MESSAGING_SYSTEM]: "marqs", ...attributesFromAuthenticatedEnv(env), }, } ); } /** * Dequeue a message from the shared worker queue (this should be used in production environments) */ public async dequeueMessageFromSharedWorkerQueue(consumerId: string) { return this.#trace( "dequeueMessageFromSharedWorkerQueue", async (span) => { span.setAttribute(SemanticAttributes.CONSUMER_ID, consumerId); const workerQueueKey = this.keys.sharedWorkerQueueKey(); span.setAttribute(SemanticAttributes.PARENT_QUEUE, workerQueueKey); // Try and pop a message from the worker queue (redis list) const messageId = await this.#trace("popMessageFromWorkerQueue", async (innerSpan) => { innerSpan.setAttribute(SemanticAttributes.PARENT_QUEUE, workerQueueKey); innerSpan.setAttribute(SemanticAttributes.CONSUMER_ID, consumerId); const results = await this.redis.popMessageFromWorkerQueue(workerQueueKey); if (!results) { return null; } const [messageId, queueLength] = results; innerSpan.setAttribute("queue_length", Number(queueLength)); return messageId; }); if (!messageId) { return; } const message = await this.readMessage(messageId); if (!message) { return; } if (this.options.subscriber) { await this.#trace( "postMessageDequeued", async (subscriberSpan) => { subscriberSpan.setAttributes({ [SemanticAttributes.MESSAGE_ID]: message.messageId, [SemanticAttributes.QUEUE]: message.queue, [SemanticAttributes.PARENT_QUEUE]: message.parentQueue, }); return await this.options.subscriber?.messageDequeued(message); }, { kind: SpanKind.INTERNAL, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "receive", [SEMATTRS_MESSAGING_SYSTEM]: "marqs", }, } ); } await this.#trace( "startHeartbeat", async (heartbeatSpan) => { heartbeatSpan.setAttributes({ [SemanticAttributes.MESSAGE_ID]: message.messageId, visibility_timeout_ms: this.visibilityTimeoutInMs, }); return await this.options.visibilityTimeoutStrategy.startHeartbeat( message.messageId, this.visibilityTimeoutInMs ); }, { kind: SpanKind.INTERNAL, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "receive", [SEMATTRS_MESSAGING_SYSTEM]: "marqs", }, } ); return message; }, { kind: SpanKind.CONSUMER, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "receive", [SEMATTRS_MESSAGING_SYSTEM]: "marqs", }, } ); } public startSharedWorkerQueueConsumer(consumerId: string) { const abortController = new AbortController(); this.#startSharedWorkerQueueConsumer(consumerId, abortController).catch((error) => { logger.error("Failed to start shared worker queue consumer", { error, service: this.name, consumerId, }); }); return () => { abortController.abort(); }; } async #startSharedWorkerQueueConsumer(consumerId: string, abortController: AbortController) { let lastProcessedAt = Date.now(); let processedCount = 0; try { for await (const _ of setIntervalAsync( this.options.sharedWorkerQueueConsumerIntervalMs ?? 500, null, { signal: abortController.signal, } )) { logger.debug(`Processing shared worker queue`, { processedCount, lastProcessedAt, service: this.name, consumerId, }); const now = performance.now(); const [error, results] = await tryCatch(this.#processSharedWorkerQueue(consumerId)); if (error) { logger.error(`Failed to process shared worker queue`, { error, service: this.name, consumerId, }); continue; } const duration = performance.now() - now; logger.debug(`Processed shared worker queue`, { processedCount, lastProcessedAt, service: this.name, duration, results, consumerId, }); processedCount++; lastProcessedAt = Date.now(); } } catch (error) { if (error instanceof Error && error.name !== "AbortError") { throw error; } logger.debug(`Shared worker queue consumer stopped`, { service: this.name, processedCount, lastProcessedAt, }); } } /** * Dequeue as many messages as possible from queues into the shared worker queue list */ async #processSharedWorkerQueue(consumerId: string) { return this.#trace( "processSharedWorkerQueue", async (span) => { span.setAttribute(SemanticAttributes.CONSUMER_ID, consumerId); const parentQueue = this.keys.sharedQueueKey(); span.setAttribute(SemanticAttributes.PARENT_QUEUE, parentQueue); // Get prioritized list of queues to try const envQueues = await this.options.queuePriorityStrategy.distributeFairQueuesFromParentQueue( parentQueue, consumerId ); span.setAttribute("environment_count", envQueues.length); if (envQueues.length === 0) { return; } let attemptedEnvs = 0; let attemptedQueues = 0; let messageCount = 0; let coolOffPeriodCount = 0; // Try each queue in order, attempt to dequeue a message from each queue, keep going until we've tried all the queues for (const env of envQueues) { attemptedEnvs++; for (const messageQueue of env.queues) { attemptedQueues++; const cooloffPeriod = this.queueDequeueCooloffPeriod.get(messageQueue); // If the queue is in a cooloff period, skip attempting to dequeue from it if (cooloffPeriod) { // If the cooloff period is still active, skip attempting to dequeue from it if (cooloffPeriod > Date.now()) { coolOffPeriodCount++; continue; } else { // If the cooloff period is over, delete the cooloff period and attempt to dequeue from the queue this.queueDequeueCooloffPeriod.delete(messageQueue); } } await this.#trace( "attemptDequeue", async (attemptDequeueSpan) => { try { attemptDequeueSpan.setAttributes({ [SemanticAttributes.QUEUE]: messageQueue, [SemanticAttributes.PARENT_QUEUE]: parentQueue, }); const messages = await this.#trace( "callDequeueMessages", async (dequeueSpan) => { dequeueSpan.setAttributes({ [SemanticAttributes.QUEUE]: messageQueue, [SemanticAttributes.PARENT_QUEUE]: parentQueue, }); return await this.#callDequeueMessages({ messageQueue, parentQueue, maxCount: this.options.sharedWorkerQueueMaxMessageCount ?? 10, }); }, { kind: SpanKind.CONSUMER, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "receive", [SEMATTRS_MESSAGING_SYSTEM]: "marqs", }, } ); if (!messages || messages.length === 0) { const cooloffCount = this.queueDequeueCooloffCounts.get(messageQueue) ?? 0; const cooloffCountThreshold = Math.max( 10, this.options.sharedWorkerQueueCooloffCountThreshold ?? 10 ); // minimum of 10 if (cooloffCount >= cooloffCountThreshold) { // If no messages were dequeued, set a cooloff period for the queue // This is to prevent the queue from being dequeued too frequently // and to give other queues a chance to dequeue messages more frequently this.queueDequeueCooloffPeriod.set( messageQueue, Date.now() + (this.options.sharedWorkerQueueCooloffPeriodMs ?? 10_000) // defaults to 10 seconds ); this.queueDequeueCooloffCounts.delete(messageQueue); } else { this.queueDequeueCooloffCounts.set(messageQueue, cooloffCount + 1); } attemptDequeueSpan.setAttribute("message_count", 0); return null; // Try next queue if no message was dequeued } this.queueDequeueCooloffCounts.delete(messageQueue); messageCount += messages.length; attemptDequeueSpan.setAttribute("message_count", messages.length); await this.#trace( "addToWorkerQueue", async (addToWorkerQueueSpan) => { const workerQueueKey = this.keys.sharedWorkerQueueKey(); addToWorkerQueueSpan.setAttributes({ message_count: messages.length, [SemanticAttributes.PARENT_QUEUE]: workerQueueKey, }); await this.redis.rpush( workerQueueKey, ...messages.map((message) => message.messageId) ); }, { kind: SpanKind.INTERNAL, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "receive", [SEMATTRS_MESSAGING_SYSTEM]: "marqs", }, } ); } catch (error) { // Log error but continue trying other queues logger.warn(`[${this.name}] Failed to dequeue from queue ${messageQueue}`, { error, }); return null; } }, { kind: SpanKind.CONSUMER, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "receive", [SEMATTRS_MESSAGING_SYSTEM]: "marqs", }, } ); } } // If we get here, we tried all queues but couldn't dequeue a message span.setAttribute("attempted_queues", attemptedQueues); span.setAttribute("attempted_envs", attemptedEnvs); span.setAttribute("message_count", messageCount); span.setAttribute("cooloff_period_count", coolOffPeriodCount); return; }, { kind: SpanKind.CONSUMER, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "receive", [SEMATTRS_MESSAGING_SYSTEM]: "marqs", }, } ); } async #processQueueForWorkerQueue(queueKey: string, parentQueueKey: string) { return this.#trace("processQueueForWorkerQueue", async (span) => { span.setAttributes({ [SemanticAttributes.QUEUE]: queueKey, [SemanticAttributes.PARENT_QUEUE]: parentQueueKey, }); const maxCount = this.options.sharedWorkerQueueMaxMessageCount ?? 10; const dequeuedMessages = await this.#callDequeueMessages({ messageQueue: queueKey, parentQueue: parentQueueKey, maxCount, }); if (!dequeuedMessages || dequeuedMessages.length === 0) { return; } await this.#trace( "addToWorkerQueue", async (addToWorkerQueueSpan) => { const workerQueueKey = this.keys.sharedWorkerQueueKey(); addToWorkerQueueSpan.setAttributes({ message_count: dequeuedMessages.length, [SemanticAttributes.PARENT_QUEUE]: workerQueueKey, }); await this.redis.rpush( workerQueueKey, ...dequeuedMessages.map((message) => message.messageId) ); }, { kind: SpanKind.INTERNAL, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "receive", [SEMATTRS_MESSAGING_SYSTEM]: "marqs", }, } ); // If we dequeued the max count, we need to enqueue another job to dequeue the next batch if (dequeuedMessages.length === maxCount) { await this.worker.enqueueOnce({ id: queueKey, job: "processQueueForWorkerQueue", payload: { queueKey, parentQueueKey, }, availableAt: new Date(Date.now() + 500), // 500ms from now }); } }); } public async acknowledgeMessage(messageId: string, reason: string = "unknown") { return this.#trace( "acknowledgeMessage", async (span) => { const message = await this.readMessage(messageId); if (!message) { logger.log(`[${this.name}].acknowledgeMessage() message not found`, { messageId, service: this.name, reason, }); return; } span.setAttributes({ [SemanticAttributes.QUEUE]: message.queue, [SemanticAttributes.MESSAGE_ID]: message.messageId, [SemanticAttributes.CONCURRENCY_KEY]: message.concurrencyKey, [SemanticAttributes.PARENT_QUEUE]: message.parentQueue, ["marqs.reason"]: reason, }); await this.options.visibilityTimeoutStrategy.cancelHeartbeat(messageId); await this.#callAcknowledgeMessage({ parentQueue: message.parentQueue, messageQueue: message.queue, messageId, }); const sharedQueueKey = this.keys.sharedQueueKey(); if (this.options.eagerDequeuingEnabled && message.parentQueue === sharedQueueKey) { await this.worker.enqueueOnce({ id: message.queue, job: "processQueueForWorkerQueue", payload: { queueKey: message.queue, parentQueueKey: message.parentQueue, }, availableAt: new Date(Date.now() + 500), // 500ms from now }); } await this.options.subscriber?.messageAcked(message); }, { kind: SpanKind.CONSUMER, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "ack", [SEMATTRS_MESSAGE_ID]: messageId, [SEMATTRS_MESSAGING_SYSTEM]: "marqs", }, } ); } /** * Negative acknowledge a message, which will requeue the message. * Returns whether it went back into the queue or not. */ public async nackMessage( messageId: string, retryAt: number = Date.now(), updates?: Record ) { return this.#trace( "nackMessage", async (span) => { const message = await this.readMessage(messageId); if (!message) { logger.debug(`[${this.name}].nackMessage() message not found`, { messageId, retryAt, updates, service: this.name, }); return false; } const nackCount = await this.#getNackCount(messageId); span.setAttribute("nack_count", nackCount); if (nackCount >= this.options.maximumNackCount) { logger.debug(`[${this.name}].nackMessage() maximum nack count reached`, { messageId, retryAt, updates, service: this.name, }); span.setAttribute("maximum_nack_count_reached", true); // If we have reached the maximum nack count, we will ack the message await this.acknowledgeMessage(messageId, "maximum nack count reached"); return false; } span.setAttributes({ [SemanticAttributes.QUEUE]: message.queue, [SemanticAttributes.MESSAGE_ID]: message.messageId, [SemanticAttributes.CONCURRENCY_KEY]: message.concurrencyKey, [SemanticAttributes.PARENT_QUEUE]: message.parentQueue, }); if (updates) { await this.replaceMessage(messageId, updates, retryAt); } await this.options.visibilityTimeoutStrategy.cancelHeartbeat(messageId); await this.#callNackMessage(messageId, message, retryAt); await this.options.subscriber?.messageNacked(message); return true; }, { kind: SpanKind.CONSUMER, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "nack", [SEMATTRS_MESSAGE_ID]: messageId, [SEMATTRS_MESSAGING_SYSTEM]: "marqs", }, } ); } public async cancelHeartbeat(messageId: string) { return this.#trace( "cancelHeartbeat", async (span) => { span.setAttributes({ [SemanticAttributes.MESSAGE_ID]: messageId, }); await this.options.visibilityTimeoutStrategy.cancelHeartbeat(messageId); }, { kind: SpanKind.CONSUMER, attributes: { [SEMATTRS_MESSAGING_OPERATION]: "cancelHeartbeat", [SEMATTRS_MESSAGE_ID]: messageId, [SEMATTRS_MESSAGING_SYSTEM]: "marqs", }, } ); } async #trace( name: string, fn: (span: Span) => Promise, options?: SpanOptions & { sampleRate?: number } ): Promise { return this.tracer.startActiveSpan( name, { ...options, attributes: { ...options?.attributes, }, }, async (span) => { try { return await fn(span); } catch (e) { if (e instanceof Error) { span.recordException(e); } else { span.recordException(new Error(String(e))); } span.setStatus({ code: SpanStatusCode.ERROR, message: e instanceof Error ? e.message : "Unknown error", }); throw e; } finally { span.end(); } } ); } #nudgeTimestampForPriority(timestamp: number, priority?: MarQSPriorityLevel) { if (!priority) { return timestamp; } switch (priority) { case "resume": { return timestamp - MARQS_RESUME_PRIORITY_TIMESTAMP_OFFSET; } case "retry": { return timestamp - MARQS_RETRY_PRIORITY_TIMESTAMP_OFFSET; } } } #calculateMessageAge(message: MessagePayload) { const $timestamp = message.availableAt ?? message.timestamp; return Date.now() - $timestamp; } async #getNackCount(messageId: string): Promise { const result = await this.redis.get(this.keys.nackCounterKey(messageId)); return result ? Number(result) : 0; } // This should increment by the number of seconds, but with a max value of Date.now() + visibilityTimeoutInMs public async heartbeatMessage(messageId: string) { await this.options.visibilityTimeoutStrategy.heartbeat(messageId, this.visibilityTimeoutInMs); } get visibilityTimeoutInMs() { return this.options.visibilityTimeoutInMs ?? 300000; // 5 minutes } async readMessage(messageId: string) { return this.#trace( "readMessage", async (span) => { const rawMessage = await this.redis.get(this.keys.messageKey(messageId)); if (!rawMessage) { return; } const message = MessagePayload.safeParse(JSON.parse(rawMessage)); if (!message.success) { logger.error(`[${this.name}] Failed to parse message`, { messageId, error: message.error, service: this.name, }); return; } return message.data; }, { attributes: { [SEMATTRS_MESSAGING_OPERATION]: "receive", [SEMATTRS_MESSAGE_ID]: messageId, [SEMATTRS_MESSAGING_SYSTEM]: "marqs", [SemanticAttributes.MESSAGE_ID]: messageId, }, } ); } #startRebalanceWorkers() { if (!this.options.enableRebalancing) { return; } // Start a new worker to rebalance parent queues periodically for (let i = 0; i < this.options.workers; i++) { const worker = new AsyncWorker(this.#rebalanceParentQueues.bind(this), 60_000); this.#rebalanceWorkers.push(worker); worker.start(); } } queueConcurrencyScanStream( count: number = 100, onEndCallback?: () => void, onErrorCallback?: (error: Error) => void ) { const pattern = this.keys.queueCurrentConcurrencyScanPattern(); logger.debug("Starting queue concurrency scan stream", { pattern, component: "marqs", operation: "queueConcurrencyScanStream", service: this.name, count, }); const redis = this.redis.duplicate(); const stream = redis.scanStream({ match: pattern, type: "set", count, }); stream.on("end", () => { onEndCallback?.(); redis.quit(); }); stream.on("error", (error) => { onErrorCallback?.(error); redis.quit(); }); return { stream, redis }; } async #rebalanceParentQueues() { return await new Promise((resolve, reject) => { // Scan for sorted sets with the parent queue pattern const pattern = this.keys.sharedQueueScanPattern(); const redis = this.redis.duplicate(); const stream = redis.scanStream({ match: pattern, type: "zset", count: 100, }); logger.debug("Streaming parent queues based on pattern", { pattern, component: "marqs", operation: "rebalanceParentQueues", service: this.name, }); stream.on("data", async (keys) => { const uniqueKeys = Array.from(new Set(keys)); if (uniqueKeys.length === 0) { return; } stream.pause(); logger.debug("Rebalancing parent queues", { component: "marqs", operation: "rebalanceParentQueues", parentQueues: uniqueKeys, service: this.name, }); Promise.all( uniqueKeys.map(async (key) => this.#rebalanceParentQueue(this.keys.stripKeyPrefix(key))) ).finally(() => { stream.resume(); }); }); stream.on("end", () => { redis.quit().finally(() => { resolve(); }); }); stream.on("error", (e) => { redis.quit().finally(() => { reject(e); }); }); }); } // Parent queue is a sorted set, the values of which are queue keys and the scores are is the oldest message in the queue // We need to scan the parent queue and rebalance the queues based on the oldest message in the queue async #rebalanceParentQueue(parentQueue: string) { return await new Promise((resolve, reject) => { const redis = this.redis.duplicate(); const stream = redis.zscanStream(parentQueue, { match: "*", count: 100, }); stream.on("data", async (childQueues) => { stream.pause(); // childQueues is a flat array but of the form [queue1, score1, queue2, score2, ...], we want to group them into pairs const childQueuesWithScores: Record = {}; for (let i = 0; i < childQueues.length; i += 2) { childQueuesWithScores[childQueues[i]] = childQueues[i + 1]; } logger.debug("Rebalancing child queues", { parentQueue, childQueuesWithScores, component: "marqs", operation: "rebalanceParentQueues", service: this.name, }); await Promise.all( Object.entries(childQueuesWithScores).map(async ([childQueue, currentScore]) => this.#callRebalanceParentQueueChild({ parentQueue, childQueue, currentScore }) ) ).finally(() => { stream.resume(); }); }); stream.on("end", () => { redis.quit().finally(() => { resolve(); }); }); stream.on("error", (e) => { redis.quit().finally(() => { reject(e); }); }); }); } async #callEnqueueMessage( message: MessagePayload, reserve?: EnqueueMessageReserveConcurrencyOptions ) { const queueKey = message.queue; const parentQueueKey = message.parentQueue; const messageKey = this.keys.messageKey(message.messageId); const queueCurrentConcurrencyKey = this.keys.queueCurrentConcurrencyKeyFromQueue(message.queue); const queueReserveConcurrencyKey = this.keys.queueReserveConcurrencyKeyFromQueue(message.queue); const envCurrentConcurrencyKey = this.keys.envCurrentConcurrencyKeyFromQueue(message.queue); const envReserveConcurrencyKey = this.keys.envReserveConcurrencyKeyFromQueue(message.queue); const envQueueKey = this.keys.envQueueKeyFromQueue(message.queue); const queueName = message.queue; const messageId = message.messageId; const messageData = JSON.stringify(message); const messageScore = String( this.#nudgeTimestampForPriority(message.timestamp, message.priority) ); if (!reserve) { logger.debug("Calling enqueueMessage", { service: this.name, queueKey, parentQueueKey, messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, envQueueKey, queueName, messageId, messageData, messageScore, }); const result = await this.redis.enqueueMessage( queueKey, parentQueueKey, messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, envQueueKey, queueName, messageId, messageData, messageScore ); logger.debug("enqueueMessage result", { service: this.name, queueKey, parentQueueKey, messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, envQueueKey, queueName, messageId, messageData, messageScore, result, }); return true; } const envConcurrencyLimitKey = this.keys.envConcurrencyLimitKeyFromQueue(message.queue); const reserveMessageId = reserve.messageId; const defaultEnvConcurrencyLimit = String(this.options.defaultEnvConcurrency); if (!reserve.recursiveQueue) { logger.debug("Calling enqueueMessageWithReservingConcurrency", { service: this.name, queueKey, parentQueueKey, messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, envConcurrencyLimitKey, envQueueKey, queueName, messageId, messageData, messageScore, reserveMessageId, defaultEnvConcurrencyLimit, }); const result = await this.redis.enqueueMessageWithReservingConcurrency( queueKey, parentQueueKey, messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, envConcurrencyLimitKey, envQueueKey, queueName, messageId, messageData, messageScore, reserveMessageId, defaultEnvConcurrencyLimit ); logger.debug("enqueueMessageWithReservingConcurrency result", { service: this.name, queueKey, parentQueueKey, messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, envConcurrencyLimitKey, envQueueKey, queueName, messageId, messageData, messageScore, reserveMessageId, defaultEnvConcurrencyLimit, result, }); return true; } else { const queueConcurrencyLimitKey = this.keys.queueConcurrencyLimitKeyFromQueue(message.queue); logger.debug("Calling enqueueMessageWithReservingConcurrencyForRecursiveQueue", { service: this.name, queueKey, parentQueueKey, messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, queueConcurrencyLimitKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, envConcurrencyLimitKey, envQueueKey, queueName, messageId, messageData, messageScore, reserveMessageId, defaultEnvConcurrencyLimit, }); const result = await this.redis.enqueueMessageWithReservingConcurrencyOnRecursiveQueue( queueKey, parentQueueKey, messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, queueConcurrencyLimitKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, envConcurrencyLimitKey, envQueueKey, queueName, messageId, messageData, messageScore, reserveMessageId, defaultEnvConcurrencyLimit ); logger.debug("enqueueMessageWithReservingConcurrencyOnRecursiveQueue result", { service: this.name, queueKey, parentQueueKey, messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, queueConcurrencyLimitKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, envConcurrencyLimitKey, envQueueKey, queueName, messageId, messageData, messageScore, reserveMessageId, defaultEnvConcurrencyLimit, result, }); return !!result; } } async #callDequeueMessages({ messageQueue, parentQueue, maxCount, }: { messageQueue: string; parentQueue: string; maxCount: number; }) { const queueConcurrencyLimitKey = this.keys.queueConcurrencyLimitKeyFromQueue(messageQueue); const queueCurrentConcurrencyKey = this.keys.queueCurrentConcurrencyKeyFromQueue(messageQueue); const envConcurrencyLimitKey = this.keys.envConcurrencyLimitKeyFromQueue(messageQueue); const envCurrentConcurrencyKey = this.keys.envCurrentConcurrencyKeyFromQueue(messageQueue); const envReserveConcurrencyKey = this.keys.envReserveConcurrencyKeyFromQueue(messageQueue); const queueReserveConcurrencyKey = this.keys.queueReserveConcurrencyKeyFromQueue(messageQueue); const envQueueKey = this.keys.envQueueKeyFromQueue(messageQueue); logger.debug("Calling dequeueMessages", { messageQueue, parentQueue, queueConcurrencyLimitKey, envConcurrencyLimitKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, envQueueKey, service: this.name, }); const result = await this.redis.dequeueMessages( messageQueue, parentQueue, queueConcurrencyLimitKey, envConcurrencyLimitKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, envQueueKey, messageQueue, String(Date.now()), String(this.options.defaultEnvConcurrency), String(maxCount) ); if (!result) { return; } logger.debug("Dequeue message result", { result, service: this.name, }); const messages = []; for (let i = 0; i < result.length; i += 2) { const messageId = result[i]; const messageScore = result[i + 1]; messages.push({ messageId, messageScore, }); } logger.debug("dequeueMessages parsed result", { messages, service: this.name, }); return messages.filter(Boolean); } async #callRequeueMessage(message: MessagePayload) { const queueKey = message.queue; const parentQueueKey = message.parentQueue; const messageKey = this.keys.messageKey(message.messageId); const queueCurrentConcurrencyKey = this.keys.queueCurrentConcurrencyKeyFromQueue(message.queue); const queueReserveConcurrencyKey = this.keys.queueReserveConcurrencyKeyFromQueue(message.queue); const envCurrentConcurrencyKey = this.keys.envCurrentConcurrencyKeyFromQueue(message.queue); const envReserveConcurrencyKey = this.keys.envReserveConcurrencyKeyFromQueue(message.queue); const envQueueKey = this.keys.envQueueKeyFromQueue(message.queue); const queueName = message.queue; const messageId = message.messageId; const messageData = JSON.stringify(message); const messageScore = String( this.#nudgeTimestampForPriority(message.timestamp, message.priority) ); logger.debug("Calling requeueMessage", { service: this.name, queueKey, parentQueueKey, messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, envQueueKey, queueName, messageId, messageData, messageScore, }); const result = await this.redis.requeueMessage( queueKey, parentQueueKey, messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, envQueueKey, queueName, messageId, messageData, messageScore ); logger.debug("requeueMessage result", { service: this.name, queueKey, parentQueueKey, messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, envQueueKey, queueName, messageId, messageData, messageScore, result, }); await this.options.subscriber?.messageRequeued(message); return true; } async #callDelayedRequeueMessage(message: MessagePayload) { const messageKey = this.keys.messageKey(message.messageId); const queueCurrentConcurrencyKey = this.keys.queueCurrentConcurrencyKeyFromQueue(message.queue); const queueReserveConcurrencyKey = this.keys.queueReserveConcurrencyKeyFromQueue(message.queue); const envCurrentConcurrencyKey = this.keys.envCurrentConcurrencyKeyFromQueue(message.queue); const envReserveConcurrencyKey = this.keys.envReserveConcurrencyKeyFromQueue(message.queue); const messageId = message.messageId; const messageData = JSON.stringify(message); logger.debug("Calling delayedRequeueMessage", { service: this.name, messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, messageId, messageData, }); const result = await this.redis.delayedRequeueMessage( messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, messageId, messageData ); logger.debug("delayedRequeueMessage result", { service: this.name, messageKey, queueCurrentConcurrencyKey, queueReserveConcurrencyKey, envCurrentConcurrencyKey, envReserveConcurrencyKey, messageId, messageData, result, }); logger.debug("Enqueuing scheduleRequeueMessage in LRE worker", { service: this.name, message, }); // Schedule the requeue in the future await legacyRunEngineWorker.enqueue({ id: `marqs-requeue-${messageId}`, job: "scheduleRequeueMessage", payload: { messageId }, availableAt: new Date( message.timestamp - MARQS_SCHEDULED_REQUEUE_AVAILABLE_AT_THRESHOLD_IN_MS ), }); return true; } async #callAcknowledgeMessage({ parentQueue, messageQueue, messageId, }: { parentQueue: string; messageQueue: string; messageId: string; }) { const messageKey = this.keys.messageKey(messageId); const concurrencyKey = this.keys.queueCurrentConcurrencyKeyFromQueue(messageQueue); const envConcurrencyKey = this.keys.envCurrentConcurrencyKeyFromQueue(messageQueue); const envReserveConcurrencyKey = this.keys.envReserveConcurrencyKeyFromQueue(messageQueue); const queueReserveConcurrencyKey = this.keys.queueReserveConcurrencyKeyFromQueue(messageQueue); const envQueueKey = this.keys.envQueueKeyFromQueue(messageQueue); logger.debug("Calling acknowledgeMessage", { messageKey, messageQueue, concurrencyKey, envConcurrencyKey, messageId, parentQueue, envQueueKey, service: this.name, }); return this.redis.acknowledgeMessage( parentQueue, messageKey, messageQueue, concurrencyKey, queueReserveConcurrencyKey, envConcurrencyKey, envReserveConcurrencyKey, envQueueKey, messageId, messageQueue ); } async #callNackMessage(messageId: string, message: MessagePayload, messageScore: number) { const messageKey = this.keys.messageKey(message.messageId); const queueKey = message.queue; const parentQueueKey = message.parentQueue; const queueCurrentConcurrencyKey = this.keys.queueCurrentConcurrencyKeyFromQueue(message.queue); const envCurrentConcurrencyKey = this.keys.envCurrentConcurrencyKeyFromQueue(message.queue); const nackCounterKey = this.keys.nackCounterKey(message.messageId); const envQueueKey = this.keys.envQueueKeyFromQueue(message.queue); const queueName = message.queue; logger.debug("Calling nackMessage", { messageKey, queueKey, parentQueueKey, queueCurrentConcurrencyKey, envCurrentConcurrencyKey, nackCounterKey, messageId, messageScore, envQueueKey, service: this.name, }); return this.redis.nackMessage( messageKey, queueKey, parentQueueKey, queueCurrentConcurrencyKey, envCurrentConcurrencyKey, envQueueKey, nackCounterKey, queueName, messageId, String(Date.now()), String(messageScore) ); } #callUpdateGlobalConcurrencyLimits({ envConcurrencyLimitKey, envConcurrencyLimit, }: { envConcurrencyLimitKey: string; envConcurrencyLimit: number; }) { return this.redis.updateGlobalConcurrencyLimits( envConcurrencyLimitKey, String(envConcurrencyLimit) ); } async #callRebalanceParentQueueChild({ parentQueue, childQueue, currentScore, }: { parentQueue: string; childQueue: string; currentScore: string; }) { const rebalanceResult = await this.redis.rebalanceParentQueueChild( childQueue, parentQueue, childQueue, currentScore ); if (rebalanceResult) { logger.debug("Rebalanced parent queue child", { parentQueue, childQueue, currentScore, rebalanceResult, operation: "rebalanceParentQueueChild", service: this.name, }); } return rebalanceResult; } #registerCommands() { this.redis.defineCommand("enqueueMessage", { numberOfKeys: 8, lua: ` local queueKey = KEYS[1] local parentQueueKey = KEYS[2] local messageKey = KEYS[3] local queueCurrentConcurrencyKey = KEYS[4] local queueReserveConcurrencyKey = KEYS[5] local envCurrentConcurrencyKey = KEYS[6] local envReserveConcurrencyKey = KEYS[7] local envQueueKey = KEYS[8] local queueName = ARGV[1] local messageId = ARGV[2] local messageData = ARGV[3] local messageScore = ARGV[4] -- Write the message to the message key redis.call('SET', messageKey, messageData) -- Add the message to the queue redis.call('ZADD', queueKey, messageScore, messageId) -- Add the message to the env queue redis.call('ZADD', envQueueKey, messageScore, messageId) -- Rebalance the parent queue local earliestMessage = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES') if #earliestMessage == 0 then redis.call('ZREM', parentQueueKey, queueName) else redis.call('ZADD', parentQueueKey, earliestMessage[2], queueName) end -- Update the concurrency keys redis.call('SREM', queueCurrentConcurrencyKey, messageId) redis.call('SREM', envCurrentConcurrencyKey, messageId) redis.call('SREM', envReserveConcurrencyKey, messageId) redis.call('SREM', queueReserveConcurrencyKey, messageId) return true `, }); this.redis.defineCommand("enqueueMessageWithReservingConcurrency", { numberOfKeys: 9, lua: ` local queueKey = KEYS[1] local parentQueueKey = KEYS[2] local messageKey = KEYS[3] local queueCurrentConcurrencyKey = KEYS[4] local queueReserveConcurrencyKey = KEYS[5] local envCurrentConcurrencyKey = KEYS[6] local envReserveConcurrencyKey = KEYS[7] local envConcurrencyLimitKey = KEYS[8] local envQueueKey = KEYS[9] local queueName = ARGV[1] local messageId = ARGV[2] local messageData = ARGV[3] local messageScore = ARGV[4] local reserveMessageId = ARGV[5] local defaultEnvConcurrencyLimit = ARGV[6] -- Write the message to the message key redis.call('SET', messageKey, messageData) -- Add the message to the queue redis.call('ZADD', queueKey, messageScore, messageId) -- Add the message to the env queue redis.call('ZADD', envQueueKey, messageScore, messageId) -- Rebalance the parent queue local earliestMessage = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES') if #earliestMessage == 0 then redis.call('ZREM', parentQueueKey, queueName) else redis.call('ZADD', parentQueueKey, earliestMessage[2], queueName) end -- Update the concurrency keys redis.call('SREM', queueCurrentConcurrencyKey, messageId) redis.call('SREM', envCurrentConcurrencyKey, messageId) redis.call('SREM', envReserveConcurrencyKey, messageId) redis.call('SREM', queueReserveConcurrencyKey, messageId) -- Reserve the concurrency for the message local envReserveConcurrencyLimit = tonumber(redis.call('GET', envConcurrencyLimitKey) or defaultEnvConcurrencyLimit) -- Count the number of messages in the reserve concurrency set local envReserveConcurrency = tonumber(redis.call('SCARD', envReserveConcurrencyKey) or '0') -- If there is space, add the messaageId to the env reserve concurrency set if envReserveConcurrency < envReserveConcurrencyLimit then redis.call('SADD', envReserveConcurrencyKey, reserveMessageId) end return true `, }); this.redis.defineCommand("enqueueMessageWithReservingConcurrencyOnRecursiveQueue", { numberOfKeys: 10, lua: ` local queueKey = KEYS[1] local parentQueueKey = KEYS[2] local messageKey = KEYS[3] local queueCurrentConcurrencyKey = KEYS[4] local queueReserveConcurrencyKey = KEYS[5] local queueConcurrencyLimitKey = KEYS[6] local envCurrentConcurrencyKey = KEYS[7] local envReserveConcurrencyKey = KEYS[8] local envConcurrencyLimitKey = KEYS[9] local envQueueKey = KEYS[10] local queueName = ARGV[1] local messageId = ARGV[2] local messageData = ARGV[3] local messageScore = ARGV[4] local reserveMessageId = ARGV[5] local defaultEnvConcurrencyLimit = ARGV[6] -- Get the env reserve concurrency limit because we need it to calculate the max reserve concurrency -- for the specific queue local envReserveConcurrencyLimit = tonumber(redis.call('GET', envConcurrencyLimitKey) or defaultEnvConcurrencyLimit) -- Count the number of messages in the queue reserve concurrency set local queueReserveConcurrency = tonumber(redis.call('SCARD', queueReserveConcurrencyKey) or '0') local queueConcurrencyLimit = tonumber(redis.call('GET', queueConcurrencyLimitKey) or '1000000') local queueReserveConcurrencyLimit = math.min(queueConcurrencyLimit, envReserveConcurrencyLimit) -- If we cannot add the reserve concurrency, then we have to return false if queueReserveConcurrency >= queueReserveConcurrencyLimit then return false end -- Write the message to the message key redis.call('SET', messageKey, messageData) -- Add the message to the queue redis.call('ZADD', queueKey, messageScore, messageId) -- Add the message to the env queue redis.call('ZADD', envQueueKey, messageScore, messageId) -- Rebalance the parent queue local earliestMessage = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES') if #earliestMessage == 0 then redis.call('ZREM', parentQueueKey, queueName) else redis.call('ZADD', parentQueueKey, earliestMessage[2], queueName) end -- Update the concurrency keys redis.call('SREM', queueCurrentConcurrencyKey, messageId) redis.call('SREM', envCurrentConcurrencyKey, messageId) redis.call('SREM', envReserveConcurrencyKey, messageId) redis.call('SREM', queueReserveConcurrencyKey, messageId) -- Count the number of messages in the env reserve concurrency set local envReserveConcurrency = tonumber(redis.call('SCARD', envReserveConcurrencyKey) or '0') -- If there is space, add the messaageId to the env reserve concurrency set if envReserveConcurrency < envReserveConcurrencyLimit then redis.call('SADD', envReserveConcurrencyKey, reserveMessageId) end redis.call('SADD', queueReserveConcurrencyKey, reserveMessageId) return true `, }); this.redis.defineCommand("dequeueMessages", { numberOfKeys: 9, lua: ` local queueKey = KEYS[1] local parentQueueKey = KEYS[2] local queueConcurrencyLimitKey = KEYS[3] local envConcurrencyLimitKey = KEYS[4] local queueCurrentConcurrencyKey = KEYS[5] local queueReserveConcurrencyKey = KEYS[6] local envCurrentConcurrencyKey = KEYS[7] local envReserveConcurrencyKey = KEYS[8] local envQueueKey = KEYS[9] local queueName = ARGV[1] local currentTime = tonumber(ARGV[2]) local defaultEnvConcurrencyLimit = ARGV[3] local maxCount = tonumber(ARGV[4] or '1') -- Check current env concurrency against the limit local envCurrentConcurrency = tonumber(redis.call('SCARD', envCurrentConcurrencyKey) or '0') local envConcurrencyLimit = tonumber(redis.call('GET', envConcurrencyLimitKey) or defaultEnvConcurrencyLimit) local envReserveConcurrency = tonumber(redis.call('SCARD', envReserveConcurrencyKey) or '0') local totalEnvConcurrencyLimit = envConcurrencyLimit + envReserveConcurrency if envCurrentConcurrency >= totalEnvConcurrencyLimit then return nil end -- Check current queue concurrency against the limit local queueCurrentConcurrency = tonumber(redis.call('SCARD', queueCurrentConcurrencyKey) or '0') local queueConcurrencyLimit = math.min(tonumber(redis.call('GET', queueConcurrencyLimitKey) or '1000000'), envConcurrencyLimit) local queueReserveConcurrency = tonumber(redis.call('SCARD', queueReserveConcurrencyKey) or '0') local totalQueueConcurrencyLimit = queueConcurrencyLimit + queueReserveConcurrency -- Check condition only if concurrencyLimit exists if queueCurrentConcurrency >= totalQueueConcurrencyLimit then return nil end -- Calculate how many messages we can actually dequeue based on concurrency limits local envAvailableCapacity = totalEnvConcurrencyLimit - envCurrentConcurrency local queueAvailableCapacity = totalQueueConcurrencyLimit - queueCurrentConcurrency local actualMaxCount = math.min(maxCount, envAvailableCapacity, queueAvailableCapacity) if actualMaxCount <= 0 then return nil end -- Attempt to dequeue messages up to actualMaxCount local messagesWithScores = redis.call('ZRANGEBYSCORE', queueKey, '-inf', currentTime, 'WITHSCORES', 'LIMIT', 0, actualMaxCount) if #messagesWithScores == 0 then return nil end local messageIds = {} for i = 1, #messagesWithScores, 2 do table.insert(messageIds, messagesWithScores[i]) end -- Remove the messages from the queue and update concurrency redis.call('ZREM', queueKey, unpack(messageIds)) redis.call('ZREM', envQueueKey, unpack(messageIds)) redis.call('SADD', queueCurrentConcurrencyKey, unpack(messageIds)) redis.call('SADD', envCurrentConcurrencyKey, unpack(messageIds)) -- Remove the message from the reserve concurrency set redis.call('SREM', envReserveConcurrencyKey, unpack(messageIds)) -- Remove the message from the queue reserve concurrency set redis.call('SREM', queueReserveConcurrencyKey, unpack(messageIds)) -- Rebalance the parent queue local earliestMessage = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES') if #earliestMessage == 0 then redis.call('ZREM', parentQueueKey, queueName) else redis.call('ZADD', parentQueueKey, earliestMessage[2], queueName) end return messagesWithScores `, }); this.redis.defineCommand("popMessageFromWorkerQueue", { numberOfKeys: 1, lua: ` local workerQueueKey = KEYS[1] -- lpop the first message from the worker queue local messageId = redis.call('LPOP', workerQueueKey) -- if there is no messageId, return nil if not messageId then return nil end -- get the length of the worker queue local queueLength = tonumber(redis.call('LLEN', workerQueueKey) or '0') return {messageId, queueLength} -- Return message details `, }); this.redis.defineCommand("acknowledgeMessage", { numberOfKeys: 8, lua: ` local parentQueueKey = KEYS[1] local messageKey = KEYS[2] local queueKey = KEYS[3] local queueConcurrencyKey = KEYS[4] local queueReserveConcurrencyKey = KEYS[5] local envCurrentConcurrencyKey = KEYS[6] local envReserveConcurrencyKey = KEYS[7] local envQueueKey = KEYS[8] local messageId = ARGV[1] local queueName = ARGV[2] -- Remove the message from the queue redis.call('ZREM', queueKey, messageId) -- Rebalance the parent queue local earliestMessage = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES') if #earliestMessage == 0 then redis.call('ZREM', parentQueueKey, queueName) else redis.call('ZADD', parentQueueKey, earliestMessage[2], queueName) end -- Update the concurrency keys redis.call('SREM', queueConcurrencyKey, messageId) redis.call('SREM', envCurrentConcurrencyKey, messageId) redis.call('SREM', envReserveConcurrencyKey, messageId) redis.call('SREM', queueReserveConcurrencyKey, messageId) redis.call('ZREM', envQueueKey, messageId) redis.call('DEL', messageKey) `, }); this.redis.defineCommand("requeueMessage", { numberOfKeys: 8, lua: ` local queueKey = KEYS[1] local parentQueueKey = KEYS[2] local messageKey = KEYS[3] local queueCurrentConcurrencyKey = KEYS[4] local queueReserveConcurrencyKey = KEYS[5] local envCurrentConcurrencyKey = KEYS[6] local envReserveConcurrencyKey = KEYS[7] local envQueueKey = KEYS[8] local queueName = ARGV[1] local messageId = ARGV[2] local messageData = ARGV[3] local messageScore = ARGV[4] -- Write the new message data redis.call('SET', messageKey, messageData) -- Add the message to the queue with a new score redis.call('ZADD', queueKey, messageScore, messageId) redis.call('ZADD', envQueueKey, messageScore, messageId) -- Rebalance the parent queue local earliestMessage = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES') if #earliestMessage == 0 then redis.call('ZREM', parentQueueKey, queueName) else redis.call('ZADD', parentQueueKey, earliestMessage[2], queueName) end -- Clear all concurrency sets (combined from both scripts) redis.call('SREM', queueCurrentConcurrencyKey, messageId) redis.call('SREM', queueReserveConcurrencyKey, messageId) redis.call('SREM', envCurrentConcurrencyKey, messageId) redis.call('SREM', envReserveConcurrencyKey, messageId) return true `, }); this.redis.defineCommand("delayedRequeueMessage", { numberOfKeys: 5, lua: ` local messageKey = KEYS[1] local queueCurrentConcurrencyKey = KEYS[2] local queueReserveConcurrencyKey = KEYS[3] local envCurrentConcurrencyKey = KEYS[4] local envReserveConcurrencyKey = KEYS[5] local messageId = ARGV[1] local messageData = ARGV[2] -- Write the new message data redis.call('SET', messageKey, messageData) -- Clear all concurrency sets redis.call('SREM', queueCurrentConcurrencyKey, messageId) redis.call('SREM', queueReserveConcurrencyKey, messageId) redis.call('SREM', envCurrentConcurrencyKey, messageId) redis.call('SREM', envReserveConcurrencyKey, messageId) return true `, }); this.redis.defineCommand("nackMessage", { numberOfKeys: 7, lua: ` local messageKey = KEYS[1] local queueKey = KEYS[2] local parentQueueKey = KEYS[3] local queueCurrentConcurrencyKey = KEYS[4] local envCurrentConcurrencyKey = KEYS[5] local envQueueKey = KEYS[6] local nackCounterKey = KEYS[7] local queueName = ARGV[1] local messageId = ARGV[2] local currentTime = tonumber(ARGV[3]) local messageScore = tonumber(ARGV[4]) -- Update the current concurrency keys redis.call('SREM', queueCurrentConcurrencyKey, messageId) redis.call('SREM', envCurrentConcurrencyKey, messageId) -- Enqueue the message into the queue redis.call('ZADD', queueKey, messageScore, messageId) -- Enqueue the message into the env queue redis.call('ZADD', envQueueKey, messageScore, messageId) -- Increment the nack counter with an expiry of 30 days redis.call('INCR', nackCounterKey) redis.call('EXPIRE', nackCounterKey, 2592000) -- Rebalance the parent queue local earliestMessage = redis.call('ZRANGE', queueKey, 0, 0, 'WITHSCORES') if #earliestMessage == 0 then redis.call('ZREM', parentQueueKey, queueName) else redis.call('ZADD', parentQueueKey, earliestMessage[2], queueName) end `, }); this.redis.defineCommand("updateGlobalConcurrencyLimits", { numberOfKeys: 1, lua: ` local envConcurrencyLimitKey = KEYS[1] local envConcurrencyLimit = ARGV[1] redis.call('SET', envConcurrencyLimitKey, envConcurrencyLimit) `, }); this.redis.defineCommand("rebalanceParentQueueChild", { numberOfKeys: 2, lua: ` -- Keys: childQueueKey, parentQueueKey local childQueueKey = KEYS[1] local parentQueueKey = KEYS[2] -- Args: childQueueName, currentScore local childQueueName = ARGV[1] local currentScore = ARGV[2] -- Rebalance the parent queue local earliestMessage = redis.call('ZRANGE', childQueueKey, 0, 0, 'WITHSCORES') if #earliestMessage == 0 then redis.call('ZREM', parentQueueKey, childQueueName) -- Return true because the parent queue was rebalanced return true else -- If the earliest message is different, update the parent queue and return true, else return false if earliestMessage[2] == currentScore then return false end redis.call('ZADD', parentQueueKey, earliestMessage[2], childQueueName) return earliestMessage[2] end `, }); } } declare module "ioredis" { interface RedisCommander { enqueueMessage( queueKey: string, parentQueueKey: string, messageKey: string, queueCurrentConcurrencyKey: string, queueReserveConcurrencyKey: string, envCurrentConcurrencyKey: string, envReserveConcurrencyKey: string, envQueueKey: string, queueName: string, messageId: string, messageData: string, messageScore: string, callback?: Callback ): Result; enqueueMessageWithReservingConcurrency( queueKey: string, parentQueueKey: string, messageKey: string, queueCurrentConcurrencyKey: string, queueReserveConcurrencyKey: string, envCurrentConcurrencyKey: string, envReserveConcurrencyKey: string, envConcurrencyLimitKey: string, envQueueKey: string, queueName: string, messageId: string, messageData: string, messageScore: string, reserveMessageId: string, defaultEnvConcurrencyLimit: string, callback?: Callback ): Result; enqueueMessageWithReservingConcurrencyOnRecursiveQueue( queueKey: string, parentQueueKey: string, messageKey: string, queueCurrentConcurrencyKey: string, queueReserveConcurrencyKey: string, queueConcurrencyLimitKey: string, envCurrentConcurrencyKey: string, envReserveConcurrencyKey: string, envConcurrencyLimitKey: string, envQueueKey: string, queueName: string, messageId: string, messageData: string, messageScore: string, reserveMessageId: string, defaultEnvConcurrencyLimit: string, callback?: Callback ): Result; dequeueMessages( queueKey: string, parentQueueKey: string, queueConcurrencyLimitKey: string, envConcurrencyLimitKey: string, queueCurrentConcurrencyKey: string, queueReserveConcurrencyKey: string, envCurrentConcurrencyKey: string, envReserveConcurrencyKey: string, envQueueKey: string, queueName: string, currentTime: string, defaultEnvConcurrencyLimit: string, maxCount: string, callback?: Callback ): Result; popMessageFromWorkerQueue( workerQueueKey: string, callback?: Callback<[string, string] | null> ): Result<[string, string] | null, Context>; requeueMessage( queueKey: string, parentQueueKey: string, messageKey: string, queueCurrentConcurrencyKey: string, queueReserveConcurrencyKey: string, envCurrentConcurrencyKey: string, envReserveConcurrencyKey: string, envQueueKey: string, queueName: string, messageId: string, messageData: string, messageScore: string, callback?: Callback ): Result; delayedRequeueMessage( messageKey: string, queueCurrentConcurrencyKey: string, queueReserveConcurrencyKey: string, envCurrentConcurrencyKey: string, envReserveConcurrencyKey: string, messageId: string, messageData: string, callback?: Callback ): Result; acknowledgeMessage( parentQueue: string, messageKey: string, messageQueue: string, concurrencyKey: string, queueReserveConcurrencyKey: string, envConcurrencyKey: string, envReserveConcurrencyKey: string, envQueueKey: string, messageId: string, messageQueueName: string, callback?: Callback ): Result; nackMessage( messageKey: string, queueKey: string, parentQueueKey: string, queueCurrentConcurrencyKey: string, envCurrentConcurrencyKey: string, envQueueKey: string, nackCounterKey: string, queueName: string, messageId: string, currentTime: string, messageScore: string, callback?: Callback ): Result; updateGlobalConcurrencyLimits( envConcurrencyLimitKey: string, envConcurrencyLimit: string, callback?: Callback ): Result; rebalanceParentQueueChild( childQueueKey: string, parentQueueKey: string, childQueueName: string, currentScore: string, callback?: Callback ): Result; } } export const marqs = singleton("marqs", getMarQSClient); function getMarQSClient() { if (!env.REDIS_HOST || !env.REDIS_PORT) { throw new Error( "Could not initialize Trigger.dev because process.env.REDIS_HOST and process.env.REDIS_PORT are required to be set." ); } const redisOptions = { keyPrefix: KEY_PREFIX, port: env.REDIS_PORT, host: env.REDIS_HOST, username: env.REDIS_USERNAME, password: env.REDIS_PASSWORD, enableAutoPipelining: true, ...(env.REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }), }; const redis = new Redis(redisOptions); const keysProducer = new MarQSShortKeyProducer(KEY_PREFIX); return new MarQS({ name: "marqs", tracer: trace.getTracer("marqs"), keysProducer, visibilityTimeoutStrategy: new V3LegacyRunEngineWorkerVisibilityTimeout(), queuePriorityStrategy: new FairDequeuingStrategy({ tracer: tracer, redis, parentQueueLimit: env.MARQS_SHARED_QUEUE_LIMIT, keys: keysProducer, defaultEnvConcurrency: env.DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT, biases: { concurrencyLimitBias: env.MARQS_CONCURRENCY_LIMIT_BIAS, availableCapacityBias: env.MARQS_AVAILABLE_CAPACITY_BIAS, queueAgeRandomization: env.MARQS_QUEUE_AGE_RANDOMIZATION_BIAS, }, reuseSnapshotCount: env.MARQS_REUSE_SNAPSHOT_COUNT, maximumEnvCount: env.MARQS_MAXIMUM_ENV_COUNT, maximumQueuePerEnvCount: env.MARQS_MAXIMUM_QUEUE_PER_ENV_COUNT, }), envQueuePriorityStrategy: new FairDequeuingStrategy({ tracer: tracer, redis, parentQueueLimit: env.MARQS_DEV_QUEUE_LIMIT, keys: keysProducer, defaultEnvConcurrency: env.DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT, biases: { concurrencyLimitBias: 0.0, availableCapacityBias: 0.0, queueAgeRandomization: 0.1, }, }), workers: 1, redis, defaultEnvConcurrency: env.DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT, defaultOrgConcurrency: env.DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT, visibilityTimeoutInMs: env.MARQS_VISIBILITY_TIMEOUT_MS, enableRebalancing: !env.MARQS_DISABLE_REBALANCING, maximumNackCount: env.MARQS_MAXIMUM_NACK_COUNT, subscriber: concurrencyTracker, sharedWorkerQueueConsumerIntervalMs: env.MARQS_SHARED_WORKER_QUEUE_CONSUMER_INTERVAL_MS, sharedWorkerQueueMaxMessageCount: env.MARQS_SHARED_WORKER_QUEUE_MAX_MESSAGE_COUNT, eagerDequeuingEnabled: env.MARQS_SHARED_WORKER_QUEUE_EAGER_DEQUEUE_ENABLED === "1", sharedWorkerQueueCooloffCountThreshold: env.MARQS_SHARED_WORKER_QUEUE_COOLOFF_COUNT_THRESHOLD, sharedWorkerQueueCooloffPeriodMs: env.MARQS_SHARED_WORKER_QUEUE_COOLOFF_PERIOD_MS, workerOptions: { enabled: env.MARQS_WORKER_ENABLED === "1", pollIntervalMs: env.MARQS_WORKER_POLL_INTERVAL_MS, immediatePollIntervalMs: env.MARQS_WORKER_IMMEDIATE_POLL_INTERVAL_MS, shutdownTimeoutMs: env.MARQS_WORKER_SHUTDOWN_TIMEOUT_MS, concurrency: { workers: env.MARQS_WORKER_COUNT, tasksPerWorker: env.MARQS_WORKER_CONCURRENCY_TASKS_PER_WORKER, limit: env.MARQS_WORKER_CONCURRENCY_LIMIT, }, redisOptions: { keyPrefix: KEY_PREFIX, port: env.REDIS_PORT ?? undefined, host: env.REDIS_HOST ?? undefined, username: env.REDIS_USERNAME ?? undefined, password: env.REDIS_PASSWORD ?? undefined, enableAutoPipelining: true, ...(env.REDIS_TLS_DISABLED === "true" ? {} : { tls: {} }), }, }, }); }