mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-07-22 06:50:38 +02:00
fix(metricstore): make stats read path non-mutating to avoid data race
recomputeStats() wrote statSum/statSamples/statMin/statMax/statsValid from inside the stats() fast path, which runs under only a shared RLock via MemoryStore.Stats -> Level.findBuffers. Two concurrent queries on the same invalid buffer could both enter the fast path and race on those fields. Require statsValid in the fast-path guard instead of recomputing inline; invalid buffers now fall through to the existing point-by-point scan, which only reads b.data. recomputeStats() is unchanged and still used by checkpoint loadFile at single-threaded load time. Updated stats_test.go: overwritten buffers now stay statsValid=false after a query (documenting non-mutating reads); TestStatsMultiBufferChain and TestStatsFastPathThenPartialTail now recompute stats after building bare buffers so the fast-path cache fold is still covered; added TestStatsConcurrentQueriesNoRace (race-clean under -race for both valid and post-overwrite buffers) and TestStatsGappedChain (real inter-buffer gap correctly excluded from Samples/Min/Max/Avg). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -74,10 +74,7 @@ func (b *buffer) stats(from, to int64) (Stats, int64, int64, error) {
|
||||
// past the buffer's real data instead of scanning each point. Any slots
|
||||
// between len(data) and cap are handled as gaps by the normal loop after
|
||||
// t advances, so the returned `to` matches the scan semantics.
|
||||
if len(b.data) > 0 && idx <= 0 && t <= b.firstWrite() && b.end() <= to {
|
||||
if !b.statsValid {
|
||||
b.recomputeStats()
|
||||
}
|
||||
if len(b.data) > 0 && b.statsValid && idx <= 0 && t <= b.firstWrite() && b.end() <= to {
|
||||
if b.statSamples > 0 {
|
||||
sum += b.statSum
|
||||
samples += b.statSamples
|
||||
|
||||
Reference in New Issue
Block a user