@@ -1182,4 +1182,104 @@ describe("FairQueue", () => {
11821182 }
11831183 ) ;
11841184 } ) ;
1185+
1186+ describe ( "concurrency block should not trigger cooloff" , ( ) => {
1187+ redisTest (
1188+ "should not enter cooloff when queue hits concurrency limit" ,
1189+ { timeout : 15000 } ,
1190+ async ( { redisOptions } ) => {
1191+ const processed : string [ ] = [ ] ;
1192+ keys = new DefaultFairQueueKeyProducer ( { prefix : "test" } ) ;
1193+
1194+ const scheduler = new DRRScheduler ( {
1195+ redis : redisOptions ,
1196+ keys,
1197+ quantum : 10 ,
1198+ maxDeficit : 100 ,
1199+ } ) ;
1200+
1201+ const queue = new TestFairQueueHelper ( redisOptions , keys , {
1202+ scheduler,
1203+ payloadSchema : TestPayloadSchema ,
1204+ shardCount : 1 ,
1205+ consumerCount : 1 ,
1206+ consumerIntervalMs : 20 ,
1207+ visibilityTimeoutMs : 5000 ,
1208+ cooloff : {
1209+ periodMs : 5000 , // Long cooloff - if triggered, messages would stall
1210+ threshold : 1 , // Enter cooloff after just 1 increment
1211+ } ,
1212+ concurrencyGroups : [
1213+ {
1214+ name : "tenant" ,
1215+ extractGroupId : ( q ) => q . tenantId ,
1216+ getLimit : async ( ) => 1 , // Only 1 concurrent per tenant
1217+ defaultLimit : 1 ,
1218+ } ,
1219+ ] ,
1220+ startConsumers : false ,
1221+ } ) ;
1222+
1223+ // Hold first message to keep concurrency slot occupied
1224+ let releaseFirst : ( ( ) => void ) | undefined ;
1225+ const firstBlocking = new Promise < void > ( ( resolve ) => {
1226+ releaseFirst = resolve ;
1227+ } ) ;
1228+ let firstStarted = false ;
1229+
1230+ queue . onMessage ( async ( ctx ) => {
1231+ if ( ctx . message . payload . value === "msg-0" ) {
1232+ firstStarted = true ;
1233+ // Block this message to saturate concurrency
1234+ await firstBlocking ;
1235+ }
1236+ processed . push ( ctx . message . payload . value ) ;
1237+ await ctx . complete ( ) ;
1238+ } ) ;
1239+
1240+ // Enqueue 3 messages to same tenant
1241+ for ( let i = 0 ; i < 3 ; i ++ ) {
1242+ await queue . enqueue ( {
1243+ queueId : "tenant:t1:queue:q1" ,
1244+ tenantId : "t1" ,
1245+ payload : { value : `msg-${ i } ` } ,
1246+ } ) ;
1247+ }
1248+
1249+ queue . start ( ) ;
1250+
1251+ // Wait for first message to start processing (blocking the concurrency slot)
1252+ await vi . waitFor (
1253+ ( ) => {
1254+ expect ( firstStarted ) . toBe ( true ) ;
1255+ } ,
1256+ { timeout : 5000 }
1257+ ) ;
1258+
1259+ // Release the first message so others can proceed
1260+ releaseFirst ! ( ) ;
1261+
1262+ // All 3 messages should process within a reasonable time.
1263+ // If cooloff was incorrectly triggered, this would take 5+ seconds.
1264+ const startTime = Date . now ( ) ;
1265+ await vi . waitFor (
1266+ ( ) => {
1267+ expect ( processed ) . toHaveLength ( 3 ) ;
1268+ } ,
1269+ { timeout : 5000 }
1270+ ) ;
1271+ const elapsed = Date . now ( ) - startTime ;
1272+
1273+ // Should complete well under the 5s cooloff period
1274+ expect ( elapsed ) . toBeLessThan ( 3000 ) ;
1275+
1276+ // Cooloff states should be empty (no spurious cooloffs)
1277+ const cacheSizes = queue . fairQueue . getCacheSizes ( ) ;
1278+ expect ( cacheSizes . cooloffStatesSize ) . toBe ( 0 ) ;
1279+
1280+ await queue . close ( ) ;
1281+ }
1282+ ) ;
1283+ } ) ;
1284+
11851285} ) ;
0 commit comments