@trigger.dev/redis-worker 4.5.0-rc.7 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -64,6 +64,22 @@ declare class SimpleQueue<TMessageCatalog extends MessageCatalogSchema> {
64
64
  size({ includeFuture }?: {
65
65
  includeFuture?: boolean;
66
66
  }): Promise<number>;
67
+ /**
68
+ * Age (in ms) of the oldest *overdue* message — the oldest item whose scheduled
69
+ * time has already passed (score <= now). Returns 0 when the queue is empty or
70
+ * only holds future/delayed or in-flight (future-scored) items.
71
+ *
72
+ * Resolves the candidate against the `items` hash so orphaned `queue` entries
73
+ * (a member whose payload is missing — the same stale state `dequeueItems`
74
+ * cleans up) don't report a phantom stall for work that can't be dequeued. The
75
+ * Lua scans due items oldest-first and returns the first score whose payload
76
+ * still exists.
77
+ *
78
+ * This is the generic stall signal: it stays at 0 while a queue drains healthily
79
+ * and rises only when due work sits undrained (poison block, dead consumer,
80
+ * backpressure).
81
+ */
82
+ oldestMessageAge(): Promise<number>;
67
83
  getJob(id: string): Promise<QueueItem<TMessageCatalog> | null>;
68
84
  moveToDeadLetterQueue(id: string, errorMessage: string): Promise<void>;
69
85
  sizeOfDeadLetterQueue(): Promise<number>;
@@ -80,6 +96,7 @@ declare module "@internal/redis" {
80
96
  moveToDeadLetterQueue(queue: string, items: string, dlq: string, dlqItems: string, id: string, errorMessage: string, callback?: Callback<number>): Result<number, Context>;
81
97
  enqueueItemOnce(queue: string, items: string, id: string, score: number, serializedItem: string, callback?: Callback<number>): Result<number, Context>;
82
98
  getJob(queue: string, items: string, id: string, callback?: Callback<[string, string, string] | null>): Result<[string, string, string] | null, Context>;
99
+ getOldestDueScore(queue: string, items: string, now: number, callback?: Callback<string | number>): Result<string | number, Context>;
83
100
  }
84
101
  }
85
102
 
@@ -893,6 +910,101 @@ interface WeightedSchedulerConfig {
893
910
  tracer?: Tracer;
894
911
  }
895
912
 
913
+ interface ConcurrencyManagerOptions {
914
+ redis: RedisOptions;
915
+ keys: FairQueueKeyProducer;
916
+ groups: ConcurrencyGroupConfig[];
917
+ }
918
+ /**
919
+ * ConcurrencyManager handles multi-level concurrency tracking and limiting.
920
+ *
921
+ * Features:
922
+ * - Multiple concurrent concurrency groups (tenant, org, project, etc.)
923
+ * - Atomic reserve/release operations using Lua scripts
924
+ * - Efficient batch checking of all groups
925
+ */
926
+ declare class ConcurrencyManager {
927
+ #private;
928
+ private options;
929
+ private redis;
930
+ private keys;
931
+ private groups;
932
+ private groupsByName;
933
+ constructor(options: ConcurrencyManagerOptions);
934
+ /**
935
+ * Check if a message can be processed given all concurrency constraints.
936
+ * Checks all configured groups and returns the first one at capacity.
937
+ */
938
+ canProcess(queue: QueueDescriptor): Promise<ConcurrencyCheckResult>;
939
+ /**
940
+ * Reserve concurrency slots for a message across all groups.
941
+ * Atomic - either all groups are reserved or none.
942
+ *
943
+ * @returns true if reservation successful, false if any group is at capacity
944
+ */
945
+ reserve(queue: QueueDescriptor, messageId: string): Promise<boolean>;
946
+ /**
947
+ * Release concurrency slots for a message across all groups.
948
+ */
949
+ release(queue: QueueDescriptor, messageId: string): Promise<void>;
950
+ /**
951
+ * Release concurrency slots for multiple messages in a single pipeline.
952
+ * More efficient than calling release() multiple times.
953
+ */
954
+ releaseBatch(messages: Array<{
955
+ queue: QueueDescriptor;
956
+ messageId: string;
957
+ }>): Promise<void>;
958
+ /**
959
+ * Get current concurrency for a specific group.
960
+ */
961
+ getCurrentConcurrency(groupName: string, groupId: string): Promise<number>;
962
+ /**
963
+ * Get available capacity for a queue across all concurrency groups.
964
+ * Returns the minimum available capacity across all groups.
965
+ */
966
+ getAvailableCapacity(queue: QueueDescriptor): Promise<number>;
967
+ /**
968
+ * Get concurrency limit for a specific group.
969
+ */
970
+ getConcurrencyLimit(groupName: string, groupId: string): Promise<number>;
971
+ /**
972
+ * Check if a group is at capacity.
973
+ */
974
+ isAtCapacity(groupName: string, groupId: string): Promise<boolean>;
975
+ /**
976
+ * Get full state for a group.
977
+ */
978
+ getState(groupName: string, groupId: string): Promise<ConcurrencyState>;
979
+ /**
980
+ * Get all active message IDs for a group.
981
+ */
982
+ getActiveMessages(groupName: string, groupId: string): Promise<string[]>;
983
+ /**
984
+ * Force-clear concurrency for a group (use with caution).
985
+ * Useful for cleanup after crashes.
986
+ */
987
+ clearGroup(groupName: string, groupId: string): Promise<void>;
988
+ /**
989
+ * Remove a specific message from concurrency tracking.
990
+ * Useful for cleanup.
991
+ */
992
+ removeMessage(messageId: string, queue: QueueDescriptor): Promise<void>;
993
+ /**
994
+ * Get configured group names.
995
+ */
996
+ getGroupNames(): string[];
997
+ /**
998
+ * Close the Redis connection.
999
+ */
1000
+ close(): Promise<void>;
1001
+ }
1002
+ declare module "@internal/redis" {
1003
+ interface RedisCommander<Context> {
1004
+ reserveConcurrency(numKeys: number, keys: string[], messageId: string, ...limits: string[]): Promise<number>;
1005
+ }
1006
+ }
1007
+
896
1008
  /**
897
1009
  * Default key producer for the fair queue system.
898
1010
  * Uses a configurable prefix and standard key structure.
@@ -1051,479 +1163,132 @@ declare module "@internal/redis" {
1051
1163
  }
1052
1164
  }
1053
1165
 
1054
- interface ConcurrencyManagerOptions {
1055
- redis: RedisOptions;
1056
- keys: FairQueueKeyProducer;
1057
- groups: ConcurrencyGroupConfig[];
1058
- }
1059
1166
  /**
1060
- * ConcurrencyManager handles multi-level concurrency tracking and limiting.
1061
- *
1062
- * Features:
1063
- * - Multiple concurrent concurrency groups (tenant, org, project, etc.)
1064
- * - Atomic reserve/release operations using Lua scripts
1065
- * - Efficient batch checking of all groups
1167
+ * Base class for scheduler implementations.
1168
+ * Provides common utilities and default implementations.
1066
1169
  */
1067
- declare class ConcurrencyManager {
1068
- #private;
1069
- private options;
1070
- private redis;
1071
- private keys;
1072
- private groups;
1073
- private groupsByName;
1074
- constructor(options: ConcurrencyManagerOptions);
1170
+ declare abstract class BaseScheduler implements FairScheduler {
1075
1171
  /**
1076
- * Check if a message can be processed given all concurrency constraints.
1077
- * Checks all configured groups and returns the first one at capacity.
1172
+ * Select queues for processing from a master queue shard.
1173
+ * Must be implemented by subclasses.
1078
1174
  */
1079
- canProcess(queue: QueueDescriptor): Promise<ConcurrencyCheckResult>;
1175
+ abstract selectQueues(masterQueueShard: string, consumerId: string, context: SchedulerContext): Promise<TenantQueues[]>;
1080
1176
  /**
1081
- * Reserve concurrency slots for a message across all groups.
1082
- * Atomic - either all groups are reserved or none.
1083
- *
1084
- * @returns true if reservation successful, false if any group is at capacity
1177
+ * Called after processing a message to update scheduler state.
1178
+ * Default implementation does nothing.
1085
1179
  */
1086
- reserve(queue: QueueDescriptor, messageId: string): Promise<boolean>;
1180
+ recordProcessed(_tenantId: string, _queueId: string): Promise<void>;
1087
1181
  /**
1088
- * Release concurrency slots for a message across all groups.
1182
+ * Called after processing multiple messages to update scheduler state.
1183
+ * Batch variant for efficiency - reduces Redis calls when processing multiple messages.
1184
+ * Default implementation does nothing.
1089
1185
  */
1090
- release(queue: QueueDescriptor, messageId: string): Promise<void>;
1186
+ recordProcessedBatch(_tenantId: string, _queueId: string, _count: number): Promise<void>;
1091
1187
  /**
1092
- * Release concurrency slots for multiple messages in a single pipeline.
1093
- * More efficient than calling release() multiple times.
1188
+ * Initialize the scheduler.
1189
+ * Default implementation does nothing.
1094
1190
  */
1095
- releaseBatch(messages: Array<{
1096
- queue: QueueDescriptor;
1097
- messageId: string;
1098
- }>): Promise<void>;
1191
+ initialize(): Promise<void>;
1099
1192
  /**
1100
- * Get current concurrency for a specific group.
1193
+ * Cleanup scheduler resources.
1194
+ * Default implementation does nothing.
1101
1195
  */
1102
- getCurrentConcurrency(groupName: string, groupId: string): Promise<number>;
1196
+ close(): Promise<void>;
1103
1197
  /**
1104
- * Get available capacity for a queue across all concurrency groups.
1105
- * Returns the minimum available capacity across all groups.
1198
+ * Helper to group queues by tenant.
1106
1199
  */
1107
- getAvailableCapacity(queue: QueueDescriptor): Promise<number>;
1200
+ protected groupQueuesByTenant(queues: Array<{
1201
+ queueId: string;
1202
+ tenantId: string;
1203
+ }>): Map<string, string[]>;
1108
1204
  /**
1109
- * Get concurrency limit for a specific group.
1205
+ * Helper to convert grouped queues to TenantQueues array.
1110
1206
  */
1111
- getConcurrencyLimit(groupName: string, groupId: string): Promise<number>;
1207
+ protected toTenantQueuesArray(grouped: Map<string, string[]>): TenantQueues[];
1112
1208
  /**
1113
- * Check if a group is at capacity.
1209
+ * Helper to filter out tenants at capacity.
1114
1210
  */
1115
- isAtCapacity(groupName: string, groupId: string): Promise<boolean>;
1211
+ protected filterAtCapacity(tenants: TenantQueues[], context: SchedulerContext, groupName?: string): Promise<TenantQueues[]>;
1212
+ }
1213
+ /**
1214
+ * Simple noop scheduler that returns empty results.
1215
+ * Useful for testing or disabling scheduling.
1216
+ */
1217
+ declare class NoopScheduler extends BaseScheduler {
1218
+ selectQueues(_masterQueueShard: string, _consumerId: string, _context: SchedulerContext): Promise<TenantQueues[]>;
1219
+ }
1220
+
1221
+ /**
1222
+ * Deficit Round Robin (DRR) Scheduler.
1223
+ *
1224
+ * DRR ensures fair processing across tenants by:
1225
+ * - Allocating a "quantum" of credits to each tenant per round
1226
+ * - Accumulating unused credits as "deficit"
1227
+ * - Processing from tenants with available deficit
1228
+ * - Capping deficit to prevent starvation
1229
+ *
1230
+ * Key improvements over basic implementations:
1231
+ * - Atomic deficit operations using Lua scripts
1232
+ * - Efficient iteration through tenants
1233
+ * - Automatic deficit cleanup for inactive tenants
1234
+ */
1235
+ declare class DRRScheduler extends BaseScheduler {
1236
+ #private;
1237
+ private config;
1238
+ private redis;
1239
+ private keys;
1240
+ private quantum;
1241
+ private maxDeficit;
1242
+ private masterQueueLimit;
1243
+ private logger;
1244
+ constructor(config: DRRSchedulerConfig);
1116
1245
  /**
1117
- * Get full state for a group.
1246
+ * Select queues for processing using DRR algorithm.
1247
+ *
1248
+ * Algorithm:
1249
+ * 1. Get all queues from the master shard
1250
+ * 2. Group by tenant
1251
+ * 3. Filter out tenants at concurrency capacity
1252
+ * 4. Add quantum to each tenant's deficit (atomically)
1253
+ * 5. Select queues from tenants with deficit >= 1
1254
+ * 6. Order tenants by deficit (highest first for fairness)
1118
1255
  */
1119
- getState(groupName: string, groupId: string): Promise<ConcurrencyState>;
1256
+ selectQueues(masterQueueShard: string, consumerId: string, context: SchedulerContext): Promise<TenantQueues[]>;
1120
1257
  /**
1121
- * Get all active message IDs for a group.
1258
+ * Select queues using the two-level tenant dispatch index.
1259
+ *
1260
+ * Algorithm:
1261
+ * 1. ZRANGEBYSCORE on dispatch index (gets only tenants with queues - much smaller)
1262
+ * 2. Add quantum to each tenant's deficit (atomically)
1263
+ * 3. Check capacity as safety net (dispatch should only have tenants with capacity)
1264
+ * 4. Select tenants with deficit >= 1, sorted by deficit (highest first)
1265
+ * 5. For each tenant, fetch their queues from Level 2 index
1122
1266
  */
1123
- getActiveMessages(groupName: string, groupId: string): Promise<string[]>;
1267
+ selectQueuesFromDispatch(dispatchShardKey: string, consumerId: string, context: DispatchSchedulerContext): Promise<TenantQueues[]>;
1124
1268
  /**
1125
- * Force-clear concurrency for a group (use with caution).
1126
- * Useful for cleanup after crashes.
1269
+ * Record that a message was processed from a tenant.
1270
+ * Decrements the tenant's deficit.
1127
1271
  */
1128
- clearGroup(groupName: string, groupId: string): Promise<void>;
1272
+ recordProcessed(tenantId: string, _queueId: string): Promise<void>;
1129
1273
  /**
1130
- * Remove a specific message from concurrency tracking.
1131
- * Useful for cleanup.
1274
+ * Record that multiple messages were processed from a tenant.
1275
+ * Decrements the tenant's deficit by count atomically.
1132
1276
  */
1133
- removeMessage(messageId: string, queue: QueueDescriptor): Promise<void>;
1277
+ recordProcessedBatch(tenantId: string, _queueId: string, count: number): Promise<void>;
1278
+ close(): Promise<void>;
1134
1279
  /**
1135
- * Get configured group names.
1280
+ * Get the current deficit for a tenant.
1136
1281
  */
1137
- getGroupNames(): string[];
1282
+ getDeficit(tenantId: string): Promise<number>;
1138
1283
  /**
1139
- * Close the Redis connection.
1284
+ * Reset deficit for a tenant.
1285
+ * Used when a tenant has no more active queues.
1140
1286
  */
1141
- close(): Promise<void>;
1142
- }
1143
- declare module "@internal/redis" {
1144
- interface RedisCommander<Context> {
1145
- reserveConcurrency(numKeys: number, keys: string[], messageId: string, ...limits: string[]): Promise<number>;
1146
- }
1147
- }
1148
-
1149
- interface VisibilityManagerOptions {
1150
- redis: RedisOptions;
1151
- keys: FairQueueKeyProducer;
1152
- shardCount: number;
1153
- defaultTimeoutMs: number;
1154
- logger?: {
1155
- debug: (message: string, context?: Record<string, unknown>) => void;
1156
- error: (message: string, context?: Record<string, unknown>) => void;
1157
- };
1158
- }
1159
- /**
1160
- * VisibilityManager handles message visibility timeouts for safe message processing.
1161
- *
1162
- * Features:
1163
- * - Claim messages with visibility timeout
1164
- * - Heartbeat to extend timeout
1165
- * - Automatic reclaim of timed-out messages
1166
- * - Per-shard in-flight tracking
1167
- *
1168
- * Data structures:
1169
- * - In-flight sorted set: score = deadline timestamp, member = "{messageId}:{queueId}"
1170
- * - In-flight data hash: field = messageId, value = JSON message data
1171
- */
1172
- declare class VisibilityManager {
1173
- #private;
1174
- private options;
1175
- private redis;
1176
- private keys;
1177
- private shardCount;
1178
- private defaultTimeoutMs;
1179
- private logger;
1180
- constructor(options: VisibilityManagerOptions);
1181
- /**
1182
- * Claim a message for processing.
1183
- * Moves the message from its queue to the in-flight set with a visibility timeout.
1184
- *
1185
- * @param queueId - The queue to claim from
1186
- * @param queueKey - The Redis key for the queue sorted set
1187
- * @param queueItemsKey - The Redis key for the queue items hash
1188
- * @param consumerId - ID of the consumer claiming the message
1189
- * @param timeoutMs - Visibility timeout in milliseconds
1190
- * @returns Claim result with the message if successful
1191
- */
1192
- claim<TPayload = unknown>(queueId: string, queueKey: string, queueItemsKey: string, consumerId: string, timeoutMs?: number): Promise<ClaimResult<TPayload>>;
1193
- /**
1194
- * Claim multiple messages for processing (batch claim).
1195
- * Moves up to maxCount messages from the queue to the in-flight set.
1196
- *
1197
- * @param queueId - The queue to claim from
1198
- * @param queueKey - The Redis key for the queue sorted set
1199
- * @param queueItemsKey - The Redis key for the queue items hash
1200
- * @param consumerId - ID of the consumer claiming the messages
1201
- * @param maxCount - Maximum number of messages to claim
1202
- * @param timeoutMs - Visibility timeout in milliseconds
1203
- * @returns Array of claimed messages
1204
- */
1205
- claimBatch<TPayload = unknown>(queueId: string, queueKey: string, queueItemsKey: string, consumerId: string, maxCount: number, timeoutMs?: number): Promise<Array<InFlightMessage<TPayload>>>;
1206
- /**
1207
- * Extend the visibility timeout for a message (heartbeat).
1208
- *
1209
- * @param messageId - The message ID
1210
- * @param queueId - The queue ID
1211
- * @param extendMs - Additional milliseconds to add to the deadline
1212
- * @returns true if the heartbeat was successful
1213
- */
1214
- heartbeat(messageId: string, queueId: string, extendMs: number): Promise<boolean>;
1215
- /**
1216
- * Mark a message as successfully processed.
1217
- * Removes the message from in-flight tracking.
1218
- *
1219
- * @param messageId - The message ID
1220
- * @param queueId - The queue ID
1221
- */
1222
- complete(messageId: string, queueId: string): Promise<void>;
1223
- /**
1224
- * Release a message back to its queue.
1225
- * Used when processing fails or consumer wants to retry later.
1226
- *
1227
- * @param messageId - The message ID
1228
- * @param queueId - The queue ID
1229
- * @param queueKey - The Redis key for the queue
1230
- * @param queueItemsKey - The Redis key for the queue items hash
1231
- * @param tenantQueueIndexKey - The Redis key for the tenant queue index (Level 2)
1232
- * @param dispatchKey - The Redis key for the dispatch index (Level 1)
1233
- * @param tenantId - The tenant ID
1234
- * @param score - Optional score for the message (defaults to now)
1235
- */
1236
- release<TPayload = unknown>(messageId: string, queueId: string, queueKey: string, queueItemsKey: string, tenantQueueIndexKey: string, dispatchKey: string, tenantId: string, score?: number, updatedData?: string): Promise<void>;
1237
- /**
1238
- * Release multiple messages back to their queue in a single operation.
1239
- * Used when processing fails or consumer wants to retry later.
1240
- * All messages must belong to the same queue.
1241
- *
1242
- * @param messages - Array of messages to release (must all have same queueId)
1243
- * @param queueId - The queue ID
1244
- * @param queueKey - The Redis key for the queue
1245
- * @param queueItemsKey - The Redis key for the queue items hash
1246
- * @param tenantQueueIndexKey - The Redis key for the tenant queue index (Level 2)
1247
- * @param dispatchKey - The Redis key for the dispatch index (Level 1)
1248
- * @param tenantId - The tenant ID
1249
- * @param score - Optional score for the messages (defaults to now)
1250
- */
1251
- releaseBatch(messages: Array<{
1252
- messageId: string;
1253
- }>, queueId: string, queueKey: string, queueItemsKey: string, tenantQueueIndexKey: string, dispatchKey: string, tenantId: string, score?: number): Promise<void>;
1254
- /**
1255
- * Reclaim timed-out messages from a shard.
1256
- * Returns messages to their original queues.
1257
- *
1258
- * @param shardId - The shard to check
1259
- * @param getQueueKeys - Function to get queue keys for a queue ID
1260
- * @returns Array of reclaimed message info for concurrency release
1261
- */
1262
- reclaimTimedOut(shardId: number, getQueueKeys: (queueId: string) => {
1263
- queueKey: string;
1264
- queueItemsKey: string;
1265
- tenantQueueIndexKey: string;
1266
- dispatchKey: string;
1267
- tenantId: string;
1268
- }): Promise<ReclaimedMessageInfo[]>;
1269
- /**
1270
- * Get all in-flight messages for a shard.
1271
- */
1272
- getInflightMessages(shardId: number): Promise<Array<{
1273
- messageId: string;
1274
- queueId: string;
1275
- deadline: number;
1276
- }>>;
1277
- /**
1278
- * Get count of in-flight messages for a shard.
1279
- */
1280
- getInflightCount(shardId: number): Promise<number>;
1281
- /**
1282
- * Get total in-flight count across all shards.
1283
- */
1284
- getTotalInflightCount(): Promise<number>;
1285
- /**
1286
- * Close the Redis connection.
1287
- */
1288
- close(): Promise<void>;
1289
- }
1290
- declare module "@internal/redis" {
1291
- interface RedisCommander<Context> {
1292
- claimMessage(queueKey: string, queueItemsKey: string, inflightKey: string, inflightDataKey: string, queueId: string, consumerId: string, deadline: string): Promise<[string, string] | null>;
1293
- claimMessageBatch(queueKey: string, queueItemsKey: string, inflightKey: string, inflightDataKey: string, queueId: string, deadline: string, maxCount: string): Promise<string[]>;
1294
- releaseMessage(inflightKey: string, inflightDataKey: string, queueKey: string, queueItemsKey: string, tenantQueueIndexKey: string, dispatchKey: string, member: string, messageId: string, score: string, queueId: string, updatedData: string, tenantId: string): Promise<number>;
1295
- releaseMessageBatch(inflightKey: string, inflightDataKey: string, queueKey: string, queueItemsKey: string, tenantQueueIndexKey: string, dispatchKey: string, score: string, queueId: string, tenantId: string, ...membersAndMessageIds: string[]): Promise<number>;
1296
- heartbeatMessage(inflightKey: string, member: string, newDeadline: string): Promise<number>;
1297
- }
1298
- }
1299
-
1300
- interface WorkerQueueManagerOptions {
1301
- redis: RedisOptions;
1302
- keys: FairQueueKeyProducer;
1303
- logger?: {
1304
- debug: (message: string, context?: Record<string, unknown>) => void;
1305
- error: (message: string, context?: Record<string, unknown>) => void;
1306
- };
1307
- }
1308
- /**
1309
- * WorkerQueueManager handles the intermediate worker queue layer.
1310
- *
1311
- * This provides:
1312
- * - Low-latency message delivery via blocking pop (BLPOP)
1313
- * - Routing of messages to specific workers/consumers
1314
- * - Efficient waiting without polling
1315
- *
1316
- * Flow:
1317
- * 1. Master queue consumer claims message from message queue
1318
- * 2. Message key is pushed to worker queue
1319
- * 3. Worker queue consumer does blocking pop to receive message
1320
- */
1321
- declare class WorkerQueueManager {
1322
- #private;
1323
- private options;
1324
- private redis;
1325
- private keys;
1326
- private logger;
1327
- constructor(options: WorkerQueueManagerOptions);
1328
- /**
1329
- * Push a message key to a worker queue.
1330
- * Called after claiming a message from the message queue.
1331
- *
1332
- * @param workerQueueId - The worker queue identifier
1333
- * @param messageKey - The message key to push (typically "messageId:queueId")
1334
- */
1335
- push(workerQueueId: string, messageKey: string): Promise<void>;
1336
- /**
1337
- * Push multiple message keys to a worker queue.
1338
- *
1339
- * @param workerQueueId - The worker queue identifier
1340
- * @param messageKeys - The message keys to push
1341
- */
1342
- pushBatch(workerQueueId: string, messageKeys: string[]): Promise<void>;
1343
- /**
1344
- * Blocking pop from a worker queue.
1345
- * Waits until a message is available or timeout expires.
1346
- *
1347
- * @param workerQueueId - The worker queue identifier
1348
- * @param timeoutSeconds - Maximum time to wait (0 = wait forever)
1349
- * @param signal - Optional abort signal to cancel waiting
1350
- * @returns The message key, or null if timeout
1351
- */
1352
- blockingPop(workerQueueId: string, timeoutSeconds: number, signal?: AbortSignal): Promise<string | null>;
1353
- /**
1354
- * Non-blocking pop from a worker queue.
1355
- *
1356
- * @param workerQueueId - The worker queue identifier
1357
- * @returns The message key and queue length, or null if empty
1358
- */
1359
- pop(workerQueueId: string): Promise<{
1360
- messageKey: string;
1361
- queueLength: number;
1362
- } | null>;
1363
- /**
1364
- * Get the current length of a worker queue.
1365
- */
1366
- getLength(workerQueueId: string): Promise<number>;
1367
- /**
1368
- * Peek at all messages in a worker queue without removing them.
1369
- * Useful for debugging and tests.
1370
- */
1371
- peek(workerQueueId: string): Promise<string[]>;
1372
- /**
1373
- * Remove a specific message from the worker queue.
1374
- * Used when a message needs to be removed without processing.
1375
- *
1376
- * @param workerQueueId - The worker queue identifier
1377
- * @param messageKey - The message key to remove
1378
- * @returns Number of removed items
1379
- */
1380
- remove(workerQueueId: string, messageKey: string): Promise<number>;
1381
- /**
1382
- * Clear all messages from a worker queue.
1383
- */
1384
- clear(workerQueueId: string): Promise<void>;
1385
- /**
1386
- * Close the Redis connection.
1387
- */
1388
- close(): Promise<void>;
1389
- /**
1390
- * Register custom commands on an external Redis client.
1391
- * Use this when initializing FairQueue with worker queues.
1392
- */
1393
- registerCommands(redis: Redis): void;
1394
- }
1395
- declare module "@internal/redis" {
1396
- interface RedisCommander<Context> {
1397
- popWithLength(workerQueueKey: string): Promise<[string, string] | null>;
1398
- }
1399
- }
1400
-
1401
- /**
1402
- * Base class for scheduler implementations.
1403
- * Provides common utilities and default implementations.
1404
- */
1405
- declare abstract class BaseScheduler implements FairScheduler {
1406
- /**
1407
- * Select queues for processing from a master queue shard.
1408
- * Must be implemented by subclasses.
1409
- */
1410
- abstract selectQueues(masterQueueShard: string, consumerId: string, context: SchedulerContext): Promise<TenantQueues[]>;
1411
- /**
1412
- * Called after processing a message to update scheduler state.
1413
- * Default implementation does nothing.
1414
- */
1415
- recordProcessed(_tenantId: string, _queueId: string): Promise<void>;
1416
- /**
1417
- * Called after processing multiple messages to update scheduler state.
1418
- * Batch variant for efficiency - reduces Redis calls when processing multiple messages.
1419
- * Default implementation does nothing.
1420
- */
1421
- recordProcessedBatch(_tenantId: string, _queueId: string, _count: number): Promise<void>;
1422
- /**
1423
- * Initialize the scheduler.
1424
- * Default implementation does nothing.
1425
- */
1426
- initialize(): Promise<void>;
1427
- /**
1428
- * Cleanup scheduler resources.
1429
- * Default implementation does nothing.
1430
- */
1431
- close(): Promise<void>;
1432
- /**
1433
- * Helper to group queues by tenant.
1434
- */
1435
- protected groupQueuesByTenant(queues: Array<{
1436
- queueId: string;
1437
- tenantId: string;
1438
- }>): Map<string, string[]>;
1439
- /**
1440
- * Helper to convert grouped queues to TenantQueues array.
1441
- */
1442
- protected toTenantQueuesArray(grouped: Map<string, string[]>): TenantQueues[];
1443
- /**
1444
- * Helper to filter out tenants at capacity.
1445
- */
1446
- protected filterAtCapacity(tenants: TenantQueues[], context: SchedulerContext, groupName?: string): Promise<TenantQueues[]>;
1447
- }
1448
- /**
1449
- * Simple noop scheduler that returns empty results.
1450
- * Useful for testing or disabling scheduling.
1451
- */
1452
- declare class NoopScheduler extends BaseScheduler {
1453
- selectQueues(_masterQueueShard: string, _consumerId: string, _context: SchedulerContext): Promise<TenantQueues[]>;
1454
- }
1455
-
1456
- /**
1457
- * Deficit Round Robin (DRR) Scheduler.
1458
- *
1459
- * DRR ensures fair processing across tenants by:
1460
- * - Allocating a "quantum" of credits to each tenant per round
1461
- * - Accumulating unused credits as "deficit"
1462
- * - Processing from tenants with available deficit
1463
- * - Capping deficit to prevent starvation
1464
- *
1465
- * Key improvements over basic implementations:
1466
- * - Atomic deficit operations using Lua scripts
1467
- * - Efficient iteration through tenants
1468
- * - Automatic deficit cleanup for inactive tenants
1469
- */
1470
- declare class DRRScheduler extends BaseScheduler {
1471
- #private;
1472
- private config;
1473
- private redis;
1474
- private keys;
1475
- private quantum;
1476
- private maxDeficit;
1477
- private masterQueueLimit;
1478
- private logger;
1479
- constructor(config: DRRSchedulerConfig);
1480
- /**
1481
- * Select queues for processing using DRR algorithm.
1482
- *
1483
- * Algorithm:
1484
- * 1. Get all queues from the master shard
1485
- * 2. Group by tenant
1486
- * 3. Filter out tenants at concurrency capacity
1487
- * 4. Add quantum to each tenant's deficit (atomically)
1488
- * 5. Select queues from tenants with deficit >= 1
1489
- * 6. Order tenants by deficit (highest first for fairness)
1490
- */
1491
- selectQueues(masterQueueShard: string, consumerId: string, context: SchedulerContext): Promise<TenantQueues[]>;
1492
- /**
1493
- * Select queues using the two-level tenant dispatch index.
1494
- *
1495
- * Algorithm:
1496
- * 1. ZRANGEBYSCORE on dispatch index (gets only tenants with queues - much smaller)
1497
- * 2. Add quantum to each tenant's deficit (atomically)
1498
- * 3. Check capacity as safety net (dispatch should only have tenants with capacity)
1499
- * 4. Select tenants with deficit >= 1, sorted by deficit (highest first)
1500
- * 5. For each tenant, fetch their queues from Level 2 index
1501
- */
1502
- selectQueuesFromDispatch(dispatchShardKey: string, consumerId: string, context: DispatchSchedulerContext): Promise<TenantQueues[]>;
1503
- /**
1504
- * Record that a message was processed from a tenant.
1505
- * Decrements the tenant's deficit.
1506
- */
1507
- recordProcessed(tenantId: string, _queueId: string): Promise<void>;
1508
- /**
1509
- * Record that multiple messages were processed from a tenant.
1510
- * Decrements the tenant's deficit by count atomically.
1511
- */
1512
- recordProcessedBatch(tenantId: string, _queueId: string, count: number): Promise<void>;
1513
- close(): Promise<void>;
1514
- /**
1515
- * Get the current deficit for a tenant.
1516
- */
1517
- getDeficit(tenantId: string): Promise<number>;
1518
- /**
1519
- * Reset deficit for a tenant.
1520
- * Used when a tenant has no more active queues.
1521
- */
1522
- resetDeficit(tenantId: string): Promise<void>;
1523
- /**
1524
- * Get all tenant deficits.
1525
- */
1526
- getAllDeficits(): Promise<Map<string, number>>;
1287
+ resetDeficit(tenantId: string): Promise<void>;
1288
+ /**
1289
+ * Get all tenant deficits.
1290
+ */
1291
+ getAllDeficits(): Promise<Map<string, number>>;
1527
1292
  }
1528
1293
  declare module "@internal/redis" {
1529
1294
  interface RedisCommander<Context> {
@@ -1925,6 +1690,258 @@ declare class TenantDispatch {
1925
1690
  close(): Promise<void>;
1926
1691
  }
1927
1692
 
1693
+ interface VisibilityManagerOptions {
1694
+ redis: RedisOptions;
1695
+ keys: FairQueueKeyProducer;
1696
+ shardCount: number;
1697
+ defaultTimeoutMs: number;
1698
+ logger?: {
1699
+ debug: (message: string, context?: Record<string, unknown>) => void;
1700
+ error: (message: string, context?: Record<string, unknown>) => void;
1701
+ };
1702
+ }
1703
+ /**
1704
+ * VisibilityManager handles message visibility timeouts for safe message processing.
1705
+ *
1706
+ * Features:
1707
+ * - Claim messages with visibility timeout
1708
+ * - Heartbeat to extend timeout
1709
+ * - Automatic reclaim of timed-out messages
1710
+ * - Per-shard in-flight tracking
1711
+ *
1712
+ * Data structures:
1713
+ * - In-flight sorted set: score = deadline timestamp, member = "{messageId}:{queueId}"
1714
+ * - In-flight data hash: field = messageId, value = JSON message data
1715
+ */
1716
+ declare class VisibilityManager {
1717
+ #private;
1718
+ private options;
1719
+ private redis;
1720
+ private keys;
1721
+ private shardCount;
1722
+ private defaultTimeoutMs;
1723
+ private logger;
1724
+ constructor(options: VisibilityManagerOptions);
1725
+ /**
1726
+ * Claim a message for processing.
1727
+ * Moves the message from its queue to the in-flight set with a visibility timeout.
1728
+ *
1729
+ * @param queueId - The queue to claim from
1730
+ * @param queueKey - The Redis key for the queue sorted set
1731
+ * @param queueItemsKey - The Redis key for the queue items hash
1732
+ * @param consumerId - ID of the consumer claiming the message
1733
+ * @param timeoutMs - Visibility timeout in milliseconds
1734
+ * @returns Claim result with the message if successful
1735
+ */
1736
+ claim<TPayload = unknown>(queueId: string, queueKey: string, queueItemsKey: string, consumerId: string, timeoutMs?: number): Promise<ClaimResult<TPayload>>;
1737
+ /**
1738
+ * Claim multiple messages for processing (batch claim).
1739
+ * Moves up to maxCount messages from the queue to the in-flight set.
1740
+ *
1741
+ * @param queueId - The queue to claim from
1742
+ * @param queueKey - The Redis key for the queue sorted set
1743
+ * @param queueItemsKey - The Redis key for the queue items hash
1744
+ * @param consumerId - ID of the consumer claiming the messages
1745
+ * @param maxCount - Maximum number of messages to claim
1746
+ * @param timeoutMs - Visibility timeout in milliseconds
1747
+ * @returns Array of claimed messages
1748
+ */
1749
+ claimBatch<TPayload = unknown>(queueId: string, queueKey: string, queueItemsKey: string, consumerId: string, maxCount: number, timeoutMs?: number): Promise<Array<InFlightMessage<TPayload>>>;
1750
+ /**
1751
+ * Extend the visibility timeout for a message (heartbeat).
1752
+ *
1753
+ * @param messageId - The message ID
1754
+ * @param queueId - The queue ID
1755
+ * @param extendMs - Additional milliseconds to add to the deadline
1756
+ * @returns true if the heartbeat was successful
1757
+ */
1758
+ heartbeat(messageId: string, queueId: string, extendMs: number): Promise<boolean>;
1759
+ /**
1760
+ * Mark a message as successfully processed.
1761
+ * Removes the message from in-flight tracking.
1762
+ *
1763
+ * @param messageId - The message ID
1764
+ * @param queueId - The queue ID
1765
+ */
1766
+ complete(messageId: string, queueId: string): Promise<void>;
1767
+ /**
1768
+ * Release a message back to its queue.
1769
+ * Used when processing fails or consumer wants to retry later.
1770
+ *
1771
+ * @param messageId - The message ID
1772
+ * @param queueId - The queue ID
1773
+ * @param queueKey - The Redis key for the queue
1774
+ * @param queueItemsKey - The Redis key for the queue items hash
1775
+ * @param tenantQueueIndexKey - The Redis key for the tenant queue index (Level 2)
1776
+ * @param dispatchKey - The Redis key for the dispatch index (Level 1)
1777
+ * @param tenantId - The tenant ID
1778
+ * @param score - Optional score for the message (defaults to now)
1779
+ */
1780
+ release<_TPayload = unknown>(messageId: string, queueId: string, queueKey: string, queueItemsKey: string, tenantQueueIndexKey: string, dispatchKey: string, tenantId: string, score?: number, updatedData?: string): Promise<void>;
1781
+ /**
1782
+ * Release multiple messages back to their queue in a single operation.
1783
+ * Used when processing fails or consumer wants to retry later.
1784
+ * All messages must belong to the same queue.
1785
+ *
1786
+ * @param messages - Array of messages to release (must all have same queueId)
1787
+ * @param queueId - The queue ID
1788
+ * @param queueKey - The Redis key for the queue
1789
+ * @param queueItemsKey - The Redis key for the queue items hash
1790
+ * @param tenantQueueIndexKey - The Redis key for the tenant queue index (Level 2)
1791
+ * @param dispatchKey - The Redis key for the dispatch index (Level 1)
1792
+ * @param tenantId - The tenant ID
1793
+ * @param score - Optional score for the messages (defaults to now)
1794
+ */
1795
+ releaseBatch(messages: Array<{
1796
+ messageId: string;
1797
+ }>, queueId: string, queueKey: string, queueItemsKey: string, tenantQueueIndexKey: string, dispatchKey: string, tenantId: string, score?: number): Promise<void>;
1798
+ /**
1799
+ * Reclaim timed-out messages from a shard.
1800
+ * Returns messages to their original queues.
1801
+ *
1802
+ * @param shardId - The shard to check
1803
+ * @param getQueueKeys - Function to get queue keys for a queue ID
1804
+ * @returns Array of reclaimed message info for concurrency release
1805
+ */
1806
+ reclaimTimedOut(shardId: number, getQueueKeys: (queueId: string) => {
1807
+ queueKey: string;
1808
+ queueItemsKey: string;
1809
+ tenantQueueIndexKey: string;
1810
+ dispatchKey: string;
1811
+ tenantId: string;
1812
+ }): Promise<ReclaimedMessageInfo[]>;
1813
+ /**
1814
+ * Get all in-flight messages for a shard.
1815
+ */
1816
+ getInflightMessages(shardId: number): Promise<Array<{
1817
+ messageId: string;
1818
+ queueId: string;
1819
+ deadline: number;
1820
+ }>>;
1821
+ /**
1822
+ * Get count of in-flight messages for a shard.
1823
+ */
1824
+ getInflightCount(shardId: number): Promise<number>;
1825
+ /**
1826
+ * Get total in-flight count across all shards.
1827
+ */
1828
+ getTotalInflightCount(): Promise<number>;
1829
+ /**
1830
+ * Close the Redis connection.
1831
+ */
1832
+ close(): Promise<void>;
1833
+ }
1834
+ declare module "@internal/redis" {
1835
+ interface RedisCommander<Context> {
1836
+ claimMessage(queueKey: string, queueItemsKey: string, inflightKey: string, inflightDataKey: string, queueId: string, consumerId: string, deadline: string): Promise<[string, string] | null>;
1837
+ claimMessageBatch(queueKey: string, queueItemsKey: string, inflightKey: string, inflightDataKey: string, queueId: string, deadline: string, maxCount: string): Promise<string[]>;
1838
+ releaseMessage(inflightKey: string, inflightDataKey: string, queueKey: string, queueItemsKey: string, tenantQueueIndexKey: string, dispatchKey: string, member: string, messageId: string, score: string, queueId: string, updatedData: string, tenantId: string): Promise<number>;
1839
+ releaseMessageBatch(inflightKey: string, inflightDataKey: string, queueKey: string, queueItemsKey: string, tenantQueueIndexKey: string, dispatchKey: string, score: string, queueId: string, tenantId: string, ...membersAndMessageIds: string[]): Promise<number>;
1840
+ heartbeatMessage(inflightKey: string, member: string, newDeadline: string): Promise<number>;
1841
+ }
1842
+ }
1843
+
1844
+ interface WorkerQueueManagerOptions {
1845
+ redis: RedisOptions;
1846
+ keys: FairQueueKeyProducer;
1847
+ logger?: {
1848
+ debug: (message: string, context?: Record<string, unknown>) => void;
1849
+ error: (message: string, context?: Record<string, unknown>) => void;
1850
+ };
1851
+ }
1852
+ /**
1853
+ * WorkerQueueManager handles the intermediate worker queue layer.
1854
+ *
1855
+ * This provides:
1856
+ * - Low-latency message delivery via blocking pop (BLPOP)
1857
+ * - Routing of messages to specific workers/consumers
1858
+ * - Efficient waiting without polling
1859
+ *
1860
+ * Flow:
1861
+ * 1. Master queue consumer claims message from message queue
1862
+ * 2. Message key is pushed to worker queue
1863
+ * 3. Worker queue consumer does blocking pop to receive message
1864
+ */
1865
+ declare class WorkerQueueManager {
1866
+ #private;
1867
+ private options;
1868
+ private redis;
1869
+ private keys;
1870
+ private logger;
1871
+ constructor(options: WorkerQueueManagerOptions);
1872
+ /**
1873
+ * Push a message key to a worker queue.
1874
+ * Called after claiming a message from the message queue.
1875
+ *
1876
+ * @param workerQueueId - The worker queue identifier
1877
+ * @param messageKey - The message key to push (typically "messageId:queueId")
1878
+ */
1879
+ push(workerQueueId: string, messageKey: string): Promise<void>;
1880
+ /**
1881
+ * Push multiple message keys to a worker queue.
1882
+ *
1883
+ * @param workerQueueId - The worker queue identifier
1884
+ * @param messageKeys - The message keys to push
1885
+ */
1886
+ pushBatch(workerQueueId: string, messageKeys: string[]): Promise<void>;
1887
+ /**
1888
+ * Blocking pop from a worker queue.
1889
+ * Waits until a message is available or timeout expires.
1890
+ *
1891
+ * @param workerQueueId - The worker queue identifier
1892
+ * @param timeoutSeconds - Maximum time to wait (0 = wait forever)
1893
+ * @param signal - Optional abort signal to cancel waiting
1894
+ * @returns The message key, or null if timeout
1895
+ */
1896
+ blockingPop(workerQueueId: string, timeoutSeconds: number, signal?: AbortSignal): Promise<string | null>;
1897
+ /**
1898
+ * Non-blocking pop from a worker queue.
1899
+ *
1900
+ * @param workerQueueId - The worker queue identifier
1901
+ * @returns The message key and queue length, or null if empty
1902
+ */
1903
+ pop(workerQueueId: string): Promise<{
1904
+ messageKey: string;
1905
+ queueLength: number;
1906
+ } | null>;
1907
+ /**
1908
+ * Get the current length of a worker queue.
1909
+ */
1910
+ getLength(workerQueueId: string): Promise<number>;
1911
+ /**
1912
+ * Peek at all messages in a worker queue without removing them.
1913
+ * Useful for debugging and tests.
1914
+ */
1915
+ peek(workerQueueId: string): Promise<string[]>;
1916
+ /**
1917
+ * Remove a specific message from the worker queue.
1918
+ * Used when a message needs to be removed without processing.
1919
+ *
1920
+ * @param workerQueueId - The worker queue identifier
1921
+ * @param messageKey - The message key to remove
1922
+ * @returns Number of removed items
1923
+ */
1924
+ remove(workerQueueId: string, messageKey: string): Promise<number>;
1925
+ /**
1926
+ * Clear all messages from a worker queue.
1927
+ */
1928
+ clear(workerQueueId: string): Promise<void>;
1929
+ /**
1930
+ * Close the Redis connection.
1931
+ */
1932
+ close(): Promise<void>;
1933
+ /**
1934
+ * Register custom commands on an external Redis client.
1935
+ * Use this when initializing FairQueue with worker queues.
1936
+ */
1937
+ registerCommands(redis: Redis): void;
1938
+ }
1939
+ declare module "@internal/redis" {
1940
+ interface RedisCommander<Context> {
1941
+ popWithLength(workerQueueKey: string): Promise<[string, string] | null>;
1942
+ }
1943
+ }
1944
+
1928
1945
  /**
1929
1946
  * FairQueue is the main orchestrator for fair queue message routing.
1930
1947
  *