diff --git a/tsdb/db.go b/tsdb/db.go index e8ab300397..c5da5b54a6 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -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) + } } } } diff --git a/tsdb/db_test.go b/tsdb/db_test.go index 2dbcb11645..403ce3636a 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -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()) +}