@@ -3761,6 +3761,145 @@ func TestOneShotGrantRequestPlusDbConfig(t *testing.T) {
37613761 oneShotComplete .Wait ()
37623762}
37633763
3764+ // TestMultiChannelChangesWithTriggeredSequence tests that the changes feed correctly handles
3765+ // a compound since value containing both LowSeq and TriggeredBy (e.g. "6:10:3"), which is
3766+ // produced when a channel access grant (backfill, setting TriggeredBy) occurs while LowSeq
3767+ // mode is active (due to skipped sequences).
3768+ //
3769+ // This exercises the corner case fixed by CBG-5429: before the fix, SequenceID.String() would
3770+ // silently drop the LowSeq field when LowSeq > Seq, causing the client to receive an incorrect
3771+ // last_seq. On the next request the changes feed would use the wrong since value, potentially
3772+ // skipping backfill documents.
3773+ //
3774+ // Sequence layout written by WriteDirect (seq 1 = user creation via CreateUser):
3775+ //
3776+ // Seq Channel Notes
3777+ // 1 (user) CreateUser allocates this via the sequence allocator
3778+ // 2 ABC
3779+ // 3 DEF not visible until DEF access is granted
3780+ // 4 DEF not visible until DEF access is granted
3781+ // 5 ABC
3782+ // 6 ABC
3783+ // 7 DEF written late (after 9) to create the skipped-sequence / LowSeq condition
3784+ // 8 (gap) never written; skipped sequence
3785+ // 9 ABC
3786+ // 10+ (user update granting DEF access, after allocator is advanced)
3787+ func TestMultiChannelChangesWithTriggeredSequence (t * testing.T ) {
3788+ // base.SetUpTestLogging(t, base.LevelDebug, base.KeyChanges, base.KeyCache, base.KeyHTTP)
3789+
3790+ // MaxWaitPending: reduce from the 5 s default to 5 ms so the cache promotes pending
3791+ // sequences to the skipped queue almost immediately, keeping test latency low.
3792+ pendingMaxWait := uint32 (5 )
3793+
3794+ rt := rest .NewRestTester (t , & rest.RestTesterConfig {
3795+ SyncFn : `function(doc, oldDoc) {channel(doc.channels);}` ,
3796+ DatabaseConfig : & rest.DatabaseConfig {
3797+ DbConfig : rest.DbConfig {
3798+ CacheConfig : & rest.CacheConfig {
3799+ ChannelCacheConfig : & rest.ChannelCacheConfig {
3800+ MaxWaitPending : & pendingMaxWait ,
3801+ },
3802+ },
3803+ },
3804+ },
3805+ })
3806+ defer rt .Close ()
3807+ testDb := rt .GetDatabase ()
3808+ ctx := rt .Context ()
3809+
3810+ // ChannelQueryLimit forces the channel cache to paginate when fetching changes, exercising
3811+ // the interaction between the per-channel query limit and the request-level limit parameter.
3812+ testDb .Options .CacheOptions .ChannelQueryLimit = 5
3813+
3814+ collection , _ := rt .GetSingleTestDatabaseCollection ()
3815+
3816+ // CreateUser allocates seq 1 from the sequence allocator (_sync:seq becomes 1).
3817+ // The user starts with access to ABC only; DEF will be granted later.
3818+ rt .CreateUser ("sg-user" , []string {"ABC" })
3819+
3820+ // WriteDirect writes documents at explicit SG sequence numbers, bypassing the sequence
3821+ // allocator entirely (_sync:seq is not updated). This lets us craft a specific sequence
3822+ // layout including a deliberate gap to trigger LowSeq mode.
3823+ //
3824+ // Seq 9 is written before seq 7, creating a gap at seqs 7 and 8. With MaxWaitPending=5ms
3825+ // the cache quickly moves seqs 7 and 8 into the skipped-sequence queue and promotes seq 9,
3826+ // setting LowSeq = 6 (last contiguous sequence before the gap).
3827+ db .WriteDirect (t , collection , []string {"ABC" }, 2 )
3828+ db .WriteDirect (t , collection , []string {"DEF" }, 3 )
3829+ db .WriteDirect (t , collection , []string {"DEF" }, 4 )
3830+ db .WriteDirect (t , collection , []string {"ABC" }, 5 )
3831+ db .WriteDirect (t , collection , []string {"ABC" }, 6 )
3832+ db .WriteDirect (t , collection , []string {"ABC" }, 9 ) // gap: seqs 7 and 8 are missing
3833+ // Wait until seq 9 is in the cache and no longer in the skipped queue.
3834+ rt .WaitForSequenceNotSkipped (9 )
3835+
3836+ // Verify initial changes feed state: user sees 5 entries (seq 1 user doc, seqs 2/5/6/9 ABC).
3837+ // DEF docs at seqs 3 and 4 are not visible since user has no DEF access yet.
3838+ // last_seq = "6::9": LowSeq=6 (seqs 7,8 are in the skipped queue), HighSeq=9.
3839+ rt .WaitForPendingChanges ()
3840+ changes := rt .GetChanges ("/{{.keyspace}}/_changes" , "sg-user" )
3841+ require .Len (t , changes .Results , 5 )
3842+ since := changes .Results [0 ].Seq
3843+ assert .Equal (t , uint64 (1 ), since .Seq )
3844+ assert .Equal (t , "6::9" , changes .Last_Seq .String ())
3845+
3846+ // Confirm no spurious results when re-requesting with the compound since value.
3847+ changesJSON := fmt .Sprintf (`{"since":"%s"}` , changes .Last_Seq .String ())
3848+ changes = rt .PostChanges ("/{{.keyspace}}/_changes" , changesJSON , "sg-user" )
3849+ require .Len (t , changes .Results , 0 )
3850+
3851+ // Fill in seq 7 (DEF channel) — one of the two skipped sequences arrives late.
3852+ // This removes seq 7 from the skipped queue; only seq 8 remains skipped.
3853+ // LowSeq advances to 7. The late-sequence feeds for this changes iteration will
3854+ // deliver seq 7 even though it arrived after the initial feed was established.
3855+ db .WriteDirect (t , collection , []string {"DEF" }, 7 )
3856+ // WaitForSequence(7) confirms the cache has processed past seq 7. Because seq 9 was
3857+ // already processed earlier (nextSequence=10), this returns immediately.
3858+ rt .WaitForSequence (7 )
3859+
3860+ // With seq 7 now in the cache, the changes feed should deliver it via the late-sequence
3861+ // path. The user still has no DEF access, but the LowSeq advancement itself produces a
3862+ // result (last_seq moves forward). The last_seq from this response is used as the since
3863+ // value for the final request below.
3864+ changes = rt .PostChanges ("/{{.keyspace}}/_changes" , changesJSON , "sg-user" )
3865+ require .Len (t , changes .Results , 1 )
3866+
3867+ // WriteDirect bypasses the sequence allocator, so _sync:seq is still at 1 after all the
3868+ // WriteDirect calls above. If we now do a normal SG write (the user update below) without
3869+ // advancing the allocator, the write would be assigned seq 2 — colliding with the
3870+ // WriteDirect doc already at seq 2, and making TriggeredBy=2 for the DEF grant. With
3871+ // TriggeredBy=2 the backfill would look for DEF docs at seq ≤ 2, missing seqs 3, 4, 7.
3872+ //
3873+ // AllocateTestSequence increments _sync:seq directly (without updating the allocator's
3874+ // local last/max state). After 8 calls _sync:seq = 9. The next normal SG write then
3875+ // triggers a new batch reservation, landing at seq 10.
3876+ for range 8 {
3877+ _ , err := db .AllocateTestSequence (ctx , testDb )
3878+ require .NoError (t , err )
3879+ }
3880+
3881+ // Grant DEF access to the user. This is a normal SG write and allocates seq 10 from the
3882+ // sequence allocator. Seq 10 becomes the seqAddedAt for the DEF channel grant, and will
3883+ // appear as TriggeredBy in subsequent changes responses during the backfill.
3884+ //
3885+ // Combined with the LowSeq still active (seq 8 remains skipped), the next changes
3886+ // response will contain a since value of the form {LowSeq: L, TriggeredBy: 10, Seq: S}
3887+ // — the exact compound format that CBG-5429 fixes.
3888+ resp := rt .SendAdminRequest (http .MethodPut , "/{{.db}}/_user/sg-user" , rest .GetUserPayload (t , "" , rest .RestTesterDefaultUserPassword , "" , rt .GetSingleDataStore (), []string {"ABC" , "DEF" }, nil ))
3889+ rest .RequireStatus (t , resp , http .StatusOK )
3890+
3891+ // Use the last_seq from the post-seq7 response as the since value. This since has LowSeq
3892+ // set, and after the DEF grant the changes feed will add TriggeredBy, producing a compound
3893+ // since value {LowSeq: L, TriggeredBy: 10, Seq: S} — the CBG-5429 corner case.
3894+ changesJSON = fmt .Sprintf (`{"since":"%s"}` , changes .Last_Seq .String ())
3895+
3896+ // Expect 4 results: the user update (seq 10) plus the 3 DEF backfill docs (seqs 3, 4, 7).
3897+ // If Before() or String() are broken for the compound since value, backfill docs will be
3898+ // missed and this assertion will fail.
3899+ changes = rt .PostChanges ("/{{.keyspace}}/_changes" , changesJSON , "sg-user" )
3900+ require .Len (t , changes .Results , 4 )
3901+ }
3902+
37643903func waitForCompactStopped (dbc * db.DatabaseContext ) error {
37653904 for range 100 {
37663905 compactRunning := dbc .CacheCompactActive ()
0 commit comments