tsdb: fix division by zero in stale series compaction (#17952)

Guard the stale series ratio calculation by checking numSeries > 0
before computing the ratio. This prevents division by zero when
the head has no series.

Fixes #17949

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
Arve Knudsen 2026-01-29 08:06:00 +01:00 committed by GitHub
parent dc34b90f93
commit 00a7faa2e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 39 additions and 14 deletions

View file

@ -1172,22 +1172,23 @@ func (db *DB) run(ctx context.Context) {
db.head.mmapHeadChunks()
numStaleSeries, numSeries := db.Head().NumStaleSeries(), db.Head().NumSeries()
staleSeriesRatio := float64(numStaleSeries) / float64(numSeries)
if db.autoCompact && db.opts.staleSeriesCompactionThreshold.Load() > 0 &&
staleSeriesRatio >= db.opts.staleSeriesCompactionThreshold.Load() {
nextCompactionIsSoon := false
if !db.lastHeadCompactionTime.IsZero() {
compactionInterval := time.Duration(db.head.chunkRange.Load()) * time.Millisecond
nextEstimatedCompactionTime := db.lastHeadCompactionTime.Add(compactionInterval)
if time.Now().Add(10 * time.Minute).After(nextEstimatedCompactionTime) {
// Next compaction is starting within next 10 mins.
nextCompactionIsSoon = true
if db.autoCompact && numSeries > 0 && db.opts.staleSeriesCompactionThreshold.Load() > 0 {
staleSeriesRatio := float64(numStaleSeries) / float64(numSeries)
if staleSeriesRatio >= db.opts.staleSeriesCompactionThreshold.Load() {
nextCompactionIsSoon := false
if !db.lastHeadCompactionTime.IsZero() {
compactionInterval := time.Duration(db.head.chunkRange.Load()) * time.Millisecond
nextEstimatedCompactionTime := db.lastHeadCompactionTime.Add(compactionInterval)
if time.Now().Add(10 * time.Minute).After(nextEstimatedCompactionTime) {
// Next compaction is starting within next 10 mins.
nextCompactionIsSoon = true
}
}
}
if !nextCompactionIsSoon {
if err := db.CompactStaleHead(); err != nil {
db.logger.Error("immediate stale series compaction failed", "err", err)
if !nextCompactionIsSoon {
if err := db.CompactStaleHead(); err != nil {
db.logger.Error("immediate stale series compaction failed", "err", err)
}
}
}
}

View file

@ -9561,3 +9561,27 @@ func TestStaleSeriesCompaction(t *testing.T) {
verifyHeadBlock()
}
}
// TestStaleSeriesCompactionWithZeroSeries verifies that CompactStaleHead handles
// an empty head (0 series) gracefully without division by zero or incorrectly
// triggering compaction. This is a regression test for issue #17949.
func TestStaleSeriesCompactionWithZeroSeries(t *testing.T) {
opts := DefaultOptions()
opts.MinBlockDuration = 1000
opts.MaxBlockDuration = 1000
db := newTestDB(t, withOpts(opts))
db.DisableCompactions()
t.Cleanup(func() {
require.NoError(t, db.Close())
})
// Verify the head is empty.
require.Equal(t, uint64(0), db.Head().NumSeries())
require.Equal(t, uint64(0), db.Head().NumStaleSeries())
// CompactStaleHead should handle zero series gracefully (no panic, no error).
require.NoError(t, db.CompactStaleHead())
// Should still have no blocks since there was nothing to compact.
require.Empty(t, db.Blocks())
}