Repository navigation
Conversation
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The concurrency paths are consistently guarded and covered by focused and cluster-level tests.
Review effort: Balanced
Findings: None
What changed in this PR
Improves persistent database configuration safety during concurrent registry writes and multi-node operations.
Changes:
- Reuses update callbacks across registry-only CAS retries.
- Detects stale registry versions and safely reloads.
- Adds concurrency hooks, safeguards, and multi-node tests.
| File | Description |
|---|---|
rest/config_manager.go |
Handles concurrent registry changes safely. |
rest/admin_api.go |
Safely updates loaded config CAS. |
rest/utilities_testing.go |
Adds bucket connection hook and template shortcut. |
rest/utilities_testing_rest_tester_cluster.go |
Propagates connection hooks across nodes. |
rest/config_manager_test.go |
Tests stale registry handling. |
rest/config_concurrency_test.go |
Exercises randomized multi-node operations. |
rest/cluster_compat_test.go |
Tests heartbeat contention during updates. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Droid finished @torcolvin's task —— View job Phase 2 validation complete. No inline comments to post. LGTM: the concurrency fixes match the PR description and the new tests cover the reported edge cases. |
…y writers
1. UpdateConfig: when a registry write fails on a CAS mismatch, for example
because a peer heartbeat wrote the registry, reuse the callback result as
long as the config's cfgCas has not changed. Before this, the retry ran the
callback again, which reloaded the database again.
2. getConfigVersionWithRetry: if the config is missing or its version does
not match, check the registry before retrying. If the registry no longer has
the requested version, for example after a concurrent delete, return
ErrConfigRegistryReloadRequired right away instead of waiting until the
timeout and rolling back.
3. getDatabaseConfig: if the config is newer than the registry the caller
read, return ErrConfigRegistryReloadRequired instead of failing. This
happens when a concurrent update wrote both after that read.
4. handlePutDbConfig: set cfgCas only if the database is still loaded with
the updated version. A concurrent request or the config poller can remove or
reload the database. Before this, a removed database caused a nil map
access.
5. RestTester: add ConnectToBucketFn so that tests can hook database reloads.
RestTesterCluster passes it to every node. It cannot be used together with
LeakyBucketConfig.
6. RestTester.templateResource: skip template parsing and the database lookup
when the resource contains no template. The lookup can race with a
concurrent database delete.
7. Add tests:
- TestUpdateDbConfigWithConcurrentPeerHeartbeats: an update succeeds and
reloads the database only once when a heartbeat runs during every reload.
- TestGetDatabaseConfigNewerThanRegistry and
TestGetDatabaseConfigDeletedAfterRegistryRead: cover the reload-required
paths.
- TestConcurrentDbConfigOperationsAcrossNodes: sends random config
operations to a 3-node cluster, then checks that every node and the
registry end up with the same config.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…baseConfigDeletedAfterRegistryRead Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bbrks
left a comment
There was a problem hiding this comment.
All of my comments are on the new random concurrency test which needs to be skipped anyway until a follow-up fix is made to avoid the use of closed bucket panic.
Can roll these comments forwards into the PR to re-enable the test?
| weight: 2, | ||
| allowed: []int{http.StatusOK, http.StatusNotFound}, | ||
| sendFunc: func(rt *RestTester, rng *rand.Rand) *TestResponse { | ||
| return rt.SendAdminRequest(http.MethodPut, "/db/_config/sync", fmt.Sprintf(`function(doc) { channel("ch%d"); }`, rng.IntN(10))) |
There was a problem hiding this comment.
This endpoint uses a full keyspace not db (sync functions are collection specific)
| weight: 2, | ||
| allowed: []int{http.StatusCreated, http.StatusPreconditionFailed}, | ||
| sendFunc: func(rt *RestTester, _ *rand.Rand) *TestResponse { | ||
| return rt.SendAdminRequest(http.MethodPut, "/db/", string(base.MustJSONMarshal(rt.TB(), rt.NewDbConfig()))) |
There was a problem hiding this comment.
should this be using a placeholder instead of db?
| sendFunc func(rt *RestTester, rng *rand.Rand) *TestResponse | ||
| } | ||
|
|
||
| func dbConfigOperations() []dbConfigOperation { |
There was a problem hiding this comment.
I don't think I'd expect this to be used for another test, so having standalone builder functions like this (Claude tends to gravitate towards this?) means we're just polluting the rest package APIs really
| // dbConfigOperation is one admin API operation that TestConcurrentDbConfigOperationsAcrossNodes sends to a random node. | ||
| type dbConfigOperation struct { |
There was a problem hiding this comment.
inline struct if it's test specific
| reloadDelay = 150 * time.Millisecond // longer than pollInterval, as a Couchbase Server backed reload is in a real cluster | ||
| convergeTimeout = 30 * time.Second | ||
| ) | ||
| seed := rand.Uint64() |
There was a problem hiding this comment.
It would be nice if this randomness was an injectable seed so it could be deterministically re-run. It's not too easy to grab shuffle value when -shuffle=on is used - but it is possible (via TestMain) which might be nice?
// testing keeps the seed it picks for -shuffle=on to itself. Choose our own value here instead so tests can read and a run can be repeated with -shuffle=N.
flag.Parse()
if f := flag.Lookup("test.shuffle"); f != nil && f.Value.String() == "on" {
if err := flag.Set("test.shuffle", strconv.FormatInt(time.Now().UnixNano(), 10)); err != nil {
FatalfCtx(ctx, "could not set test.shuffle: %v", err)
}
}…-5935 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CBG-5909
Stacked on #8834 (CBG-5731). Merge that first; this PR's base is
CBG-5731.I wrote a
config_concurrency_test.goto flush out the missing changes.Makes database config updates safe against concurrent registry writers. Each section shows the operations that break, then the fix.
1. UpdateConfig reloads the database again on each retry
POST /db/_configreads the registry.Fix: On a retry, node1 reuses the result of the earlier callback if the config's
cfgCasdid not change, so only the registry write is retried.2. Config read waits and rolls back after a concurrent delete
POST /db/_configreads the registry and sees version v1.DELETE /db/removes the database from the registry and deletes its config.configRetryTimeoutends, then rolls back the registry.Fix: When the config is missing or has the wrong version, node1 reads the registry again, and if v1 is gone it returns
ErrConfigRegistryReloadRequiredright away.3. Config read fails when the config is newer than the registry
POST /db/_configreads the registry and sees version v1.POST /db/_configwrites v2 to the registry and to the config.ErrConfigVersionMismatch.Fix:
getDatabaseConfigchangesErrConfigVersionMismatchtoErrConfigRegistryReloadRequired, so node1 reads the registry again and continues with v2.4. handlePutDbConfig sets cfgCas on a database that was removed or reloaded
PUT /db/_configwrites the config and reloads the database.DELETE /db/deletes the database._dbConfigs._dbConfigs["db"].cfgCas, gets a nil entry, and panics.In a second version of this problem, node2 sends
POST /db/_configat step 2 and the poller on node1 loads the newer version at step 3. At step 4, node1 then writes its old CAS onto the newer config.Fix: The handler sets
cfgCasonly if the database is still in_dbConfigswith the same version that the handler wrote.5. Test hook
RestTesterConfig.ConnectToBucketFnlets tests hook database reloads.RestTesterClusterpasses it to every node. It cannot be used together withLeakyBucketConfig.6. Test requests fail when a concurrent delete removes the database
DELETE /db/.rt.SendAdminRequestwith/db/_config.templateResourcelooks up the database to fill in the template, and the lookup fails or races with the delete.Fix:
templateResourcereturns the path unchanged when it contains no{{, so it does not look up the database.7. Tests
TestUpdateDbConfigWithConcurrentPeerHeartbeatscovers case 1: a heartbeat runs during every reload, and the update must succeed with one reload.TestGetDatabaseConfigDeletedAfterRegistryReadcovers case 2, andTestGetDatabaseConfigNewerThanRegistrycovers case 3.TestConcurrentDbConfigOperationsAcrossNodessends random config operations to a 3-node cluster, then checks that every node and the registry end up with the same config.TestConcurrentDbConfigOperationsAcrossNodesdepends on #8834: without it, a concurrentDELETE /db/returns 500 in about 1 of 5 runs. With #8834, it passed 45/45 runs with-race.Pre-review checklist
base.UD(docID),base.MD(dbName))docs/apiDependencies (if applicable)
Integration Tests
🤖 Generated with Claude Code