Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions rest/admin_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,10 @@ func (h *handler) handleCreateDB() error {
SGVersion: base.ProductVersion.String(),
}

h.server.lock.Lock()
defer h.server.lock.Unlock()
h.server._databasesLock.Lock()
defer h.server._databasesLock.Unlock()

if _, exists := h.server.databases_[dbName]; exists {
if _, exists := h.server._databases[dbName]; exists {
return base.HTTPErrorf(http.StatusPreconditionFailed, // what CouchDB returns
"Duplicate database name %q", dbName)
}
Expand Down Expand Up @@ -154,7 +154,7 @@ func (h *handler) handleCreateDB() error {
return base.HTTPErrorf(http.StatusInternalServerError, "couldn't save database config: %v", err)
}
// store the cas in the loaded config after a successful insert
h.server.dbConfigs[dbName].cfgCas = cas
h.server._dbConfigs[dbName].cfgCas = cas
} else {
// Intentionally pass in an empty BootstrapConfig to avoid inheriting any credentials or server when running with a legacy config (CBG-1764)
if err := config.setup(h.ctx(), dbName, BootstrapConfig{}, nil, nil, false); err != nil {
Expand Down Expand Up @@ -820,9 +820,9 @@ func (h *handler) handlePutDbConfig() (err error) {
auditDbAuditEnabled(h.ctx(), dbName, previousAuditEnabled, updatedAuditEnabled, updatedAuditEvents)
// store the cas in the loaded config after a successful update
h.setEtag(updatedDbConfig.Version)
h.server.lock.Lock()
defer h.server.lock.Unlock()
h.server.dbConfigs[dbName].cfgCas = cas
h.server._databasesLock.Lock()
defer h.server._databasesLock.Unlock()
h.server._dbConfigs[dbName].cfgCas = cas

base.Audit(h.ctx(), base.AuditIDUpdateDatabaseConfig, auditFields)
return base.HTTPErrorf(http.StatusCreated, "updated")
Expand Down Expand Up @@ -1145,8 +1145,8 @@ func (h *handler) handleDeleteCollectionConfigSync() error {
return err
}

h.server.lock.Lock()
defer h.server.lock.Unlock()
h.server._databasesLock.Lock()
defer h.server._databasesLock.Unlock()

// TODO: Dynamic update instead of reload
if err := h.server._reloadDatabaseWithConfig(h.ctx(), *updatedDbConfig, false, false); err != nil {
Expand Down Expand Up @@ -1214,8 +1214,8 @@ func (h *handler) handlePutCollectionConfigSync() error {
return err
}

h.server.lock.Lock()
defer h.server.lock.Unlock()
h.server._databasesLock.Lock()
defer h.server._databasesLock.Unlock()

// TODO: Dynamic update instead of reload
if err := h.server._reloadDatabaseWithConfig(h.ctx(), *updatedDbConfig, false, false); err != nil {
Expand Down Expand Up @@ -1314,8 +1314,8 @@ func (h *handler) handleDeleteCollectionConfigImportFilter() error {
return err
}

h.server.lock.Lock()
defer h.server.lock.Unlock()
h.server._databasesLock.Lock()
defer h.server._databasesLock.Unlock()

// TODO: Dynamic update instead of reload
if err := h.server._reloadDatabaseWithConfig(h.ctx(), *updatedDbConfig, false, false); err != nil {
Expand Down Expand Up @@ -1384,8 +1384,8 @@ func (h *handler) handlePutCollectionConfigImportFilter() error {
return err
}

h.server.lock.Lock()
defer h.server.lock.Unlock()
h.server._databasesLock.Lock()
defer h.server._databasesLock.Unlock()

// TODO: Dynamic update instead of reload
if err := h.server._reloadDatabaseWithConfig(h.ctx(), *updatedDbConfig, false, false); err != nil {
Expand Down Expand Up @@ -1542,7 +1542,7 @@ func (h *handler) handleGetStatus() error {
status.Vendor.Version = base.ProductAPIVersion
}

for _, database := range h.server.databases_ {
for _, database := range h.server._databases {
lastSeq := uint64(0)
runState := db.RunStateString[atomic.LoadUint32(&database.State)]

Expand Down
6 changes: 3 additions & 3 deletions rest/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2989,7 +2989,7 @@ func TestCreateDBWithXattrsDisabled(t *testing.T) {
resp = rt.CreateDatabase(dbName, dbConfig)
RequireStatus(t, resp, http.StatusCreated)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line should be a lock but actually i'd just change the test so it makes a different REST api call

I don't want to accidentally have the race detector pick up this error because we are doing something naughty in the tests and config polling runs simultaneously.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test case isn't quite a regular /db/_config operation - it's effectively doing a persistent config load of an old configuration - bypassing the upfront validation that /db/_config would've run.

Either way, I can move it to a function to wrap in a read mutex (the read lock is required to read the dbName entry, but nothing more to update the underlying configuration)

rt.RestTesterServerContext.dbConfigs[dbName].DatabaseConfig.EnableXattrs = base.Ptr(false)
rt.RestTesterServerContext._dbConfigs[dbName].DatabaseConfig.EnableXattrs = base.Ptr(false)

_, err := rt.RestTesterServerContext.ReloadDatabase(t.Context(), dbName, false)
assert.Error(t, err, errResp)
Expand Down Expand Up @@ -3482,7 +3482,7 @@ func TestAllowConflictsConfig(t *testing.T) {
bucketName := rt.GetDatabase().Bucket.GetName()

// Attempt to set the `AllowConflicts` property to true in the database configuration.
rt.RestTesterServerContext.dbConfigs[dbName].DatabaseConfig.DbConfig.AllowConflicts = base.Ptr(true)
rt.RestTesterServerContext._dbConfigs[dbName].DatabaseConfig.DbConfig.AllowConflicts = base.Ptr(true)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above about potential race and just use /db/_config API

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, as above - this test is ensuring coverage of loading an older pre 4.0 config, where AllowConflicts was allowed to be set. I cannot set this value through the REST API anymore (intentionally so)

I have created a named function to do this to make it more obvious why it's being done this way.


// Reload the database configuration and verify that an error is returned.
_, err := rt.RestTesterServerContext.ReloadDatabase(ctx, dbName, false)
Expand Down Expand Up @@ -3525,7 +3525,7 @@ func TestDisableAllowStarChannel(t *testing.T) {
RequireStatus(t, resp, http.StatusCreated)

// Attempting to disable `enable_star_channel`
rt.ServerContext().dbConfigs[dbName].DatabaseConfig.CacheConfig.ChannelCacheConfig.EnableStarChannel = base.Ptr(false)
rt.ServerContext()._dbConfigs[dbName].DatabaseConfig.CacheConfig.ChannelCacheConfig.EnableStarChannel = base.Ptr(false)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, use REST API

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto


// Reloading the database after updating the config
_, err := rt.ServerContext().ReloadDatabase(ctx, dbName, false)
Expand Down
50 changes: 25 additions & 25 deletions rest/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1657,21 +1657,21 @@ func (sc *ServerContext) fetchAndLoadConfigs(ctx context.Context, isInitialStart
// we don't need to do this two-stage lock on initial startup as the REST APIs aren't even online yet.
var deletedDatabases []string
if !isInitialStartup {
sc.lock.RLock()
for dbName, _ := range sc.dbRegistry {
sc._databasesLock.RLock()
for dbName, _ := range sc._dbRegistry {
if _, foundMatchingDb := fetchedConfigs[dbName]; !foundMatchingDb {
deletedDatabases = append(deletedDatabases, dbName)
delete(fetchedConfigs, dbName)
}
}
for dbName, fetchedConfig := range fetchedConfigs {
if dbConfig, ok := sc.dbConfigs[dbName]; ok && dbConfig.cfgCas >= fetchedConfig.cfgCas {
if dbConfig, ok := sc._dbConfigs[dbName]; ok && dbConfig.cfgCas >= fetchedConfig.cfgCas {
sc.invalidDatabaseConfigTracking.remove(dbName)
base.DebugfCtx(ctx, base.KeyConfig, "Database %q bucket %q config has not changed since last update", fetchedConfig.Name, *fetchedConfig.Bucket)
delete(fetchedConfigs, dbName)
}
}
sc.lock.RUnlock()
sc._databasesLock.RUnlock()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to file a separate ticket for this, if there was a panic between this line and and the readLock, then I want this to unlock.

If the panic happens in config polling, I believe that this will take down Sync Gateway. However, this can get called from any db scoped REST API handler:

GetInactiveDatabase (if serverless) -> FetchAndLoadDatabaseSince -> fetchAndLoadConfigs

Since this is only serverless, this is low priority.

For the issue that spawned this ticket (see https://jira.issues.couchbase.com/browse/CBG-5010 for the full logs), something locked the serverContext.lock mutex but didn't unlock it. I don't think it's this line but this is one of the only cases where this is called outside a defer.

I think it would be fine to do this as part of a separate defensive ticket.

Copy link
Member Author

@bbrks bbrks Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a ticket to review usage of the lock. This ticket is simply splitting out the http server lock.

I tested this by running /_ping and /db/_config in a loop to ensure that the db config update does not interfere with the ping response (which it now does not)

Will review with the other larger sc._databasesLock changes in CBG-5012


// nothing to do, we can bail out without needing the write lock
if len(deletedDatabases) == 0 && len(fetchedConfigs) == 0 {
Expand All @@ -1681,10 +1681,10 @@ func (sc *ServerContext) fetchAndLoadConfigs(ctx context.Context, isInitialStart
}

// we have databases to update/remove
sc.lock.Lock()
defer sc.lock.Unlock()
sc._databasesLock.Lock()
defer sc._databasesLock.Unlock()
for _, dbName := range deletedDatabases {
dbc, ok := sc.databases_[dbName]
dbc, ok := sc._databases[dbName]
if !ok {
base.DebugfCtx(ctx, base.KeyConfig, "Database %q already removed from server context after acquiring write lock - do not need to remove not removing database", base.MD(dbName))
continue
Expand Down Expand Up @@ -1723,8 +1723,8 @@ func (sc *ServerContext) fetchAndLoadDatabaseSince(ctx context.Context, dbName s
}

func (sc *ServerContext) fetchAndLoadDatabase(nonContextStruct base.NonCancellableContext, dbName string, forceReload bool) (found bool, err error) {
sc.lock.Lock()
defer sc.lock.Unlock()
sc._databasesLock.Lock()
defer sc._databasesLock.Unlock()
return sc._fetchAndLoadDatabase(nonContextStruct, dbName, forceReload)
}

Expand Down Expand Up @@ -1809,8 +1809,8 @@ func (sc *ServerContext) findBucketWithCallback(ctx context.Context, callback fu

func (sc *ServerContext) fetchDatabase(ctx context.Context, dbName string) (found bool, dbConfig *DatabaseConfig, err error) {
// fetch will update the databses
sc.lock.Lock()
defer sc.lock.Unlock()
sc._databasesLock.Lock()
defer sc._databasesLock.Unlock()
return sc._fetchDatabase(ctx, dbName)
}

Expand Down Expand Up @@ -1880,8 +1880,8 @@ func (sc *ServerContext) _fetchDatabase(ctx context.Context, dbName string) (fou
}

func (sc *ServerContext) handleInvalidDatabaseConfig(ctx context.Context, bucket string, cnf DatabaseConfig) {
sc.lock.Lock()
defer sc.lock.Unlock()
sc._databasesLock.Lock()
defer sc._databasesLock.Unlock()
sc._handleInvalidDatabaseConfig(ctx, bucket, cnf, nil)
}

Expand All @@ -1895,9 +1895,9 @@ func (sc *ServerContext) _handleInvalidDatabaseConfig(ctx context.Context, bucke
func (sc *ServerContext) bucketNameFromDbName(ctx context.Context, dbName string) (bucketName string, found bool) {
// Minimal representation of config struct to be tolerant of invalid database configurations where we still need to find a database name
// see if we find the database in-memory first, otherwise fall back to scanning buckets for db configs
sc.lock.RLock()
dbc, ok := sc.databases_[dbName]
sc.lock.RUnlock()
sc._databasesLock.RLock()
dbc, ok := sc._databases[dbName]
sc._databasesLock.RUnlock()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is so unlikely that this would panic, but due to excessive paranoia I am almost tempted to wrap this in a function in case sc._databases was nil when this function was called in a case where panics are handled.

Feel free to disregard.

Copy link
Member Author

@bbrks bbrks Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will review with the other larger sc._databasesLock changes in CBG-5012


if ok {
return dbc.Bucket.GetName(), true
Expand Down Expand Up @@ -1948,7 +1948,7 @@ func (sc *ServerContext) fetchConfigsSince(ctx context.Context, refreshInterval
sc.fetchConfigsLastUpdate = time.Now()
}

return sc.dbConfigs, nil
return sc._dbConfigs, nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is weird because it returns a value that would require you to hold a lock. This behavior should be noted in the docstring and considered.

I didn't look through the implications of this.

Copy link
Member Author

@bbrks bbrks Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree this is weird and seems wrong - will review with the other larger sc._databasesLock changes in CBG-5012

}

// GetBucketNames returns a slice of the bucket names associated with the server context
Expand Down Expand Up @@ -2064,8 +2064,8 @@ func (sc *ServerContext) _applyConfigs(ctx context.Context, dbNameConfigs map[st
}

func (sc *ServerContext) applyConfigs(ctx context.Context, dbNameConfigs map[string]DatabaseConfig) (count int) {
sc.lock.Lock()
defer sc.lock.Unlock()
sc._databasesLock.Lock()
defer sc._databasesLock.Unlock()
return sc._applyConfigs(ctx, dbNameConfigs, false, false)
}

Expand Down Expand Up @@ -2104,13 +2104,13 @@ func (sc *ServerContext) _applyConfig(nonContextStruct base.NonCancellableContex
}

// skip if we already have this config loaded, and we've got a cas value to compare with
_, exists := sc.dbRegistry[cnf.Name]
_, exists := sc._dbRegistry[cnf.Name]
if exists {
if cnf.cfgCas == 0 {
// force an update when the new config's cas was set to zero prior to load
base.InfofCtx(ctx, base.KeyConfig, "Forcing update of config for database %q bucket %q", cnf.Name, *cnf.Bucket)
} else {
if sc.dbConfigs[cnf.Name].cfgCas >= cnf.cfgCas {
if sc._dbConfigs[cnf.Name].cfgCas >= cnf.cfgCas {
base.DebugfCtx(ctx, base.KeyConfig, "Database %q bucket %q config has not changed since last update", cnf.Name, *cnf.Bucket)
return false, nil
}
Expand Down Expand Up @@ -2211,8 +2211,8 @@ func StartServer(ctx context.Context, config *StartupConfig, sc *ServerContext)
}

func sharedBucketDatabaseCheck(ctx context.Context, sc *ServerContext) (errors error) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we lock this function or rename to _sharedBucketDatabaseCheck and note which lock you are required to hold?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will review with the other larger sc._databasesLock changes in CBG-5012

bucketUUIDToDBContext := make(map[string][]*db.DatabaseContext, len(sc.databases_))
for _, dbContext := range sc.databases_ {
bucketUUIDToDBContext := make(map[string][]*db.DatabaseContext, len(sc._databases))
for _, dbContext := range sc._databases {
if uuid, err := dbContext.Bucket.UUID(); err == nil {
bucketUUIDToDBContext[uuid] = append(bucketUUIDToDBContext[uuid], dbContext)
}
Expand Down Expand Up @@ -2272,15 +2272,15 @@ func (sc *ServerContext) _findDuplicateCollections(cnf DatabaseConfig) []string
// If scopes aren't defined, check the default collection
if cnf.Scopes == nil {
defaultFQName := base.FullyQualifiedCollectionName(*cnf.Bucket, base.DefaultScope, base.DefaultCollection)
existingDbName, ok := sc.collectionRegistry[defaultFQName]
existingDbName, ok := sc._collectionRegistry[defaultFQName]
if ok && existingDbName != cnf.Name {
duplicatedCollections = append(duplicatedCollections, defaultFQName)
}
} else {
for scopeName, scope := range cnf.Scopes {
for collectionName, _ := range scope.Collections {
fqName := base.FullyQualifiedCollectionName(*cnf.Bucket, scopeName, collectionName)
existingDbName, ok := sc.collectionRegistry[fqName]
existingDbName, ok := sc._collectionRegistry[fqName]
if ok && existingDbName != cnf.Name {
duplicatedCollections = append(duplicatedCollections, fqName)
}
Expand Down
6 changes: 3 additions & 3 deletions rest/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2978,7 +2978,7 @@ func TestInvalidDbConfigNoLongerPresentInBucket(t *testing.T) {
require.EventuallyWithT(t, func(c *assert.CollectT) {
invalidDatabases := rt.ServerContext().AllInvalidDatabaseNames(t)
assert.Equal(c, 1, len(invalidDatabases))
assert.Equal(c, 0, len(rt.ServerContext().dbConfigs))
assert.Equal(c, 0, len(rt.ServerContext()._dbConfigs))
}, time.Second*10, time.Millisecond*100)

// remove the invalid config from the bucket
Expand All @@ -2991,7 +2991,7 @@ func TestInvalidDbConfigNoLongerPresentInBucket(t *testing.T) {
require.EventuallyWithT(t, func(c *assert.CollectT) {
invalidDatabases := rt.ServerContext().AllInvalidDatabaseNames(t)
assert.Equal(c, 0, len(invalidDatabases))
assert.Equal(c, 0, len(rt.ServerContext().dbConfigs))
assert.Equal(c, 0, len(rt.ServerContext()._dbConfigs))
}, time.Second*10, time.Millisecond*100)

// create db again, should succeed
Expand Down Expand Up @@ -3064,7 +3064,7 @@ func TestNotFoundOnInvalidDatabase(t *testing.T) {
require.EventuallyWithT(t, func(c *assert.CollectT) {
invalidDatabases := rt.ServerContext().AllInvalidDatabaseNames(t)
assert.Equal(c, 0, len(invalidDatabases))
assert.Equal(c, 1, len(rt.ServerContext().dbConfigs))
assert.Equal(c, 1, len(rt.ServerContext()._dbConfigs))
}, time.Second*10, time.Millisecond*100)
}

Expand Down
4 changes: 2 additions & 2 deletions rest/handler_config_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ func (h *handler) mutateDbConfig(mutator func(*DbConfig) error) error {
return err
}

h.server.lock.Lock()
defer h.server.lock.Unlock()
h.server._databasesLock.Lock()
defer h.server._databasesLock.Unlock()

// TODO: Dynamic update instead of reload
if err := h.server._reloadDatabaseWithConfig(h.ctx(), *updatedDbConfig, false, false); err != nil {
Expand Down
Loading
Loading