8
8
trace ,
9
9
Tracer ,
10
10
} from "@opentelemetry/api" ;
11
+ import { type RedisOptions } from "@internal/redis" ;
11
12
import {
12
13
SEMATTRS_MESSAGE_ID ,
13
14
SEMATTRS_MESSAGING_SYSTEM ,
@@ -44,6 +45,9 @@ import {
44
45
} from "./constants.server" ;
45
46
import { setInterval } from "node:timers/promises" ;
46
47
import { tryCatch } from "@trigger.dev/core/utils" ;
48
+ import { Worker , type WorkerConcurrencyOptions } from "@trigger.dev/redis-worker" ;
49
+ import z from "zod" ;
50
+ import { Logger } from "@trigger.dev/core/logger" ;
47
51
48
52
const KEY_PREFIX = "marqs:" ;
49
53
@@ -74,6 +78,25 @@ export type MarQSOptions = {
74
78
subscriber ?: MessageQueueSubscriber ;
75
79
sharedWorkerQueueConsumerIntervalMs ?: number ;
76
80
sharedWorkerQueueMaxMessageCount ?: number ;
81
+ eagerDequeuingEnabled ?: boolean ;
82
+ workerOptions : {
83
+ pollIntervalMs ?: number ;
84
+ immediatePollIntervalMs ?: number ;
85
+ shutdownTimeoutMs ?: number ;
86
+ concurrency ?: WorkerConcurrencyOptions ;
87
+ enabled ?: boolean ;
88
+ redisOptions : RedisOptions ;
89
+ } ;
90
+ } ;
91
+
92
+ const workerCatalog = {
93
+ processQueueForWorkerQueue : {
94
+ schema : z . object ( {
95
+ queueKey : z . string ( ) ,
96
+ parentQueueKey : z . string ( ) ,
97
+ } ) ,
98
+ visibilityTimeoutMs : 30_000 ,
99
+ } ,
77
100
} ;
78
101
79
102
/**
@@ -83,6 +106,7 @@ export class MarQS {
83
106
private redis : Redis ;
84
107
public keys : MarQSKeyProducer ;
85
108
#rebalanceWorkers: Array < AsyncWorker > = [ ] ;
109
+ private worker : Worker < typeof workerCatalog > ;
86
110
87
111
constructor ( private readonly options : MarQSOptions ) {
88
112
this . redis = options . redis ;
@@ -91,6 +115,26 @@ export class MarQS {
91
115
92
116
this . #startRebalanceWorkers( ) ;
93
117
this . #registerCommands( ) ;
118
+
119
+ this . worker = new Worker ( {
120
+ name : "marqs-worker" ,
121
+ redisOptions : options . workerOptions . redisOptions ,
122
+ catalog : workerCatalog ,
123
+ concurrency : options . workerOptions ?. concurrency ,
124
+ pollIntervalMs : options . workerOptions ?. pollIntervalMs ?? 1000 ,
125
+ immediatePollIntervalMs : options . workerOptions ?. immediatePollIntervalMs ?? 100 ,
126
+ shutdownTimeoutMs : options . workerOptions ?. shutdownTimeoutMs ?? 10_000 ,
127
+ logger : new Logger ( "MarQSWorker" , "info" ) ,
128
+ jobs : {
129
+ processQueueForWorkerQueue : async ( job ) => {
130
+ await this . #processQueueForWorkerQueue( job . payload . queueKey , job . payload . parentQueueKey ) ;
131
+ } ,
132
+ } ,
133
+ } ) ;
134
+
135
+ if ( options . workerOptions ?. enabled ) {
136
+ this . worker . start ( ) ;
137
+ }
94
138
}
95
139
96
140
get name ( ) {
@@ -280,6 +324,21 @@ export class MarQS {
280
324
span . setAttribute ( "reserve_recursive_queue" , reserve . recursiveQueue ) ;
281
325
}
282
326
327
+ if ( env . type !== "DEVELOPMENT" && this . options . eagerDequeuingEnabled ) {
328
+ // This will move the message to the worker queue so it can be dequeued
329
+ await this . worker . enqueueOnce ( {
330
+ id : messageQueue , // dedupe by environment, queue, and concurrency key
331
+ job : "processQueueForWorkerQueue" ,
332
+ payload : {
333
+ queueKey : messageQueue ,
334
+ parentQueueKey : parentQueue ,
335
+ } ,
336
+ // Add a small delay to dedupe messages so at most one of these will processed,
337
+ // every 500ms per queue, concurrency key, and environment
338
+ availableAt : new Date ( Date . now ( ) + 500 ) , // 500ms from now
339
+ } ) ;
340
+ }
341
+
283
342
const result = await this . #callEnqueueMessage( messagePayload , reserve ) ;
284
343
285
344
if ( result ) {
@@ -870,6 +929,64 @@ export class MarQS {
870
929
) ;
871
930
}
872
931
932
+ async #processQueueForWorkerQueue( queueKey : string , parentQueueKey : string ) {
933
+ return this . #trace( "processQueueForWorkerQueue" , async ( span ) => {
934
+ span . setAttributes ( {
935
+ [ SemanticAttributes . QUEUE ] : queueKey ,
936
+ [ SemanticAttributes . PARENT_QUEUE ] : parentQueueKey ,
937
+ } ) ;
938
+
939
+ const maxCount = this . options . sharedWorkerQueueMaxMessageCount ?? 10 ;
940
+
941
+ const dequeuedMessages = await this . #callDequeueMessages( {
942
+ messageQueue : queueKey ,
943
+ parentQueue : parentQueueKey ,
944
+ maxCount,
945
+ } ) ;
946
+
947
+ if ( ! dequeuedMessages || dequeuedMessages . length === 0 ) {
948
+ return ;
949
+ }
950
+
951
+ await this . #trace(
952
+ "addToWorkerQueue" ,
953
+ async ( addToWorkerQueueSpan ) => {
954
+ const workerQueueKey = this . keys . sharedWorkerQueueKey ( ) ;
955
+
956
+ addToWorkerQueueSpan . setAttributes ( {
957
+ message_count : dequeuedMessages . length ,
958
+ [ SemanticAttributes . PARENT_QUEUE ] : workerQueueKey ,
959
+ } ) ;
960
+
961
+ await this . redis . rpush (
962
+ workerQueueKey ,
963
+ ...dequeuedMessages . map ( ( message ) => message . messageId )
964
+ ) ;
965
+ } ,
966
+ {
967
+ kind : SpanKind . INTERNAL ,
968
+ attributes : {
969
+ [ SEMATTRS_MESSAGING_OPERATION ] : "receive" ,
970
+ [ SEMATTRS_MESSAGING_SYSTEM ] : "marqs" ,
971
+ } ,
972
+ }
973
+ ) ;
974
+
975
+ // If we dequeued the max count, we need to enqueue another job to dequeue the next batch
976
+ if ( dequeuedMessages . length === maxCount ) {
977
+ await this . worker . enqueueOnce ( {
978
+ id : queueKey ,
979
+ job : "processQueueForWorkerQueue" ,
980
+ payload : {
981
+ queueKey,
982
+ parentQueueKey,
983
+ } ,
984
+ availableAt : new Date ( Date . now ( ) + 500 ) , // 500ms from now
985
+ } ) ;
986
+ }
987
+ } ) ;
988
+ }
989
+
873
990
public async acknowledgeMessage ( messageId : string , reason : string = "unknown" ) {
874
991
return this . #trace(
875
992
"acknowledgeMessage" ,
@@ -901,6 +1018,20 @@ export class MarQS {
901
1018
messageId,
902
1019
} ) ;
903
1020
1021
+ const sharedQueueKey = this . keys . sharedQueueKey ( ) ;
1022
+
1023
+ if ( this . options . eagerDequeuingEnabled && message . parentQueue === sharedQueueKey ) {
1024
+ await this . worker . enqueueOnce ( {
1025
+ id : message . queue ,
1026
+ job : "processQueueForWorkerQueue" ,
1027
+ payload : {
1028
+ queueKey : message . queue ,
1029
+ parentQueueKey : message . parentQueue ,
1030
+ } ,
1031
+ availableAt : new Date ( Date . now ( ) + 500 ) , // 500ms from now
1032
+ } ) ;
1033
+ }
1034
+
904
1035
await this . options . subscriber ?. messageAcked ( message ) ;
905
1036
} ,
906
1037
{
@@ -2482,5 +2613,26 @@ function getMarQSClient() {
2482
2613
subscriber : concurrencyTracker ,
2483
2614
sharedWorkerQueueConsumerIntervalMs : env . MARQS_SHARED_WORKER_QUEUE_CONSUMER_INTERVAL_MS ,
2484
2615
sharedWorkerQueueMaxMessageCount : env . MARQS_SHARED_WORKER_QUEUE_MAX_MESSAGE_COUNT ,
2616
+ eagerDequeuingEnabled : env . MARQS_SHARED_WORKER_QUEUE_EAGER_DEQUEUE_ENABLED === "1" ,
2617
+ workerOptions : {
2618
+ enabled : env . MARQS_WORKER_ENABLED === "1" ,
2619
+ pollIntervalMs : env . MARQS_WORKER_POLL_INTERVAL_MS ,
2620
+ immediatePollIntervalMs : env . MARQS_WORKER_IMMEDIATE_POLL_INTERVAL_MS ,
2621
+ shutdownTimeoutMs : env . MARQS_WORKER_SHUTDOWN_TIMEOUT_MS ,
2622
+ concurrency : {
2623
+ workers : env . MARQS_WORKER_COUNT ,
2624
+ tasksPerWorker : env . MARQS_WORKER_CONCURRENCY_TASKS_PER_WORKER ,
2625
+ limit : env . MARQS_WORKER_CONCURRENCY_LIMIT ,
2626
+ } ,
2627
+ redisOptions : {
2628
+ keyPrefix : KEY_PREFIX ,
2629
+ port : env . REDIS_PORT ?? undefined ,
2630
+ host : env . REDIS_HOST ?? undefined ,
2631
+ username : env . REDIS_USERNAME ?? undefined ,
2632
+ password : env . REDIS_PASSWORD ?? undefined ,
2633
+ enableAutoPipelining : true ,
2634
+ ...( env . REDIS_TLS_DISABLED === "true" ? { } : { tls : { } } ) ,
2635
+ } ,
2636
+ } ,
2485
2637
} ) ;
2486
2638
}
0 commit comments