@@ -661,45 +661,129 @@ describe("CK virtual-time (SFQ) dequeue", () => {
661661 }
662662 ) ;
663663
664- redisTest ( "future-scheduled variants are skipped without advance" , async ( { redisContainer } ) => {
665- const queue = createQueue ( redisContainer ) ;
666- try {
667- const t0 = Date . now ( ) - 100_000 ;
664+ redisTest (
665+ "future-scheduled variants are skipped, not advanced, and de-registered" ,
666+ async ( { redisContainer } ) => {
667+ const queue = createQueue ( redisContainer ) ;
668+ try {
669+ const t0 = Date . now ( ) - 100_000 ;
668670
669- // a normal ready variant so the :ck:* wildcard is selected from the master queue
670- await queue . enqueueMessage ( {
671- env : authenticatedEnvDev ,
672- message : makeMessage ( { runId : "r-now" , concurrencyKey : "now" , timestamp : t0 } ) ,
673- workerQueue : authenticatedEnvDev . id ,
674- skipDequeueProcessing : true ,
675- } ) ;
676- // a future-scheduled variant
677- await queue . enqueueMessage ( {
678- env : authenticatedEnvDev ,
679- message : makeMessage ( {
680- runId : "r-future" ,
681- concurrencyKey : "future" ,
682- timestamp : Date . now ( ) + 60_000 ,
683- } ) ,
684- workerQueue : authenticatedEnvDev . id ,
685- skipDequeueProcessing : true ,
686- } ) ;
671+ // a normal ready variant so the :ck:* wildcard is selected from the master queue
672+ await queue . enqueueMessage ( {
673+ env : authenticatedEnvDev ,
674+ message : makeMessage ( { runId : "r-now" , concurrencyKey : "now" , timestamp : t0 } ) ,
675+ workerQueue : authenticatedEnvDev . id ,
676+ skipDequeueProcessing : true ,
677+ } ) ;
678+ // a future-scheduled variant
679+ await queue . enqueueMessage ( {
680+ env : authenticatedEnvDev ,
681+ message : makeMessage ( {
682+ runId : "r-future" ,
683+ concurrencyKey : "future" ,
684+ timestamp : Date . now ( ) + 60_000 ,
685+ } ) ,
686+ workerQueue : authenticatedEnvDev . id ,
687+ skipDequeueProcessing : true ,
688+ } ) ;
687689
688- const ckVtimeKey = testOptions . keys . ckVtimeKeyFromQueue ( variantName ( "now" ) ) ;
689- const futureVariant = variantName ( "future" ) ;
690- await queue . redis . zadd ( ckVtimeKey , 0 , variantName ( "now" ) , 5 , futureVariant ) ;
690+ const ckVtimeKey = testOptions . keys . ckVtimeKeyFromQueue ( variantName ( "now" ) ) ;
691+ const futureVariant = variantName ( "future" ) ;
692+ await queue . redis . zadd ( ckVtimeKey , 0 , variantName ( "now" ) , 5 , futureVariant ) ;
691693
692- const shard = testOptions . keys . masterQueueShardForEnvironment ( authenticatedEnvDev . id , 2 ) ;
693- const messages = await queue . testDequeueFromMasterQueue ( shard , authenticatedEnvDev . id , 10 ) ;
694+ const shard = testOptions . keys . masterQueueShardForEnvironment ( authenticatedEnvDev . id , 2 ) ;
695+ const messages = await queue . testDequeueFromMasterQueue ( shard , authenticatedEnvDev . id , 10 ) ;
694696
695- expect ( messages . some ( ( m ) => m . message . concurrencyKey === "future" ) ) . toBe ( false ) ;
697+ expect ( messages . some ( ( m ) => m . message . concurrencyKey === "future" ) ) . toBe ( false ) ;
696698
697- const futureTag = Number ( await queue . redis . zscore ( ckVtimeKey , futureVariant ) ) ;
698- expect ( futureTag ) . toBe ( 5 ) ;
699- } finally {
700- await queue . quit ( ) ;
699+ // Not served, so never charged a quantum: its tag is not advanced past the 5 it
700+ // was seeded with. It is de-registered instead, because a variant with no ready
701+ // work is not competing and must not hold the floor down (a pinned floor is what
702+ // let a later arrival register underneath the established keys and take every
703+ // pass-1 slot). It rejoins at the floor of the day once it has ready work.
704+ const futureTag = await queue . redis . zscore ( ckVtimeKey , futureVariant ) ;
705+ expect ( futureTag ) . toBeNull ( ) ;
706+ } finally {
707+ await queue . quit ( ) ;
708+ }
701709 }
702- } ) ;
710+ ) ;
711+
712+ redisTest (
713+ "an unservable variant does not pin the floor for later arrivals" ,
714+ async ( { redisContainer } ) => {
715+ // Regression: a variant with work but nothing ready (a nack backoff is the common
716+ // case) used to sit in ckVtime holding the lowest tag. The floor is the minimum
717+ // stored tag, so it froze at that value while served keys advanced, and because new
718+ // keys register at the floor, a key arriving later started far below the established
719+ // ones and won every pass-1 slot until it caught up. That is the starvation this
720+ // feature exists to prevent, inverted.
721+ const queue = createQueue ( redisContainer ) ;
722+ try {
723+ const t0 = Date . now ( ) - 100_000 ;
724+
725+ // Stalled: registered on enqueue, but its head never becomes ready during the test.
726+ await queue . enqueueMessage ( {
727+ env : authenticatedEnvDev ,
728+ message : makeMessage ( {
729+ runId : "r-stalled" ,
730+ concurrencyKey : "stalled" ,
731+ timestamp : Date . now ( ) + 60 * 60 * 1000 ,
732+ } ) ,
733+ workerQueue : authenticatedEnvDev . id ,
734+ skipDequeueProcessing : true ,
735+ } ) ;
736+
737+ for ( let i = 0 ; i < 12 ; i ++ ) {
738+ await queue . enqueueMessage ( {
739+ env : authenticatedEnvDev ,
740+ message : makeMessage ( {
741+ runId : `r-busy-${ i } ` ,
742+ concurrencyKey : "busy" ,
743+ timestamp : t0 + i ,
744+ } ) ,
745+ workerQueue : authenticatedEnvDev . id ,
746+ skipDequeueProcessing : true ,
747+ } ) ;
748+ }
749+
750+ const shard = testOptions . keys . masterQueueShardForEnvironment ( authenticatedEnvDev . id , 2 ) ;
751+ for ( let call = 0 ; call < 6 ; call ++ ) {
752+ const messages = await queue . testDequeueFromMasterQueue ( shard , authenticatedEnvDev . id , 1 ) ;
753+ for ( const m of messages ) {
754+ await queue . acknowledgeMessage ( authenticatedEnvDev . organization . id , m . messageId , {
755+ skipDequeueProcessing : true ,
756+ } ) ;
757+ }
758+ }
759+
760+ const busyVariant = variantName ( "busy" ) ;
761+ const floorKey = testOptions . keys . ckVtimeFloorKeyFromQueue ( busyVariant ) ;
762+ const floor = Number ( ( await queue . redis . get ( floorKey ) ) ?? "0" ) ;
763+
764+ // The floor tracked the key that was actually being served.
765+ expect ( floor ) . toBeGreaterThan ( 0 ) ;
766+
767+ // A key arriving now joins level with the established keys rather than underneath
768+ // them, so it gets its turn instead of monopolising the fair pass.
769+ await queue . enqueueMessage ( {
770+ env : authenticatedEnvDev ,
771+ message : makeMessage ( { runId : "r-newcomer" , concurrencyKey : "newcomer" , timestamp : t0 } ) ,
772+ workerQueue : authenticatedEnvDev . id ,
773+ skipDequeueProcessing : true ,
774+ } ) ;
775+
776+ const ckVtimeKey = testOptions . keys . ckVtimeKeyFromQueue ( busyVariant ) ;
777+ const newcomerTag = Number ( await queue . redis . zscore ( ckVtimeKey , variantName ( "newcomer" ) ) ) ;
778+ const busyTag = Number ( await queue . redis . zscore ( ckVtimeKey , busyVariant ) ) ;
779+
780+ expect ( newcomerTag ) . toBe ( floor ) ;
781+ expect ( busyTag - newcomerTag ) . toBeLessThanOrEqual ( 1 ) ;
782+ } finally {
783+ await queue . quit ( ) ;
784+ }
785+ }
786+ ) ;
703787
704788 redisTest (
705789 "enqueue registers the variant at the current floor with NX" ,
0 commit comments