couchbase / couchbase/sync_gateway
`_purge` only evicts the channel cache on the node that served the request; other nodes keep serving purged doc IDs from `_changes`
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 455
- Forks
- 145
- Avg merge
- 2d 1m
- Merged PRs (30d)
- 143
Description
Version
Couchbase Sync Gateway 3.3.5.1 EE, running as Capella App Services (multi-node
endpoint behind a load balancer). Server: Couchbase Sync Gateway/3.3.5.1 EE.
Summary
POST /{keyspace}/_purge evicts the purged doc IDs from the channel cache of the
node that handles the request, but not from any other node in the cluster. Those
nodes keep returning the purged IDs from _changes indefinitely. A client that
follows the feed gets a doc ID that GET /{keyspace}/{docid} answers 404 for.
Steps to reproduce
Against a multi-node deployment (2 nodes is enough):
- Write a few docs into channel
X. GET /{keyspace}/_changes?filter=sync_gateway/bychannel&channels=X&active_only=true
repeatedly until every node has warmed its cache forX.POST /{keyspace}/_purgewith{"<docid>":["*"]}for one of them. Response is
200and lists the doc as purged.GET /{keyspace}/<docid>— consistently404/ error code1404, from every
node. The document really is gone.- Repeat the
_changesrequest from step 2 many times.
Actual
The purged ID is still returned by roughly half the _changes requests — whichever
node did not serve the purge. Ten identical requests in my case:
result-count distribution over 10 identical _changes queries: {21: 5, 7: 5}
Seven is correct. The 21-row responses contain 14 IDs that were purged (two earlier
batches), every one of which _bulk_get reports as missing:
GONE IC5BD4AD2C-…::FO::WSCARRIER::…
GONE RWRiskSummary::IC5BD4AD2C-…::…
… 14 total …
LIVE IC7F353AB0-…::FO::WSCARRIER::…
… 7 total …
The split is stable across hours and survives repeated purges — it is not a
propagation delay.
Expected
Either the purge invalidates the channel cache cluster-wide, or _changes does not
hand out IDs the same deployment answers 404 for.
Analysis
handlePurge does evict the cache (added by #3765, covered by TestPurgeWithChannelCache):
rest/admin_api.go
count := h.collection.RemoveFromChangeCache(h.ctx(), docIDs, startTime)
but that path is in-process only — channelCacheImpl.Remove ranges over
c.channelCaches, this node's map:
db/channel_cache.go
func (c *channelCacheImpl) Remove(...) (count int) {
c.channelCaches.Range(removeCallback)
Nothing notifies the other nodes, and they cannot infer it from the feed either.
Purge removes the doc with DeleteWithXattrs(key, [_sync, _globalSync]), so the
DCP deletion the other nodes receive has no _sync xattr, and changeCache.DocChanged
returns before doing anything with it:
db/change_cache.go
// If the document has no xattrs, it can not have a _sync xattr
if event.DataType&base.MemcachedDataTypeXattr == 0 {
return
}
So the eviction is unreachable for every node except the one that served the REST
call. db.Compact uses the same RemoveFromChangeCache call for tombstone purging
and looks to have the same limitation.
Impact
Any client driving reads off _changes has to tolerate 404 on IDs the feed just
gave it. In our case it surfaced as an intermittent, node-dependent test failure that
looked like a product bug for a while.
Workaround
Pass since=<sequence captured before the run> on _changes — stale cache entries
sit below that sequence, so they are filtered out regardless of which node answers.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing handlePurge in rest/admin_api.go through RemoveFromChangeCache in db/channel_cache.go, then compare it with changeCache.DocChanged in db/change_cache.go and the db.Compact path. Use TestPurgeWithChannelCache as the existing baseline; done means purged IDs are no longer returned by _changes from any node in a multi-node deployment without breaking cache behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- backend, databases, distributed-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100