Files
triggerdotdev--trigger.dev/apps/webapp/app/v3/marqs/index.server.ts
T
2026-07-13 13:32:57 +08:00

2692 lines
82 KiB
TypeScript

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<AsyncWorker> = [];
private worker: Worker<typeof workerCatalog>;
private queueDequeueCooloffPeriod: Map<string, number> = new Map();
private queueDequeueCooloffCounts: Map<string, number> = 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<void>((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<string, unknown>,
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<string, unknown>,
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<string, unknown>,
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<string, unknown>
) {
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<T>(
name: string,
fn: (span: Span) => Promise<T>,
options?: SpanOptions & { sampleRate?: number }
): Promise<T> {
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<number> {
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<void>((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<string>(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<void>((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<string, string> = {};
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<Context> {
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<string>
): Result<string, Context>;
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<string>
): Result<string, Context>;
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<string>
): Result<string, Context>;
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<string[]>
): Result<string[] | null, Context>;
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<string>
): Result<string, Context>;
delayedRequeueMessage(
messageKey: string,
queueCurrentConcurrencyKey: string,
queueReserveConcurrencyKey: string,
envCurrentConcurrencyKey: string,
envReserveConcurrencyKey: string,
messageId: string,
messageData: string,
callback?: Callback<string>
): Result<string, Context>;
acknowledgeMessage(
parentQueue: string,
messageKey: string,
messageQueue: string,
concurrencyKey: string,
queueReserveConcurrencyKey: string,
envConcurrencyKey: string,
envReserveConcurrencyKey: string,
envQueueKey: string,
messageId: string,
messageQueueName: string,
callback?: Callback<void>
): Result<void, Context>;
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<void>
): Result<void, Context>;
updateGlobalConcurrencyLimits(
envConcurrencyLimitKey: string,
envConcurrencyLimit: string,
callback?: Callback<void>
): Result<void, Context>;
rebalanceParentQueueChild(
childQueueKey: string,
parentQueueKey: string,
childQueueName: string,
currentScore: string,
callback?: Callback<number | string | null>
): Result<number | string | null, Context>;
}
}
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: {} }),
},
},
});
}