@@ -2,7 +2,7 @@ import { describe, expect } from "vitest";
22import { redisTest } from "@internal/testcontainers" ;
33import { createRedisClient } from "@internal/redis" ;
44import { VisibilityManager , DefaultFairQueueKeyProducer } from "../index.js" ;
5- import type { FairQueueKeyProducer } from "../types.js" ;
5+ import type { FairQueueKeyProducer , ReclaimedMessageInfo } from "../types.js" ;
66
77describe ( "VisibilityManager" , ( ) => {
88 let keys : FairQueueKeyProducer ;
@@ -597,5 +597,184 @@ describe("VisibilityManager", () => {
597597 }
598598 ) ;
599599 } ) ;
600+
601+ describe ( "reclaimTimedOut" , ( ) => {
602+ redisTest (
603+ "should return reclaimed message info with tenantId for concurrency release" ,
604+ { timeout : 10000 } ,
605+ async ( { redisOptions } ) => {
606+ keys = new DefaultFairQueueKeyProducer ( { prefix : "test" } ) ;
607+
608+ const manager = new VisibilityManager ( {
609+ redis : redisOptions ,
610+ keys,
611+ shardCount : 1 ,
612+ defaultTimeoutMs : 100 , // Very short timeout
613+ } ) ;
614+
615+ const redis = createRedisClient ( redisOptions ) ;
616+ const queueId = "tenant:t1:queue:reclaim-test" ;
617+ const queueKey = keys . queueKey ( queueId ) ;
618+ const queueItemsKey = keys . queueItemsKey ( queueId ) ;
619+ const masterQueueKey = keys . masterQueueKey ( 0 ) ;
620+
621+ // Add and claim a message
622+ const messageId = "reclaim-msg" ;
623+ const storedMessage = {
624+ id : messageId ,
625+ queueId,
626+ tenantId : "t1" ,
627+ payload : { id : 1 , value : "test" } ,
628+ timestamp : Date . now ( ) - 1000 ,
629+ attempt : 1 ,
630+ metadata : { orgId : "org-123" } ,
631+ } ;
632+
633+ await redis . zadd ( queueKey , storedMessage . timestamp , messageId ) ;
634+ await redis . hset ( queueItemsKey , messageId , JSON . stringify ( storedMessage ) ) ;
635+
636+ // Claim with very short timeout
637+ const claimResult = await manager . claim ( queueId , queueKey , queueItemsKey , "consumer-1" , 100 ) ;
638+ expect ( claimResult . claimed ) . toBe ( true ) ;
639+
640+ // Wait for timeout to expire
641+ await new Promise ( ( resolve ) => setTimeout ( resolve , 150 ) ) ;
642+
643+ // Reclaim should return the message info
644+ const reclaimedMessages = await manager . reclaimTimedOut ( 0 , ( qId ) => ( {
645+ queueKey : keys . queueKey ( qId ) ,
646+ queueItemsKey : keys . queueItemsKey ( qId ) ,
647+ masterQueueKey,
648+ } ) ) ;
649+
650+ expect ( reclaimedMessages ) . toHaveLength ( 1 ) ;
651+ expect ( reclaimedMessages [ 0 ] ) . toEqual ( {
652+ messageId,
653+ queueId,
654+ tenantId : "t1" ,
655+ metadata : { orgId : "org-123" } ,
656+ } ) ;
657+
658+ // Verify message is back in queue
659+ const queueCount = await redis . zcard ( queueKey ) ;
660+ expect ( queueCount ) . toBe ( 1 ) ;
661+
662+ // Verify message is no longer in-flight
663+ const inflightCount = await manager . getTotalInflightCount ( ) ;
664+ expect ( inflightCount ) . toBe ( 0 ) ;
665+
666+ await manager . close ( ) ;
667+ await redis . quit ( ) ;
668+ }
669+ ) ;
670+
671+ redisTest (
672+ "should return empty array when no messages have timed out" ,
673+ { timeout : 10000 } ,
674+ async ( { redisOptions } ) => {
675+ keys = new DefaultFairQueueKeyProducer ( { prefix : "test" } ) ;
676+
677+ const manager = new VisibilityManager ( {
678+ redis : redisOptions ,
679+ keys,
680+ shardCount : 1 ,
681+ defaultTimeoutMs : 60000 , // Long timeout
682+ } ) ;
683+
684+ const redis = createRedisClient ( redisOptions ) ;
685+ const queueId = "tenant:t1:queue:no-timeout" ;
686+ const queueKey = keys . queueKey ( queueId ) ;
687+ const queueItemsKey = keys . queueItemsKey ( queueId ) ;
688+ const masterQueueKey = keys . masterQueueKey ( 0 ) ;
689+
690+ // Add and claim a message with long timeout
691+ const messageId = "long-timeout-msg" ;
692+ const storedMessage = {
693+ id : messageId ,
694+ queueId,
695+ tenantId : "t1" ,
696+ payload : { id : 1 } ,
697+ timestamp : Date . now ( ) - 1000 ,
698+ attempt : 1 ,
699+ } ;
700+
701+ await redis . zadd ( queueKey , storedMessage . timestamp , messageId ) ;
702+ await redis . hset ( queueItemsKey , messageId , JSON . stringify ( storedMessage ) ) ;
703+
704+ await manager . claim ( queueId , queueKey , queueItemsKey , "consumer-1" ) ;
705+
706+ // Reclaim should return empty array (message hasn't timed out)
707+ const reclaimedMessages = await manager . reclaimTimedOut ( 0 , ( qId ) => ( {
708+ queueKey : keys . queueKey ( qId ) ,
709+ queueItemsKey : keys . queueItemsKey ( qId ) ,
710+ masterQueueKey,
711+ } ) ) ;
712+
713+ expect ( reclaimedMessages ) . toHaveLength ( 0 ) ;
714+
715+ await manager . close ( ) ;
716+ await redis . quit ( ) ;
717+ }
718+ ) ;
719+
720+ redisTest (
721+ "should reclaim multiple timed-out messages and return all their info" ,
722+ { timeout : 10000 } ,
723+ async ( { redisOptions } ) => {
724+ keys = new DefaultFairQueueKeyProducer ( { prefix : "test" } ) ;
725+
726+ const manager = new VisibilityManager ( {
727+ redis : redisOptions ,
728+ keys,
729+ shardCount : 1 ,
730+ defaultTimeoutMs : 100 ,
731+ } ) ;
732+
733+ const redis = createRedisClient ( redisOptions ) ;
734+ const masterQueueKey = keys . masterQueueKey ( 0 ) ;
735+
736+ // Add and claim messages for two different tenants
737+ for ( const tenant of [ "t1" , "t2" ] ) {
738+ const queueId = `tenant:${ tenant } :queue:multi-reclaim` ;
739+ const queueKey = keys . queueKey ( queueId ) ;
740+ const queueItemsKey = keys . queueItemsKey ( queueId ) ;
741+
742+ const messageId = `msg-${ tenant } ` ;
743+ const storedMessage = {
744+ id : messageId ,
745+ queueId,
746+ tenantId : tenant ,
747+ payload : { id : 1 } ,
748+ timestamp : Date . now ( ) - 1000 ,
749+ attempt : 1 ,
750+ } ;
751+
752+ await redis . zadd ( queueKey , storedMessage . timestamp , messageId ) ;
753+ await redis . hset ( queueItemsKey , messageId , JSON . stringify ( storedMessage ) ) ;
754+
755+ await manager . claim ( queueId , queueKey , queueItemsKey , "consumer-1" , 100 ) ;
756+ }
757+
758+ // Wait for timeout
759+ await new Promise ( ( resolve ) => setTimeout ( resolve , 150 ) ) ;
760+
761+ // Reclaim should return both messages
762+ const reclaimedMessages = await manager . reclaimTimedOut ( 0 , ( qId ) => ( {
763+ queueKey : keys . queueKey ( qId ) ,
764+ queueItemsKey : keys . queueItemsKey ( qId ) ,
765+ masterQueueKey,
766+ } ) ) ;
767+
768+ expect ( reclaimedMessages ) . toHaveLength ( 2 ) ;
769+
770+ // Verify both tenants are represented
771+ const tenantIds = reclaimedMessages . map ( ( m : ReclaimedMessageInfo ) => m . tenantId ) . sort ( ) ;
772+ expect ( tenantIds ) . toEqual ( [ "t1" , "t2" ] ) ;
773+
774+ await manager . close ( ) ;
775+ await redis . quit ( ) ;
776+ }
777+ ) ;
778+ } ) ;
600779} ) ;
601780
0 commit comments