1
+ import { type RedisOptions } from "@internal/redis" ;
1
2
import {
2
3
context ,
3
4
propagation ,
@@ -8,21 +9,32 @@ import {
8
9
trace ,
9
10
Tracer ,
10
11
} from "@opentelemetry/api" ;
11
- import { type RedisOptions } from "@internal/redis" ;
12
12
import {
13
13
SEMATTRS_MESSAGE_ID ,
14
- SEMATTRS_MESSAGING_SYSTEM ,
15
14
SEMATTRS_MESSAGING_OPERATION ,
15
+ SEMATTRS_MESSAGING_SYSTEM ,
16
16
} from "@opentelemetry/semantic-conventions" ;
17
+ import { Logger } from "@trigger.dev/core/logger" ;
18
+ import { tryCatch } from "@trigger.dev/core/utils" ;
17
19
import { flattenAttributes } from "@trigger.dev/core/v3" ;
20
+ import { Worker , type WorkerConcurrencyOptions } from "@trigger.dev/redis-worker" ;
18
21
import Redis , { type Callback , type Result } from "ioredis" ;
22
+ import { setInterval as setIntervalAsync } from "node:timers/promises" ;
23
+ import z from "zod" ;
19
24
import { env } from "~/env.server" ;
20
25
import { AuthenticatedEnvironment } from "~/services/apiAuth.server" ;
21
26
import { logger } from "~/services/logger.server" ;
22
27
import { singleton } from "~/utils/singleton" ;
28
+ import { legacyRunEngineWorker } from "../legacyRunEngineWorker.server" ;
23
29
import { concurrencyTracker } from "../services/taskRunConcurrencyTracker.server" ;
24
30
import { attributesFromAuthenticatedEnv , tracer } from "../tracer.server" ;
25
31
import { AsyncWorker } from "./asyncWorker.server" ;
32
+ import {
33
+ MARQS_DELAYED_REQUEUE_THRESHOLD_IN_MS ,
34
+ MARQS_RESUME_PRIORITY_TIMESTAMP_OFFSET ,
35
+ MARQS_RETRY_PRIORITY_TIMESTAMP_OFFSET ,
36
+ MARQS_SCHEDULED_REQUEUE_AVAILABLE_AT_THRESHOLD_IN_MS ,
37
+ } from "./constants.server" ;
26
38
import { FairDequeuingStrategy } from "./fairDequeuingStrategy.server" ;
27
39
import { MarQSShortKeyProducer } from "./marqsKeyProducer" ;
28
40
import {
@@ -36,18 +48,6 @@ import {
36
48
VisibilityTimeoutStrategy ,
37
49
} from "./types" ;
38
50
import { V3LegacyRunEngineWorkerVisibilityTimeout } from "./v3VisibilityTimeout.server" ;
39
- import { legacyRunEngineWorker } from "../legacyRunEngineWorker.server" ;
40
- import {
41
- MARQS_DELAYED_REQUEUE_THRESHOLD_IN_MS ,
42
- MARQS_RESUME_PRIORITY_TIMESTAMP_OFFSET ,
43
- MARQS_RETRY_PRIORITY_TIMESTAMP_OFFSET ,
44
- MARQS_SCHEDULED_REQUEUE_AVAILABLE_AT_THRESHOLD_IN_MS ,
45
- } from "./constants.server" ;
46
- import { setInterval } from "node:timers/promises" ;
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" ;
51
51
52
52
const KEY_PREFIX = "marqs:" ;
53
53
@@ -78,6 +78,8 @@ export type MarQSOptions = {
78
78
subscriber ?: MessageQueueSubscriber ;
79
79
sharedWorkerQueueConsumerIntervalMs ?: number ;
80
80
sharedWorkerQueueMaxMessageCount ?: number ;
81
+ sharedWorkerQueueCooloffPeriodMs ?: number ;
82
+ sharedWorkerQueueCooloffCountThreshold ?: number ;
81
83
eagerDequeuingEnabled ?: boolean ;
82
84
workerOptions : {
83
85
pollIntervalMs ?: number ;
@@ -107,6 +109,9 @@ export class MarQS {
107
109
public keys : MarQSKeyProducer ;
108
110
#rebalanceWorkers: Array < AsyncWorker > = [ ] ;
109
111
private worker : Worker < typeof workerCatalog > ;
112
+ private queueDequeueCooloffPeriod : Map < string , number > = new Map ( ) ;
113
+ private queueDequeueCooloffCounts : Map < string , number > = new Map ( ) ;
114
+ private clearCooloffPeriodInterval : NodeJS . Timeout ;
110
115
111
116
constructor ( private readonly options : MarQSOptions ) {
112
117
this . redis = options . redis ;
@@ -116,6 +121,12 @@ export class MarQS {
116
121
this . #startRebalanceWorkers( ) ;
117
122
this . #registerCommands( ) ;
118
123
124
+ // This will prevent these cooloff maps from growing indefinitely
125
+ this . clearCooloffPeriodInterval = setInterval ( ( ) => {
126
+ this . queueDequeueCooloffCounts . clear ( ) ;
127
+ this . queueDequeueCooloffPeriod . clear ( ) ;
128
+ } , 60_000 * 10 ) ; // 10 minutes
129
+
119
130
this . worker = new Worker ( {
120
131
name : "marqs-worker" ,
121
132
redisOptions : options . workerOptions . redisOptions ,
@@ -135,6 +146,19 @@ export class MarQS {
135
146
if ( options . workerOptions ?. enabled ) {
136
147
this . worker . start ( ) ;
137
148
}
149
+
150
+ this . #setupShutdownHandlers( ) ;
151
+ }
152
+
153
+ #setupShutdownHandlers( ) {
154
+ process . on ( "SIGTERM" , ( ) => this . shutdown ( "SIGTERM" ) ) ;
155
+ process . on ( "SIGINT" , ( ) => this . shutdown ( "SIGINT" ) ) ;
156
+ }
157
+
158
+ async shutdown ( signal : NodeJS . Signals ) {
159
+ console . log ( "👇 Shutting down marqs" , this . name , signal ) ;
160
+ clearInterval ( this . clearCooloffPeriodInterval ) ;
161
+ this . #rebalanceWorkers. forEach ( ( worker ) => worker . stop ( ) ) ;
138
162
}
139
163
140
164
get name ( ) {
@@ -737,7 +761,7 @@ export class MarQS {
737
761
let processedCount = 0 ;
738
762
739
763
try {
740
- for await ( const _ of setInterval (
764
+ for await ( const _ of setIntervalAsync (
741
765
this . options . sharedWorkerQueueConsumerIntervalMs ?? 500 ,
742
766
null ,
743
767
{
@@ -821,6 +845,7 @@ export class MarQS {
821
845
let attemptedEnvs = 0 ;
822
846
let attemptedQueues = 0 ;
823
847
let messageCount = 0 ;
848
+ let coolOffPeriodCount = 0 ;
824
849
825
850
// Try each queue in order, attempt to dequeue a message from each queue, keep going until we've tried all the queues
826
851
for ( const env of envQueues ) {
@@ -829,6 +854,20 @@ export class MarQS {
829
854
for ( const messageQueue of env . queues ) {
830
855
attemptedQueues ++ ;
831
856
857
+ const cooloffPeriod = this . queueDequeueCooloffPeriod . get ( messageQueue ) ;
858
+
859
+ // If the queue is in a cooloff period, skip attempting to dequeue from it
860
+ if ( cooloffPeriod ) {
861
+ // If the cooloff period is still active, skip attempting to dequeue from it
862
+ if ( cooloffPeriod > Date . now ( ) ) {
863
+ coolOffPeriodCount ++ ;
864
+ continue ;
865
+ } else {
866
+ // If the cooloff period is over, delete the cooloff period and attempt to dequeue from the queue
867
+ this . queueDequeueCooloffPeriod . delete ( messageQueue ) ;
868
+ }
869
+ }
870
+
832
871
await this . #trace(
833
872
"attemptDequeue" ,
834
873
async ( attemptDequeueSpan ) => {
@@ -862,10 +901,32 @@ export class MarQS {
862
901
) ;
863
902
864
903
if ( ! messages || messages . length === 0 ) {
904
+ const cooloffCount = this . queueDequeueCooloffCounts . get ( messageQueue ) ?? 0 ;
905
+
906
+ const cooloffCountThreshold = Math . max (
907
+ 10 ,
908
+ this . options . sharedWorkerQueueCooloffCountThreshold ?? 10
909
+ ) ; // minimum of 10
910
+
911
+ if ( cooloffCount >= cooloffCountThreshold ) {
912
+ // If no messages were dequeued, set a cooloff period for the queue
913
+ // This is to prevent the queue from being dequeued too frequently
914
+ // and to give other queues a chance to dequeue messages more frequently
915
+ this . queueDequeueCooloffPeriod . set (
916
+ messageQueue ,
917
+ Date . now ( ) + ( this . options . sharedWorkerQueueCooloffPeriodMs ?? 10_000 ) // defaults to 10 seconds
918
+ ) ;
919
+ this . queueDequeueCooloffCounts . delete ( messageQueue ) ;
920
+ } else {
921
+ this . queueDequeueCooloffCounts . set ( messageQueue , cooloffCount + 1 ) ;
922
+ }
923
+
865
924
attemptDequeueSpan . setAttribute ( "message_count" , 0 ) ;
866
925
return null ; // Try next queue if no message was dequeued
867
926
}
868
927
928
+ this . queueDequeueCooloffCounts . delete ( messageQueue ) ;
929
+
869
930
messageCount += messages . length ;
870
931
871
932
attemptDequeueSpan . setAttribute ( "message_count" , messages . length ) ;
@@ -916,6 +977,7 @@ export class MarQS {
916
977
span . setAttribute ( "attempted_queues" , attemptedQueues ) ;
917
978
span . setAttribute ( "attempted_envs" , attemptedEnvs ) ;
918
979
span . setAttribute ( "message_count" , messageCount ) ;
980
+ span . setAttribute ( "cooloff_period_count" , coolOffPeriodCount ) ;
919
981
920
982
return ;
921
983
} ,
@@ -2614,6 +2676,8 @@ function getMarQSClient() {
2614
2676
sharedWorkerQueueConsumerIntervalMs : env . MARQS_SHARED_WORKER_QUEUE_CONSUMER_INTERVAL_MS ,
2615
2677
sharedWorkerQueueMaxMessageCount : env . MARQS_SHARED_WORKER_QUEUE_MAX_MESSAGE_COUNT ,
2616
2678
eagerDequeuingEnabled : env . MARQS_SHARED_WORKER_QUEUE_EAGER_DEQUEUE_ENABLED === "1" ,
2679
+ sharedWorkerQueueCooloffCountThreshold : env . MARQS_SHARED_WORKER_QUEUE_COOLOFF_COUNT_THRESHOLD ,
2680
+ sharedWorkerQueueCooloffPeriodMs : env . MARQS_SHARED_WORKER_QUEUE_COOLOFF_PERIOD_MS ,
2617
2681
workerOptions : {
2618
2682
enabled : env . MARQS_WORKER_ENABLED === "1" ,
2619
2683
pollIntervalMs : env . MARQS_WORKER_POLL_INTERVAL_MS ,
0 commit comments