@@ -598,3 +598,288 @@ func TestChannelCacheBackgroundTaskWithIllegalTimeInterval(t *testing.T) {
598598 assert .Equal (t , "CleanAgedItems" , backgroundTaskError .TaskName )
599599 assert .Equal (t , options .ChannelCacheAge , backgroundTaskError .Interval )
600600}
601+
602+ // - The channel cache is validFrom sequence n, with one active mutation resident in the cache
603+ // - There are channel removals (only) in the bucket with m < sequence < n
604+ // - Client issues a GetChanges request with since=m
605+ func TestChannelCacheActiveOnlyAndLimit (t * testing.T ) {
606+ ctx , db , collection := setupDBWithChannelCacheSize (t , 2 )
607+
608+ const (
609+ activeChannel = "active"
610+ inactiveChannel = "inactive"
611+ doc1 = "doc1"
612+ doc2 = "doc2"
613+ doc3 = "doc3"
614+ )
615+
616+ // doc1 rev1: channel active
617+ // doc1 rev2: channel inactive
618+ // doc2 rev1: channel active
619+ // doc2 rev2: channel inactive
620+ // doc3 rev1: channel active
621+ revID , _ , err := collection .Put (ctx , doc1 , Body {"channels" : activeChannel })
622+ require .NoError (t , err )
623+ _ , _ , err = collection .Put (ctx , doc1 , Body {"channels" : inactiveChannel , "_rev" : revID })
624+ require .NoError (t , err )
625+ revID , _ , err = collection .Put (ctx , doc2 , Body {"channels" : activeChannel })
626+ require .NoError (t , err )
627+ _ , _ , err = collection .Put (ctx , doc2 , Body {"channels" : inactiveChannel , "_rev" : revID })
628+ require .NoError (t , err )
629+
630+ _ , _ , err = collection .Put (ctx , doc3 , Body {"channels" : activeChannel })
631+ require .NoError (t , err )
632+
633+ db .WaitForPendingChanges (t )
634+
635+ // prime channel cache, doc2 and doc3 should be in cache
636+ changesOptions := ChangesOptions {
637+ Since : SequenceID {Seq : 0 },
638+ ActiveOnly : false ,
639+ ChangesCtx : base .TestCtx (t ),
640+ }
641+ require .Len (t , getChanges (t , collection , base .SetOf (activeChannel ), changesOptions ), 3 )
642+
643+ // whether limit or no limit, should only be 1 active entry
644+ for _ , limit := range []int {0 , 1 } {
645+ t .Run ("limit=" + fmt .Sprint (limit ), func (t * testing.T ) {
646+ changesOptions = ChangesOptions {
647+ Since : SequenceID {Seq : 0 },
648+ ActiveOnly : true ,
649+ ChangesCtx : base .TestCtx (t ),
650+ Limit : limit ,
651+ }
652+ require .Len (t , getChanges (t , collection , base .SetOf (activeChannel ), changesOptions ), 1 )
653+ })
654+ }
655+ }
656+
657+ func TestChannelCacheActiveOnlyScenarios (t * testing.T ) {
658+ const activeChannel = "active"
659+
660+ t .Run ("query returns an active rev, cache is all removals" , func (t * testing.T ) {
661+ ctx , db , collection := setupDBWithChannelCacheSize (t , 2 )
662+
663+ // doc1: active (seq 1) - will be in backing store, not cache
664+ _ , _ , _ = collection .Put (ctx , "doc1" , Body {"channels" : activeChannel })
665+
666+ // doc2: active (seq 2) -> inactive (seq 3) - seq 3 in cache
667+ revID2 , _ , _ := collection .Put (ctx , "doc2" , Body {"channels" : activeChannel })
668+ _ , _ , _ = collection .Put (ctx , "doc2" , Body {"channels" : "other" , "_rev" : revID2 })
669+
670+ // doc3: active (seq 4) -> inactive (seq 5) - seq 5 in cache
671+ revID3 , _ , _ := collection .Put (ctx , "doc3" , Body {"channels" : activeChannel })
672+ _ , _ , _ = collection .Put (ctx , "doc3" , Body {"channels" : "other" , "_rev" : revID3 })
673+
674+ db .WaitForPendingChanges (t )
675+
676+ // With limit 1 (before)
677+ changesOptions := ChangesOptions {Since : SequenceID {Seq : 0 }, ActiveOnly : true , Limit : 1 , ChangesCtx : base .TestCtx (t )}
678+ changes := getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
679+ require .Len (t , changes , 1 )
680+ assert .Equal (t , "doc1" , changes [0 ].ID )
681+
682+ // No limit
683+ changesOptions .Limit = 0
684+ changes = getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
685+ require .Len (t , changes , 1 )
686+ assert .Equal (t , "doc1" , changes [0 ].ID )
687+
688+ // With limit 1 (after)
689+ changesOptions .Limit = 1
690+ changes = getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
691+ require .Len (t , changes , 1 )
692+ assert .Equal (t , "doc1" , changes [0 ].ID )
693+ })
694+
695+ t .Run ("query returns an active rev, cache also has an active rev" , func (t * testing.T ) {
696+ ctx , db , collection := setupDBWithChannelCacheSize (t , 2 )
697+
698+ // doc1: active (seq 1) - backing store
699+ _ , _ , _ = collection .Put (ctx , "doc1" , Body {"channels" : activeChannel })
700+
701+ // doc2: active (seq 2) -> inactive (seq 3) - cache
702+ revID2 , _ , _ := collection .Put (ctx , "doc2" , Body {"channels" : activeChannel })
703+ _ , _ , _ = collection .Put (ctx , "doc2" , Body {"channels" : "other" , "_rev" : revID2 })
704+
705+ // doc3: active (seq 4) - cache
706+ _ , _ , _ = collection .Put (ctx , "doc3" , Body {"channels" : activeChannel })
707+
708+ db .WaitForPendingChanges (t )
709+
710+ // With limit 1 (before)
711+ changesOptions := ChangesOptions {Since : SequenceID {Seq : 0 }, ActiveOnly : true , Limit : 1 , ChangesCtx : base .TestCtx (t )}
712+ changes := getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
713+ require .Len (t , changes , 1 )
714+ assert .Equal (t , "doc1" , changes [0 ].ID )
715+
716+ // No limit: should get doc1 and doc3
717+ changesOptions .Limit = 0
718+ changes = getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
719+ require .Len (t , changes , 2 )
720+ assert .Equal (t , "doc1" , changes [0 ].ID )
721+ assert .Equal (t , "doc3" , changes [1 ].ID )
722+
723+ // With limit 1 (after)
724+ changesOptions .Limit = 1
725+ changes = getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
726+ require .Len (t , changes , 1 )
727+ assert .Equal (t , "doc1" , changes [0 ].ID )
728+ })
729+
730+ t .Run ("query has no active revs, cache has no active revs" , func (t * testing.T ) {
731+ ctx , db , collection := setupDBWithChannelCacheSize (t , 2 )
732+
733+ // doc1: active (seq 1) -> inactive (seq 2)
734+ revID1 , _ , _ := collection .Put (ctx , "doc1" , Body {"channels" : activeChannel })
735+ _ , _ , _ = collection .Put (ctx , "doc1" , Body {"channels" : "other" , "_rev" : revID1 })
736+
737+ // doc2: active (seq 3) -> inactive (seq 4)
738+ revID2 , _ , _ := collection .Put (ctx , "doc2" , Body {"channels" : activeChannel })
739+ _ , _ , _ = collection .Put (ctx , "doc2" , Body {"channels" : "other" , "_rev" : revID2 })
740+
741+ db .WaitForPendingChanges (t )
742+
743+ // With limit 1 (before)
744+ changesOptions := ChangesOptions {Since : SequenceID {Seq : 0 }, ActiveOnly : true , Limit : 1 , ChangesCtx : base .TestCtx (t )}
745+ changes := getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
746+ require .Len (t , changes , 0 )
747+
748+ // No limit: should get nothing
749+ changesOptions .Limit = 0
750+ changes = getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
751+ require .Len (t , changes , 0 )
752+
753+ // With limit 1 (after)
754+ changesOptions .Limit = 1
755+ changes = getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
756+ require .Len (t , changes , 0 )
757+ })
758+ t .Run ("cache populated, query requires pagination" , func (t * testing.T ) {
759+ cacheOptions := DefaultCacheOptions ()
760+ cacheOptions .ChannelCacheMaxLength = 5
761+ cacheOptions .ChannelQueryLimit = 5
762+ ctx , db , collection := setupDBWithChannelCacheSettings (t , cacheOptions )
763+ // seed activeChannel in the cache prior to writing docs
764+ changesOptions := ChangesOptions {Since : SequenceID {Seq : 0 }, ChangesCtx : base .TestCtx (t )}
765+ _ = getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
766+ // Write 20 docs to the channel. 5 should be cached, 15 require query
767+ for i := range 20 {
768+ docID := fmt .Sprintf ("doc%d" , i + 1 )
769+ _ , _ , _ = collection .Put (ctx , docID , Body {"channels" : activeChannel })
770+ }
771+ db .WaitForPendingChanges (t )
772+ changesOptions = ChangesOptions {Since : SequenceID {Seq : 0 }, ActiveOnly : true , Limit : 0 , ChangesCtx : base .TestCtx (t )}
773+ changes := getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
774+ require .Len (t , changes , 20 )
775+ for i , change := range changes {
776+ assert .Equal (t , fmt .Sprintf ("doc%d" , i + 1 ), change .ID , "change at index %d" , i )
777+ }
778+ })
779+ t .Run ("cache populated, query requires pagination with limit" , func (t * testing.T ) {
780+ // With ChannelQueryLimit=5 and Limit=10, each GetChanges call returns 5 entries from the
781+ // query range. Without the queryHitActiveLimit guard, the first call would append the
782+ // cache (docs 16-20) immediately and skip docs 6-15.
783+ cacheOptions := DefaultCacheOptions ()
784+ cacheOptions .ChannelCacheMaxLength = 5
785+ cacheOptions .ChannelQueryLimit = 5
786+ ctx , db , collection := setupDBWithChannelCacheSettings (t , cacheOptions )
787+ changesOptions := ChangesOptions {Since : SequenceID {Seq : 0 }, ChangesCtx : base .TestCtx (t )}
788+ _ = getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
789+ for i := 1 ; i <= 20 ; i ++ {
790+ _ , _ , _ = collection .Put (ctx , fmt .Sprintf ("doc%d" , i ), Body {"channels" : activeChannel })
791+ }
792+ db .WaitForPendingChanges (t )
793+ // Limit=10 spans two query batches (5+5) before reaching cache. The pagination loop must
794+ // not prematurely append cache after the first batch hits the active limit.
795+ changesOptions = ChangesOptions {Since : SequenceID {Seq : 0 }, ActiveOnly : true , Limit : 10 , ChangesCtx : base .TestCtx (t )}
796+ changes := getChanges (t , collection , base .SetOf (activeChannel ), changesOptions )
797+ require .Len (t , changes , 10 )
798+ for i , change := range changes {
799+ assert .Equal (t , fmt .Sprintf ("doc%d" , i + 1 ), change .ID , "change at index %d" , i )
800+ }
801+ })
802+ }
803+
804+ func setupDBWithChannelCacheSize (t * testing.T , maxLength int ) (context.Context , * Database , * DatabaseCollectionWithUser ) {
805+ cacheOptions := DefaultCacheOptions ()
806+ cacheOptions .ChannelCacheMaxLength = maxLength
807+ return setupDBWithChannelCacheSettings (t , cacheOptions )
808+ }
809+
810+ func setupDBWithChannelCacheSettings (t * testing.T , cacheOptions CacheOptions ) (context.Context , * Database , * DatabaseCollectionWithUser ) {
811+ db , ctx := setupTestDBWithCacheOptions (t , cacheOptions )
812+ t .Cleanup (func () { db .Close (ctx ) })
813+ collection , ctx := GetSingleDatabaseCollectionWithUser (ctx , t , db )
814+ _ , err := collection .UpdateSyncFun (ctx , channels .DocChannelsSyncFunction )
815+ require .NoError (t , err )
816+ return ctx , db , collection
817+ }
818+
819+ // FuzzChannelCacheActiveOnly verifies that GetChanges with ActiveOnly=true returns every active entry
820+ // exactly once, in sequence order, regardless of how results are split between backing-store query
821+ // batches and the in-memory cache.
822+ func FuzzChannelCacheActiveOnly (f * testing.F ) {
823+ f .Add (uint8 (20 ), uint8 (5 ), uint8 (5 ), uint8 (10 )) // query + cache, limit spans two batches
824+ f .Add (uint8 (5 ), uint8 (5 ), uint8 (5 ), uint8 (0 )) // all docs fit in cache, no limit
825+ f .Add (uint8 (1 ), uint8 (5 ), uint8 (5 ), uint8 (1 )) // single doc, limit=1
826+ f .Add (uint8 (0 ), uint8 (5 ), uint8 (5 ), uint8 (5 )) // no docs
827+ f .Add (uint8 (10 ), uint8 (2 ), uint8 (2 ), uint8 (5 )) // tiny cache + single-entry query batches
828+ f .Add (uint8 (15 ), uint8 (5 ), uint8 (5 ), uint8 (5 )) // limit < cache boundary
829+
830+ f .Fuzz (func (t * testing.T , numDocs , cacheMaxLength , queryLimit , requestLimit uint8 ) {
831+ if cacheMaxLength == 0 {
832+ cacheMaxLength = 1
833+ }
834+ if queryLimit == 0 {
835+ queryLimit = 1
836+ }
837+ const maxDocs = 30
838+ n := int (numDocs )
839+ if n > maxDocs {
840+ n = maxDocs
841+ }
842+
843+ cacheOptions := DefaultCacheOptions ()
844+ cacheOptions .ChannelCacheMaxLength = int (cacheMaxLength )
845+ cacheOptions .ChannelQueryLimit = int (queryLimit )
846+ ctx , db , collection := setupDBWithChannelCacheSettings (t , cacheOptions )
847+
848+ // Seed the channel in the cache before writing docs so subsequent writes land
849+ // in the cache from sequence 1.
850+ _ = getChanges (t , collection , base .SetOf ("active" ), ChangesOptions {
851+ Since : SequenceID {Seq : 0 }, ChangesCtx : base .TestCtx (t ),
852+ })
853+ for i := 1 ; i <= n ; i ++ {
854+ _ , _ , _ = collection .Put (ctx , fmt .Sprintf ("doc%d" , i ), Body {"channels" : "active" })
855+ }
856+ db .WaitForPendingChanges (t )
857+
858+ changes := getChanges (t , collection , base .SetOf ("active" ), ChangesOptions {
859+ Since : SequenceID {Seq : 0 },
860+ ActiveOnly : true ,
861+ Limit : int (requestLimit ),
862+ ChangesCtx : base .TestCtx (t ),
863+ })
864+
865+ // No duplicates.
866+ seen := make (map [string ]bool , len (changes ))
867+ for _ , c := range changes {
868+ require .False (t , seen [c .ID ], "duplicate entry %s" , c .ID )
869+ seen [c .ID ] = true
870+ }
871+
872+ // Correct count: all docs, or exactly requestLimit if that's the smaller constraint.
873+ limit := int (requestLimit )
874+ expectedCount := n
875+ if limit > 0 && limit < n {
876+ expectedCount = limit
877+ }
878+ require .Len (t , changes , expectedCount )
879+
880+ // Entries must be the first expectedCount docs in insertion order.
881+ for i , c := range changes {
882+ require .Equal (t , fmt .Sprintf ("doc%d" , i + 1 ), c .ID , "wrong doc at index %d" , i )
883+ }
884+ })
885+ }
0 commit comments