Skip to content

Conversation

@bbrks
Copy link
Member

@bbrks bbrks commented Nov 28, 2025

CBG-5032

Adds a new shared mutex on RevCache Value that is shared by concurrent GetDelta callers to syncronise delta generation between multiple clients on the same fromRev.

Benchmark:

$ benchstat main.bench 5032.bench
goos: darwin
goarch: arm64
pkg: github.com/couchbase/sync_gateway/db
cpu: Apple M1 Pro
                                                                 │     main.bench      │             5032.bench              │
                                                                 │       sec/op        │    sec/op     vs base               │
DeltaSyncConcurrentClientCachePopulation/100KBDoc_100Clients-10        140.9µ ±    24%   134.3µ ± 73%        ~ (p=1.000 n=6)
DeltaSyncConcurrentClientCachePopulation/100KBDoc_1000Clients-10      1585.2µ ± 63266%   953.5µ ± 25%  -39.85% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/100KBDoc_5000Clients-10       6.621m ±   486%   4.091m ± 15%  -38.22% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/5MBDoc_10Clients-10           13.74µ ±    14%   14.21µ ±  4%        ~ (p=0.132 n=6)
DeltaSyncConcurrentClientCachePopulation/5MBDoc_100Clients-10         222.12µ ±    13%   94.23µ ± 12%  -57.57% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/5MBDoc_1000Clients-10     5732323.2µ ±    51%   754.7µ ±  3%  -99.99% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/5MBDoc_5000Clients-10      7395.462m ±    87%   3.767m ±  3%  -99.95% (p=0.002 n=6)
geomean                                                                5.682m            411.4µ        -92.76%

                                                                 │       main.bench       │              5032.bench              │
                                                                 │          B/op          │     B/op      vs base                │
DeltaSyncConcurrentClientCachePopulation/100KBDoc_100Clients-10        155.21Ki ±     17%   61.87Ki ± 1%   -60.14% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/100KBDoc_1000Clients-10       5237.2Ki ± 144638%   616.1Ki ± 0%   -88.24% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/100KBDoc_5000Clients-10       19.262Mi ±   1598%   3.004Mi ± 0%   -84.40% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/5MBDoc_10Clients-10           15.783Ki ±     12%   7.080Ki ± 0%   -55.14% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/5MBDoc_100Clients-10         1592.98Ki ±     13%   67.08Ki ± 2%   -95.79% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/5MBDoc_1000Clients-10     67408876.2Ki ±     51%   659.3Ki ± 0%  -100.00% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/5MBDoc_5000Clients-10      79174.452Mi ±     83%   3.219Mi ± 0%  -100.00% (p=0.002 n=6)
geomean                                                                 21.11Mi             275.7Ki        -98.72%

                                                                 │    main.bench    │              5032.bench              │
                                                                 │    allocs/op     │  allocs/op   vs base                 │
DeltaSyncConcurrentClientCachePopulation/100KBDoc_100Clients-10       702.0 ±    0%    701.0 ± 0%   -0.14% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/100KBDoc_1000Clients-10     7.078k ± 1745%   7.003k ± 0%   -1.06% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/100KBDoc_5000Clients-10     35.30k ±   14%   35.02k ± 0%   -0.81% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/5MBDoc_10Clients-10          71.00 ±    0%    71.00 ± 0%        ~ (p=1.000 n=6) ¹
DeltaSyncConcurrentClientCachePopulation/5MBDoc_100Clients-10         703.5 ±    0%    701.0 ± 0%   -0.36% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/5MBDoc_1000Clients-10     137.184k ±   49%   7.001k ± 0%  -94.90% (p=0.002 n=6)
DeltaSyncConcurrentClientCachePopulation/5MBDoc_5000Clients-10      193.38k ±   70%   35.02k ± 0%  -81.89% (p=0.002 n=6)
geomean                                                              5.843k           2.982k       -48.96%
¹ all samples are equal

Integration Tests

Copy link
Contributor

Copilot AI left a comment

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 adds a shared mutex (deltaLock) to the revision cache value structure to synchronize delta generation between multiple clients requesting the same fromRev. This prevents redundant delta computation when multiple clients concurrently request deltas for the same source revision.

Key changes:

  • Added sync.RWMutex to revCacheValue struct to coordinate delta generation across concurrent requests
  • Modified GetDelta function to acquire read lock first (checking for cached delta), then upgrade to write lock if delta generation is needed
  • Added comprehensive test and benchmark suite demonstrating up to 99.99% performance improvement in high-concurrency scenarios

Reviewed changes

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

File Description
db/revision_cache_lru.go Added deltaLock field to revCacheValue struct and exposed it through DocumentRevision
db/revision_cache_interface.go Added RevCacheValueDeltaLock field to DocumentRevision struct
db/crud.go Refactored GetDelta to use read/write locking pattern for synchronized delta generation
db/database_test.go Added test and benchmark cases for concurrent delta generation scenarios

@adamcfraser adamcfraser changed the title Add revcache value lock for syncronized delta generation Add revcache value lock for synchronized delta generation Nov 28, 2025
@adamcfraser adamcfraser changed the title Add revcache value lock for synchronized delta generation CBG-5032 Add revcache value lock for synchronized delta generation Nov 28, 2025
db/crud.go Outdated
if fromRevision.BodyBytes == nil && fromRevision.Delta == nil {
return nil, nil, err
// locking ensures clients requesting deltas sharing the same from revision memoize the work to generate the delta and share the cached result
initialFromRevision.RevCacheValueDeltaLock.RLock()
Copy link
Collaborator

Choose a reason for hiding this comment

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

We already have separate synchronization for actually updating the delta in the rev cache (in UpdateDelta) - I believe the intention is that DocumentRevision.Delta points to an immutable delta, and in UpdateDelta a new delta is stored and referenced. Given that, do we actually need to acquire a read lock on RevCacheValueDeltaLock, if the only intention is to prevent concurrent delta computation? Is it sufficient to just acquire the write lock before starting the work to fetch/compute the delta?

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree with this analysis, thanks. Dropped the lock to a regular mutex and dropped the read lock before we move on to write lock when needed.

db/crud.go Outdated
var fromRevisionForDiff DocumentRevision

isAuthorized, redactedBody := db.authorizeUserForChannels(docID, toRev, fromRevision.CV, fromRevision.Delta.ToChannels, fromRevision.Delta.ToDeleted, encodeRevisions(ctx, docID, fromRevision.Delta.RevisionHistory))
// check the revcache again for a delta - it's possible another writer generated it while we were waiting for the write lock
Copy link
Collaborator

Choose a reason for hiding this comment

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

It looks like the only performance overhead of this change is this additional rev cache Get the first time the delta is computed. In a cases where only one reader is using the delta this will be a small amount of additional overhead, but I expect the cache fetch (which should always hit) will be trivial compared to delta computation in the first place. Agree?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yea, agreed.

I did write BenchmarkDeltaSyncSingleClientCachePopulation just to check, which just runs GetDelta over 1000 documents and there was no difference between main and with the double cache fetch.

~4ms per GetDelta computation in both cases and no difference in heap allocs.

$ benchstat main-1000docs1client.bench 5032-1000docs1client.bench
goos: darwin
goarch: arm64
pkg: github.com/couchbase/sync_gateway/db
cpu: Apple M1 Pro
                                                 │ main-1000docs1client.bench │   5032-1000docs1client.bench   │
                                                 │           sec/op           │   sec/op     vs base           │
DeltaSyncSingleClientCachePopulation/100KBDoc-10                  4.173 ± ∞ ¹   4.176 ± ∞ ¹  ~ (p=1.000 n=1) ²
¹ need >= 6 samples for confidence interval at level 0.95
² need >= 4 samples to detect a difference at alpha level 0.05

                                                 │ main-1000docs1client.bench │    5032-1000docs1client.bench    │
                                                 │            B/op            │     B/op       vs base           │
DeltaSyncSingleClientCachePopulation/100KBDoc-10                7.199Gi ± ∞ ¹   7.200Gi ± ∞ ¹  ~ (p=1.000 n=1) ²
¹ need >= 6 samples for confidence interval at level 0.95
² need >= 4 samples to detect a difference at alpha level 0.05

                                                 │ main-1000docs1client.bench │   5032-1000docs1client.bench    │
                                                 │         allocs/op          │  allocs/op    vs base           │
DeltaSyncSingleClientCachePopulation/100KBDoc-10                 127.4k ± ∞ ¹   127.4k ± ∞ ¹  ~ (p=1.000 n=1) ²
¹ need >= 6 samples for confidence interval at level 0.95
² need >= 4 samples to detect a difference at alpha level 0.05

@bbrks bbrks assigned bbrks and unassigned adamcfraser Dec 1, 2025
@adamcfraser adamcfraser merged commit f175922 into main Dec 1, 2025
42 checks passed
@adamcfraser adamcfraser deleted the CBG-5032 branch December 1, 2025 16:56
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.

3 participants