Skip to content

Commit 3cc2bed

Browse files
committed
add test to ensure no negative interaction between changes feed and compound sequences.
1 parent 1247b36 commit 3cc2bed

1 file changed

Lines changed: 139 additions & 0 deletions

File tree

rest/changestest/changes_api_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
37643903
func waitForCompactStopped(dbc *db.DatabaseContext) error {
37653904
for range 100 {
37663905
compactRunning := dbc.CacheCompactActive()

0 commit comments

Comments
 (0)