Skip to content

Commit 25cae9b

Browse files
authored
CBG-5383: [4.0.5 backport] fix resync regenerate sequences set sync info no-op (#8273)
1 parent 57f7e12 commit 25cae9b

6 files changed

Lines changed: 159 additions & 8 deletions

File tree

base/constants_syncdocs.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const MetadataIdPrefix = "m_" // Prefix for met
2222
const SyncDocMetadataPrefix = SyncDocPrefix + MetadataIdPrefix // Prefix for all namespaced Sync Gateway metadata documents
2323
const DCPCheckpointPrefix = "dcp_ck:"
2424

25+
const DefaultMetadataID = "_default" // MetadataID assigned to databases that include _default._default, signalling legacy (unprefixed) metadata key format.
26+
2527
const minimumAttachmentMigrationMetadataVersion = "4.0.0" // minimum metadata version that needs to be defined for metadata migration.
2628

2729
// Sync Gateway Metadata document types
@@ -138,10 +140,11 @@ var DefaultMetadataKeys = &MetadataKeys{
138140
sessionPrefix: formatDefaultMetadataKey(MetaKeySessionPrefix),
139141
}
140142

141-
// NewMetadataKeys returns MetadataKeys for the specified MetadataID If metadataID is empty string, returns the default (legacy) metadata keys.
143+
// NewMetadataKeys returns MetadataKeys for the specified MetadataID. If metadataID is empty string or DefaultMetadataID,
144+
// returns the default (legacy) metadata keys with no prefix.
142145
// Key and prefix formatting is done in this constructor to minimize the work done per retrieval.
143146
func NewMetadataKeys(metadataID string) *MetadataKeys {
144-
if metadataID == "" {
147+
if metadataID == "" || metadataID == DefaultMetadataID {
145148
return DefaultMetadataKeys
146149
} else {
147150
return &MetadataKeys{

db/database_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4716,6 +4716,42 @@ func TestSettingSyncInfo(t *testing.T) {
47164716
}, syncInfo)
47174717
}
47184718

4719+
// TestSetSyncInfoDefaultMetadataID verifies that a database created with the default collection gets
4720+
// MetadataID="_default" on its DatabaseContextOptions, and that this value correctly updates the syncInfo
4721+
// document via SetSyncInfoMetadataID so that subsequent InitSyncInfo calls do not require resync.
4722+
func TestSetSyncInfoDefaultMetadataID(t *testing.T) {
4723+
cacheOptions := DefaultCacheOptions()
4724+
dbcOptions := DatabaseContextOptions{
4725+
Scopes: GetScopesOptionsDefaultCollectionOnly(t),
4726+
CacheOptions: &cacheOptions,
4727+
MetadataID: base.DefaultMetadataID,
4728+
}
4729+
db, ctx := SetupTestDBWithOptions(t, dbcOptions)
4730+
defer db.Close(ctx)
4731+
4732+
collection, _ := GetSingleDatabaseCollectionWithUser(ctx, t, db)
4733+
ds := collection.GetCollectionDatastore()
4734+
4735+
require.Equal(t, base.DefaultMetadataID, db.DatabaseContext.Options.MetadataID)
4736+
4737+
// Simulate a collection previously owned by a different database, including metadata_version for realism
4738+
oldSyncInfo := &base.SyncInfo{MetadataID: base.Ptr("previous_database"), MetaDataVersion: MetaVersionValue}
4739+
require.NoError(t, ds.Set(base.SGSyncInfo, 0, nil, oldSyncInfo))
4740+
4741+
// InitSyncInfo should detect the mismatch and require resync
4742+
requiresResync, _, err := base.InitSyncInfo(ctx, ds, db.DatabaseContext.Options.MetadataID)
4743+
require.NoError(t, err)
4744+
require.True(t, requiresResync)
4745+
4746+
// SetSyncInfoMetadataID with the same Options.MetadataID should update the syncInfo document
4747+
require.NoError(t, base.SetSyncInfoMetadataID(ds, db.DatabaseContext.Options.MetadataID))
4748+
4749+
// Subsequent InitSyncInfo should find a match and not require resync
4750+
requiresResync, _, err = base.InitSyncInfo(ctx, ds, db.DatabaseContext.Options.MetadataID)
4751+
require.NoError(t, err)
4752+
assert.False(t, requiresResync)
4753+
}
4754+
47194755
func TestInitSyncInfo(t *testing.T) {
47204756
type testCase struct {
47214757
name string

rest/adminapitest/resync_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,3 +368,107 @@ function sync(doc, oldDoc){
368368
require.NoError(t, err)
369369
assert.Equal(t, string(bodyGet), string(specBody))
370370
}
371+
372+
// TestResyncRequireResyncDefaultMetadataID
373+
//
374+
// Setup: db1 owns _default.sg_test_0 (gets non-default metadataID). db2 owns _default._default (default metadata id).
375+
// Delete db1, update db2 to include _default.sg_test_0. Because db2 includes _default._default,
376+
// it gets metadataID="_default". Assert that after resync regenerate sequences, db2 comes online successfully.
377+
func TestResyncRequireResyncDefaultMetadataID(t *testing.T) {
378+
if base.UnitTestUrlIsWalrus() {
379+
t.Skip("Test requires Couchbase Server")
380+
}
381+
base.TestRequiresCollections(t)
382+
base.SetUpTestLogging(t, base.LevelInfo, base.KeyAll)
383+
384+
ctx := base.TestCtx(t)
385+
tb := base.GetTestBucket(t)
386+
defer tb.Close(ctx)
387+
388+
const namedCollectionName = "sg_test_0"
389+
require.NoError(t, tb.CreateDataStore(ctx, base.ScopeAndCollectionName{Scope: base.DefaultScope, Collection: namedCollectionName}))
390+
defer func() {
391+
assert.NoError(t, tb.DropDataStore(base.ScopeAndCollectionName{Scope: base.DefaultScope, Collection: namedCollectionName}))
392+
}()
393+
394+
rt := rest.NewRestTester(t, &rest.RestTesterConfig{
395+
CustomTestBucket: tb.NoCloseClone(),
396+
PersistentConfig: true,
397+
})
398+
defer rt.Close()
399+
400+
db1Name := "db1"
401+
db2Name := "db2"
402+
403+
// Create db1 with _default scope and sg_test_0 collection only — gets metadataID="db1"
404+
db1Cfg := rt.NewDbConfig()
405+
db1Cfg.Scopes = rest.ScopesConfig{
406+
base.DefaultScope: rest.ScopeConfig{
407+
Collections: rest.CollectionsConfig{
408+
namedCollectionName: {},
409+
},
410+
},
411+
}
412+
rest.RequireStatus(t, rt.CreateDatabase(db1Name, db1Cfg), http.StatusCreated)
413+
414+
// Write a doc to the sg_test_0 collection in db1
415+
keyspace := db1Name + "._default." + namedCollectionName
416+
resp := rt.SendAdminRequest("PUT", "/"+keyspace+"/testDoc1", `{"foo":"bar"}`)
417+
rest.RequireStatus(t, resp, http.StatusCreated)
418+
419+
// Create db2 with _default scope and _default collection only — gets metadataID="_default"
420+
db2Cfg := rt.NewDbConfig()
421+
db2Cfg.Scopes = rest.ScopesConfig{
422+
base.DefaultScope: rest.ScopeConfig{
423+
Collections: rest.CollectionsConfig{
424+
base.DefaultCollection: {},
425+
},
426+
},
427+
}
428+
rest.RequireStatus(t, rt.CreateDatabase(db2Name, db2Cfg), http.StatusCreated)
429+
430+
// Delete db1 to free the sg_test_0 collection
431+
resp = rt.SendAdminRequest("DELETE", "/"+db1Name+"/", "")
432+
rest.RequireStatus(t, resp, http.StatusOK)
433+
434+
// Update db2 to include both _default._default AND _default.sg_test_0.
435+
// db2 keeps metadataID="_default" because it includes _default._default.
436+
// The sg_test_0 collection's syncInfo still has db1's non-default metadataID — requires resync.
437+
db2Cfg.Scopes = rest.ScopesConfig{
438+
base.DefaultScope: rest.ScopeConfig{
439+
Collections: rest.CollectionsConfig{
440+
base.DefaultCollection: {},
441+
namedCollectionName: {},
442+
},
443+
},
444+
}
445+
rest.RequireStatus(t, rt.UpsertDbConfig(db2Name, db2Cfg), http.StatusCreated)
446+
447+
// db2 should now require resync for the sg_test_0 collection and be offline
448+
rt.WaitForDatabaseState(db2Name, db.RunStateString[db.DBOffline])
449+
450+
dbRoot := rt.GetDatabaseRoot(db2Name)
451+
expectedResyncCollection := base.DefaultScope + "." + namedCollectionName
452+
require.Contains(t, dbRoot.RequireResync, expectedResyncCollection,
453+
"sg_test_0 collection should require resync after moving from db1 to db2")
454+
455+
// Run resync with regenerate_sequences=true on the sg_test_0 collection only
456+
resyncBody := fmt.Sprintf(`{"scopes": {"_default": ["%s"]}}`, namedCollectionName)
457+
resp = rt.SendAdminRequest("POST", "/"+db2Name+"/_resync?action=start&regenerate_sequences=true", resyncBody)
458+
rest.RequireStatus(t, resp, http.StatusOK)
459+
460+
// Wait for resync to complete
461+
rt.WaitForResyncDCPStatusForDB(db.BackgroundProcessStateCompleted, db2Name)
462+
463+
// Verify in-memory RequireResync is cleared — this passes because Run() updated db.RequireResync
464+
dbRoot = rt.GetDatabaseRoot(db2Name)
465+
assert.Empty(t, dbRoot.RequireResync, "RequireResync should be empty after resync completed")
466+
467+
// Bring db2 online via POST /_config with offline:false.
468+
// TakeDbOnline calls ReloadDatabase which re-runs InitSyncInfo for every collection.
469+
// Requires resync should evaluate to false so syncInfo should be updated with the new metadataID and the database should come online successfully.
470+
resp = rt.SendAdminRequest("POST", "/"+db2Name+"/_config", `{"offline":false}`)
471+
rest.RequireStatus(t, resp, http.StatusCreated)
472+
473+
rt.WaitForDatabaseState(db2Name, db.RunStateString[db.DBOnline])
474+
}

rest/config_manager.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ var _ ConfigManager = &bootstrapContext{}
4646
const configUpdateMaxRetryAttempts = 5 // Maximum number of retries due to conflicting updates or rollback
4747
const configFetchMaxRetryAttempts = 5 // Maximum number of retries due to registry rollback
4848

49-
const defaultMetadataID = "_default"
49+
const defaultMetadataID = base.DefaultMetadataID
5050

5151
// GetConfig fetches a database name for a given bucket and config group ID.
5252
func (b *bootstrapContext) GetConfigName(ctx context.Context, bucketName, groupID, dbName string, configName *dbConfigNameOnly) (cas uint64, err error) {

rest/server_context.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -926,10 +926,8 @@ func (sc *ServerContext) _getOrAddDatabaseFromConfig(ctx context.Context, config
926926
}
927927
}
928928

929-
// If identified as default database, use metadataID of "" so legacy non namespaced docs are used
930-
if config.MetadataID != defaultMetadataID {
931-
contextOptions.MetadataID = config.MetadataID
932-
}
929+
// NewMetadataKeys handles the "_default" -> legacy (unprefixed) key mapping, so we can pass config.MetadataID through directly.
930+
contextOptions.MetadataID = config.MetadataID
933931

934932
contextOptions.BlipStatsReportingInterval = defaultBytesStatsReportingInterval.Milliseconds()
935933
contextOptions.ImportVersion = config.ImportVersion

rest/utilities_testing_resttester.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,9 +403,19 @@ func (rt *RestTester) RunResync() db.ResyncManagerResponseDCP {
403403

404404
// WaitForResyncDCPStatus waits for the resync status to reach the expected status and returns the final status.
405405
func (rt *RestTester) WaitForResyncDCPStatus(status db.BackgroundProcessState) db.ResyncManagerResponseDCP {
406+
return rt.waitForResyncDCPStatus(status, "{{.db}}")
407+
}
408+
409+
// WaitForResyncDCPStatusForDB waits for the resync status for a specific database name to reach the expected status.
410+
func (rt *RestTester) WaitForResyncDCPStatusForDB(status db.BackgroundProcessState, dbName string) db.ResyncManagerResponseDCP {
411+
return rt.waitForResyncDCPStatus(status, dbName)
412+
}
413+
414+
// waitForResyncDCPStatus waits for the resync status to reach the expected status and returns the final status.
415+
func (rt *RestTester) waitForResyncDCPStatus(status db.BackgroundProcessState, dbName string) db.ResyncManagerResponseDCP {
406416
var resyncStatus db.ResyncManagerResponseDCP
407417
require.EventuallyWithT(rt.TB(), func(c *assert.CollectT) {
408-
response := rt.SendAdminRequest("GET", "/{{.db}}/_resync", "")
418+
response := rt.SendAdminRequest("GET", "/"+dbName+"/_resync", "")
409419
RequireStatus(rt.TB(), response, http.StatusOK)
410420
require.NoError(rt.TB(), json.Unmarshal(response.BodyBytes(), &resyncStatus))
411421

0 commit comments

Comments
 (0)