Skip to content

CBG-5472: Make the stats logger independent of the database lock#8431

Open
bbrks wants to merge 2 commits into
mainfrom
CBG-5472
Open

CBG-5472: Make the stats logger independent of the database lock#8431
bbrks wants to merge 2 commits into
mainfrom
CBG-5472

Conversation

@bbrks

@bbrks bbrks commented Jul 6, 2026

Copy link
Copy Markdown
Member

CBG-5472

The stats logger could stall, leaving gaps in sg_stats.log, whenever a database config update held a lock the stats path also needed. Two independent locks were involved:

  • _databasesLock: updateCalculatedStats took RLock, but a config update holds the write lock across an infinite index-readiness wait, blocking the reader for the whole (unbounded) window.
  • base.SgwStats.dbStatsMapMutex: String() held it for the entire marshal, and NewDBStats/ClearDBStats held it for all Prometheus (un)registration under _databasesLock.

Decouple the reader from both:

  • ServerContext now keeps a lock-free atomic snapshot of the database contexts, refreshed under _databasesLock at each _databases mutation. updateCalculatedStats reads the snapshot, never the lock.
  • dbStatsMapMutex is now held only for the map operation: String() marshals a shallow copy, and NewDBStats/ClearDBStats do their Prometheus work outside the lock. The unregister* calls only touch Prometheus (not the DbStats maps), so this stays race-free.

The stats ticker now keeps emitting (stale-but-present) while databases are added, removed or reloaded.

Pre-review checklist

  • Removed debug logging (fmt.Print, log.Print, ...)
  • Logging sensitive data? Make sure it's tagged (e.g. base.UD(docID), base.MD(dbName))
  • Updated relevant information in the API specifications (such as endpoint descriptions, schemas, ...) in docs/api
  • Self-review @bbrks

Dependencies (if applicable)

Integration Tests

@bbrks bbrks self-assigned this Jul 6, 2026
Base automatically changed from CBG-3658 to main July 7, 2026 13:41
The stats logger could stall, leaving gaps in sg_stats.log, whenever a
database config update held a lock the stats path also needed. Two
independent locks were involved:

- _databasesLock: updateCalculatedStats took RLock, but a config update
  holds the write lock across an infinite index-readiness wait, blocking
  the reader for the whole (unbounded) window.
- base.SgwStats.dbStatsMapMutex: String() held it for the entire marshal,
  and NewDBStats/ClearDBStats held it for all Prometheus (un)registration
  under _databasesLock.

Decouple the reader from both:

- ServerContext now keeps a lock-free atomic snapshot of the database
  contexts, refreshed under _databasesLock at each _databases mutation.
  updateCalculatedStats reads the snapshot, never the lock.
- dbStatsMapMutex is now held only for the map operation: String()
  marshals a shallow copy, and NewDBStats/ClearDBStats do their Prometheus
  work outside the lock. The unregister* calls only touch Prometheus (not
  the DbStats maps), so this stays race-free.

The stats ticker now keeps emitting (stale-but-present) while databases
are added, removed or reloaded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 7, 2026 13:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses CBG-5472 by ensuring periodic stats logging cannot stall behind long-held database/config locks, keeping sg_stats.log emission continuous (potentially stale, but present) during database add/remove/reload and index readiness waits.

Changes:

  • Added a lock-free atomic snapshot of database contexts in ServerContext, so updateCalculatedStats never blocks on _databasesLock.
  • Narrowed base.SgwStats.dbStatsMapMutex critical sections: String() marshals a shallow copy, and NewDBStats/ClearDBStats only lock for map mutation (Prometheus work outside the lock).
  • Added unit tests covering lock-independence and concurrent stats serialization/DB registration/removal scenarios.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
rest/server_context.go Maintains an atomic snapshot of DB contexts and updates calculated stats from the snapshot instead of _databasesLock.
rest/server_context_test.go Adds regression tests ensuring stats calculation doesn’t block on _databasesLock, and remains safe during concurrent removal.
base/stats.go Narrows dbStatsMapMutex usage and performs serialization/Prometheus operations outside the map lock.
base/stats_test.go Adds tests for serialization behavior across Clear/New and concurrent String()/DB registration/removal.

Comment thread base/stats.go
Comment on lines +1458 to 1463
dbStats.unregisterCacheStats()
dbStats.unregisterCBLReplicationPullStats()
dbStats.unregisterCBLReplicationPushStats()
for replName := range dbStats.DbReplicatorStats {
dbStats.unregisterReplicationStats(replName)
}
Comment thread base/stats_test.go
Comment on lines +224 to +226
func TestStatsSerializationConcurrentWithDBRegistration(t *testing.T) {
stats := &SgwStats{DbStats: map[string]*DbStats{}}

Comment thread base/stats_test.go
Comment on lines +213 to +218
_, err := stats.NewDBStats(dbName, false, false, false, false, nil, nil)
require.NoError(t, err)
require.Contains(t, stats.String(), dbName)

stats.ClearDBStats(dbName)
require.NotContains(t, stats.String(), dbName)
Comment on lines +410 to +414
select {
case <-done:
case <-time.After(10 * time.Second):
t.Fatal("updateCalculatedStats blocked on _databasesLock held by a simulated config update (CBG-5472)")
}
Guard ClearDBStats's iteration of the lazily-written DbReplicatorStats map
with dbReplicatorStatsMutex. Replications register their stats at runtime via
DBReplicatorStats() (writing that map under the mutex), which can overlap a
database teardown; iterating without the mutex could trigger a fatal
"concurrent map read and map write". This race is pre-existing (not introduced
by CBG-5472) but is in-theme and cheap to fix here. Adds a regression test that
reproduces the fatal map race without the fix.

Also address reviewer feedback:
- Make TestStatsSerializationConcurrentWithDBRegistration exercise the real
  Prometheus (un)registration path (the base TestMain forces
  SkipPrometheusStatsRegistration=true), restoring the flag afterwards.
- defer ClearDBStats cleanup in TestClearDBStatsRemovesFromSerialization so an
  early assertion failure can't leave stats registered.
- Shorten the lock-independence regression timeout from 10s to 2s.
- Document the SgwStats shallow-copy field-drift hazard in String().
- Document the _databases/databasesSnapshot sync invariant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants