Skip to content

Commit 5fbcd36

Browse files
torcolvingregns1claudeCopilot
authored
[4.1.2 Backport] CBG-5614: lateLogs compaction (#8634)
Co-authored-by: Gregory Newman-Smith <gregory.newmansmith@couchbase.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent bc30043 commit 5fbcd36

7 files changed

Lines changed: 1574 additions & 21 deletions

base/stats.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,11 @@ type CacheStats struct {
455455
ChannelCacheChannelsEvictedInactive *SgwIntStat `json:"chan_cache_channels_evicted_inactive"`
456456
// The total number of active channel cache channels evicted, based on ‘not recently used’ criteria.
457457
ChannelCacheChannelsEvictedNRU *SgwIntStat `json:"chan_cache_channels_evicted_nru"`
458+
// The total number of entries currently held across all channels' late-arriving-sequence queues (lateLogs).
459+
NumEntriesInLateFeed *SgwIntStat `json:"num_entries_in_late_feed"`
460+
// The total number of times a continuous _changes feed was forced to roll back to its low sequence because
461+
// its lastSequence was pruned from a channel's lateLogs (length/age cap firing on a lagging feed).
462+
LateFeedForcedRollbacks *SgwIntStat `json:"late_feed_forced_rollbacks"`
458463
// The total number of channel cache compaction runs.
459464
ChannelCacheCompactCount *SgwIntStat `json:"chan_cache_compact_count"`
460465
// The total amount of time taken by channel cache compaction across all compaction runs.
@@ -1481,6 +1486,14 @@ func (d *DbStats) initCacheStats() error {
14811486
if err != nil {
14821487
return err
14831488
}
1489+
resUtil.NumEntriesInLateFeed, err = NewIntStat(SubsystemCacheKey, "num_entries_in_late_feed", StatUnitNoUnits, NumEntriesInLateFeedDesc, StatAddedVersion4dot1dot0, StatDeprecatedVersionNotDeprecated, StatStabilityInternal, labelKeys, labelVals, prometheus.GaugeValue, 0)
1490+
if err != nil {
1491+
return err
1492+
}
1493+
resUtil.LateFeedForcedRollbacks, err = NewIntStat(SubsystemCacheKey, "late_feed_forced_rollbacks", StatUnitNoUnits, LateFeedForcedRollbacksDesc, StatAddedVersion4dot1dot0, StatDeprecatedVersionNotDeprecated, StatStabilityInternal, labelKeys, labelVals, prometheus.CounterValue, 0)
1494+
if err != nil {
1495+
return err
1496+
}
14841497
resUtil.ChannelCacheCompactCount, err = NewIntStat(SubsystemCacheKey, "chan_cache_compact_count", StatUnitNoUnits, ChanCacheCompactCountDesc, StatAddedVersion3dot0dot0, StatDeprecatedVersionNotDeprecated, StatStabilityCommitted, labelKeys, labelVals, prometheus.CounterValue, 0)
14851498
if err != nil {
14861499
return err
@@ -1597,6 +1610,8 @@ func (d *DbStats) unregisterCacheStats() {
15971610
prometheus.Unregister(d.CacheStats.ChannelCacheChannelsAdded)
15981611
prometheus.Unregister(d.CacheStats.ChannelCacheChannelsEvictedInactive)
15991612
prometheus.Unregister(d.CacheStats.ChannelCacheChannelsEvictedNRU)
1613+
prometheus.Unregister(d.CacheStats.NumEntriesInLateFeed)
1614+
prometheus.Unregister(d.CacheStats.LateFeedForcedRollbacks)
16001615
prometheus.Unregister(d.CacheStats.ChannelCacheCompactCount)
16011616
prometheus.Unregister(d.CacheStats.ChannelCacheCompactTime)
16021617
prometheus.Unregister(d.CacheStats.ChannelCacheHits)

base/stats_descriptions.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ const (
9898

9999
ChanCacheChannelsEvictedNRUDesc = "The total number of active channel cache channels evicted, based on 'not recently used' criteria."
100100

101+
NumEntriesInLateFeedDesc = "The total number of late-arriving-sequence entries currently held across all channels."
102+
103+
LateFeedForcedRollbacksDesc = "The total number of times a continuous _changes feed was forced to roll back to its low sequence because its lastSequence had been pruned from a channel's lateLogs by the length or age cap."
104+
101105
ChanCacheCompactCountDesc = "The total number of channel cache compaction runs."
102106

103107
ChanCacheCompactTimeDesc = "The total amount of time taken by channel cache compaction across all compaction runs."

db/change_cache.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,20 @@ type CacheOptions struct {
124124
CachePendingSeqMaxWait time.Duration // Max wait for pending sequence before skipping
125125
CachePendingSeqMaxNum int // Max number of pending sequences before skipping
126126
CacheSkippedSeqMaxWait time.Duration // Max wait for skipped sequence before abandoning
127+
// Broadcast ticker intervals for waking continuous _changes feeds. Zero means use the package default
128+
// (DefaultBroadcastChangesTime / SkippedSequenceBroadcastChangesTime). Internal tuning knob - primarily
129+
// so tests can avoid the 500ms skipped-sequence slow mode dominating runtime. Not exposed via REST config.
130+
BroadcastChangesInterval time.Duration // Normal-mode feed broadcast interval
131+
SkippedSequenceBroadcastInterval time.Duration // Slow-mode feed broadcast interval (skipped sequences present)
127132
}
128133

129134
func DefaultCacheOptions() CacheOptions {
130135
return CacheOptions{
131-
CachePendingSeqMaxWait: DefaultCachePendingSeqMaxWait,
132-
CachePendingSeqMaxNum: DefaultCachePendingSeqMaxNum,
133-
CacheSkippedSeqMaxWait: DefaultSkippedSeqMaxWait,
136+
CachePendingSeqMaxWait: DefaultCachePendingSeqMaxWait,
137+
CachePendingSeqMaxNum: DefaultCachePendingSeqMaxNum,
138+
CacheSkippedSeqMaxWait: DefaultSkippedSeqMaxWait,
139+
BroadcastChangesInterval: DefaultBroadcastChangesTime,
140+
SkippedSequenceBroadcastInterval: SkippedSequenceBroadcastChangesTime,
134141
ChannelCacheOptions: ChannelCacheOptions{
135142
ChannelCacheAge: DefaultChannelCacheAge,
136143
ChannelCacheMinLength: DefaultChannelCacheMinLength,
@@ -139,6 +146,7 @@ func DefaultCacheOptions() CacheOptions {
139146
CompactHighWatermarkPercent: DefaultCompactHighWatermarkPercent,
140147
CompactLowWatermarkPercent: DefaultCompactLowWatermarkPercent,
141148
ChannelQueryLimit: DefaultQueryPaginationLimit,
149+
LateLogAge: DefaultLateLogAge,
142150
},
143151
}
144152
}

db/change_listener.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -257,9 +257,9 @@ func (listener *changeListener) Notify(ctx context.Context, keys channels.Set) {
257257
}
258258

259259
func (listener *changeListener) StartNotifierBroadcaster(ctx context.Context) {
260-
ticker := time.NewTicker(DefaultBroadcastChangesTime)
261-
// boolean to indicate whether ticker is using the default value, this is needed so we don't call reset on ticker
262-
// for a value it already has
260+
ticker := time.NewTicker(listener.broadcastInterval(false))
261+
// Tracks whether we're currently using the slow-mode (skipped sequences present) interval, so we only
262+
// Reset the ticker when the mode changes.
263263
broadcastSlowMode := false
264264
go func(terminator chan bool, doneChan chan struct{}) {
265265
defer func() {
@@ -285,7 +285,7 @@ func (listener *changeListener) StartNotifierBroadcaster(ctx context.Context) {
285285
newBroadcastSlowMode := listener.dbCtx.BroadcastSlowMode.Load()
286286
if broadcastSlowMode != newBroadcastSlowMode {
287287
// broadcast changes interval has changed, reset ticker
288-
duration := tickerValForBroadcastSpeed(newBroadcastSlowMode)
288+
duration := listener.broadcastInterval(newBroadcastSlowMode)
289289
base.DebugfCtx(ctx, base.KeyChanges, "Updating broadcast changes interval for %q to %v", base.MD(listener.bucketName), duration)
290290
broadcastSlowMode = newBroadcastSlowMode
291291
ticker.Reset(duration)
@@ -295,14 +295,20 @@ func (listener *changeListener) StartNotifierBroadcaster(ctx context.Context) {
295295
}(listener.terminator, listener.broadcastChangesDoneChan)
296296
}
297297

298-
// tickerValForBroadcastSpeed will return the duration for the ticker to be reset to based on input boolean to indicate
299-
// if skipped sequences are present or not
300-
func tickerValForBroadcastSpeed(skippedSequencePresent bool) time.Duration {
301-
// if the skipped sequence broadcast is enabled, return the slow ticker value
298+
// broadcastInterval returns the duration for the broadcast ticker based on whether skipped sequences are
299+
// present, honoring the optional per-DB CacheOptions override and falling back to the package defaults when
300+
// unset. The override is an internal tuning (primarily to keep tests fast); production uses the defaults.
301+
func (listener *changeListener) broadcastInterval(skippedSequencePresent bool) time.Duration {
302+
opts := listener.dbCtx.Options.CacheOptions
302303
if skippedSequencePresent {
304+
if opts != nil && opts.SkippedSequenceBroadcastInterval > 0 {
305+
return opts.SkippedSequenceBroadcastInterval
306+
}
303307
return SkippedSequenceBroadcastChangesTime
304308
}
305-
// otherwise return the default ticker value
309+
if opts != nil && opts.BroadcastChangesInterval > 0 {
310+
return opts.BroadcastChangesInterval
311+
}
306312
return DefaultBroadcastChangesTime
307313
}
308314

db/channel_cache.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var (
3131
DefaultChannelCacheMaxNumber = 50000 // Default of 50k channel caches
3232
DefaultCompactHighWatermarkPercent = 80 // Default compaction high watermark (percent of MaxNumber)
3333
DefaultCompactLowWatermarkPercent = 60 // Default compaction low watermark (percent of MaxNumber)
34+
DefaultLateLogAge = 5 * time.Minute // Force-prune late-arriving entries older than this, even if a feed is still parked on them
3435
)
3536

3637
type ChannelCache interface {
@@ -131,6 +132,18 @@ func newChannelCache(ctx context.Context, dbName string, options ChannelCacheOpt
131132
return nil, err
132133
}
133134
channelCache.backgroundTasks = append(channelCache.backgroundTasks, bgt)
135+
136+
// Late-log pruning runs on its own task at LateLogAge, independent of the (potentially much larger)
137+
// ChannelCacheAge, so LateLogAge is honored regardless of how ChannelCacheAge is set.
138+
lateLogAge := options.LateLogAge
139+
if lateLogAge <= 0 {
140+
lateLogAge = DefaultLateLogAge
141+
}
142+
lateLogBGT, err := NewBackgroundTask(ctx, "CleanAgedLateLogs", channelCache.cleanAgedLateLogs, lateLogAge, channelCache.terminator)
143+
if err != nil {
144+
return nil, err
145+
}
146+
channelCache.backgroundTasks = append(channelCache.backgroundTasks, lateLogBGT)
134147
base.DebugfCtx(ctx, base.KeyCache, "Initialized channel cache with maxChannels:%d, HWM: %d, LWM: %d",
135148
channelCache.maxChannels, channelCache.compactHighWatermark, channelCache.compactLowWatermark)
136149
return channelCache, nil
@@ -139,6 +152,9 @@ func newChannelCache(ctx context.Context, dbName string, options ChannelCacheOpt
139152
func (c *channelCacheImpl) Clear() {
140153
c.seqLock.Lock()
141154
c.channelCaches.Init()
155+
// All channel caches (and their lateLogs, including sentinels) have just been dropped, so reset the
156+
// late-feed gauge rather than leaking it across change-cache reinitialization.
157+
c.cacheStats.NumEntriesInLateFeed.Set(0)
142158
c.seqLock.Unlock()
143159
}
144160

@@ -315,6 +331,24 @@ func (c *channelCacheImpl) cleanAgedItems(ctx context.Context) error {
315331
return nil
316332
}
317333

334+
// cleanAgedLateLogs prunes each channel's late-arriving sequence queue by LateLogAge. It runs on its own
335+
// background task (separate from cleanAgedItems) so late_log_expiry_seconds is honored on its own cadence,
336+
// independent of the channel cache's expiry_seconds. Error returned to fulfill BackgroundTaskFunc signature.
337+
func (c *channelCacheImpl) cleanAgedLateLogs(ctx context.Context) error {
338+
339+
callback := func(v any) bool {
340+
channelCache := AsSingleChannelCache(ctx, v)
341+
if channelCache == nil {
342+
return false
343+
}
344+
channelCache.pruneLateLogAge(ctx)
345+
return true
346+
}
347+
c.channelCaches.Range(callback)
348+
349+
return nil
350+
}
351+
318352
func (c *channelCacheImpl) getChannelCache(ctx context.Context, channel channels.ID) (SingleChannelCache, error) {
319353

320354
cacheValue, found := c.channelCaches.Get(channel)
@@ -396,6 +430,11 @@ func (c *channelCacheImpl) addChannelCache(ctx context.Context, channel channels
396430
cacheValue, created, cacheSize := c.channelCaches.GetOrInsert(channel, singleChannelCache)
397431
c.validFromLock.Unlock()
398432

433+
// If another goroutine won the insert race our freshly-built cache is discarded, but it never contributed to
434+
// NumEntriesInLateFeed: initializeLateLogs doesn't count the retained placeholder, and since the discarded
435+
// instance was never in channelCaches no late arrival could have been added to it. So there's nothing to
436+
// release here.
437+
399438
singleChannelCache = AsSingleChannelCache(ctx, cacheValue)
400439

401440
if cacheSize > c.compactHighWatermark {
@@ -547,6 +586,18 @@ func (c *channelCacheImpl) compactChannelCache(ctx context.Context) {
547586

548587
cacheSize = c.channelCaches.RemoveElements(evictionElements)
549588

589+
// Evicted channel caches take their lateLogs entries (including the retained placeholder) with them, and
590+
// those entries never go through the per-entry purge paths, so release each cache's contribution to the
591+
// late-feed gauge here to stop NumEntriesInLateFeed from leaking upward as channels are evicted.
592+
// releaseLateLogsForEviction reads-and-decrements atomically under the cache's lateLogLock (and runs
593+
// after RemoveElements, so any add that landed just before removal is captured by the count read),
594+
// rather than subtracting a pre-removal snapshot that could race concurrent adds/prunes.
595+
for _, elem := range evictionElements {
596+
if scc, ok := elem.Value.(*singleChannelCacheImpl); ok {
597+
scc.releaseLateLogsForEviction()
598+
}
599+
}
600+
550601
// Update eviction stats
551602
c.updateEvictionStats(inactiveEvictCount, len(evictionElements), compactIterationStart)
552603

0 commit comments

Comments
 (0)