diff --git a/tsdb/db_test.go b/tsdb/db_test.go index 498e26c58..e98f60299 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -1994,6 +1994,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { // Should be set to init values if no WAL or blocks exist so far. require.Equal(t, int64(math.MaxInt64), db.head.MinTime()) require.Equal(t, int64(math.MinInt64), db.head.MaxTime()) + require.False(t, db.head.initialized()) // First added sample initializes the writable range. ctx := context.Background() @@ -2003,6 +2004,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { require.Equal(t, int64(1000), db.head.MinTime()) require.Equal(t, int64(1000), db.head.MaxTime()) + require.True(t, db.head.initialized()) }) t.Run("wal-only", func(t *testing.T) { dir := t.TempDir() @@ -2031,6 +2033,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { require.Equal(t, int64(5000), db.head.MinTime()) require.Equal(t, int64(15000), db.head.MaxTime()) + require.True(t, db.head.initialized()) }) t.Run("existing-block", func(t *testing.T) { dir := t.TempDir() @@ -2043,6 +2046,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { require.Equal(t, int64(2000), db.head.MinTime()) require.Equal(t, int64(2000), db.head.MaxTime()) + require.True(t, db.head.initialized()) }) t.Run("existing-block-and-wal", func(t *testing.T) { dir := t.TempDir() @@ -2075,6 +2079,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { require.Equal(t, int64(6000), db.head.MinTime()) require.Equal(t, int64(15000), db.head.MaxTime()) + require.True(t, db.head.initialized()) // Check that old series has been GCed. require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.series)) }) diff --git a/tsdb/head.go b/tsdb/head.go index 820fba9e2..8b3d9787c 100644 --- a/tsdb/head.go +++ b/tsdb/head.go @@ -1081,11 +1081,11 @@ func (h *Head) SetMinValidTime(minValidTime int64) { // Truncate removes old data before mint from the head and WAL. func (h *Head) Truncate(mint int64) (err error) { - initialize := h.MinTime() == math.MaxInt64 + initialized := h.initialized() if err := h.truncateMemory(mint); err != nil { return err } - if initialize { + if !initialized { return nil } return h.truncateWAL(mint) @@ -1107,9 +1107,9 @@ func (h *Head) truncateMemory(mint int64) (err error) { } }() - initialize := h.MinTime() == math.MaxInt64 + initialized := h.initialized() - if h.MinTime() >= mint && !initialize { + if h.MinTime() >= mint && initialized { return nil } @@ -1120,7 +1120,7 @@ func (h *Head) truncateMemory(mint int64) (err error) { defer h.memTruncationInProcess.Store(false) // We wait for pending queries to end that overlap with this truncation. - if !initialize { + if initialized { h.WaitForPendingReadersInTimeRange(h.MinTime(), mint) } @@ -1134,7 +1134,7 @@ func (h *Head) truncateMemory(mint int64) (err error) { // This was an initial call to Truncate after loading blocks on startup. // We haven't read back the WAL yet, so do not attempt to truncate it. - if initialize { + if !initialized { return nil } @@ -1622,10 +1622,19 @@ func (h *Head) MaxOOOTime() int64 { return h.maxOOOTime.Load() } +// initialized returns true if the head has a MinTime set, false otherwise. +func (h *Head) initialized() bool { + return h.MinTime() != math.MaxInt64 +} + // compactable returns whether the head has a compactable range. // The head has a compactable range when the head time range is 1.5 times the chunk range. // The 0.5 acts as a buffer of the appendable window. func (h *Head) compactable() bool { + if !h.initialized() { + return false + } + return h.MaxTime()-h.MinTime() > h.chunkRange.Load()/2*3 } diff --git a/tsdb/head_append.go b/tsdb/head_append.go index 6342a19d4..58d24ceb9 100644 --- a/tsdb/head_append.go +++ b/tsdb/head_append.go @@ -138,7 +138,7 @@ func (h *Head) Appender(_ context.Context) storage.Appender { // The head cache might not have a starting point yet. The init appender // picks up the first appended timestamp as the base. - if h.MinTime() == math.MaxInt64 { + if !h.initialized() { return &initAppender{ head: h, } @@ -191,7 +191,7 @@ func (h *Head) appendableMinValidTime() int64 { // AppendableMinValidTime returns the minimum valid time for samples to be appended to the Head. // Returns false if Head hasn't been initialized yet and the minimum time isn't known yet. func (h *Head) AppendableMinValidTime() (int64, bool) { - if h.MinTime() == math.MaxInt64 { + if !h.initialized() { return 0, false } diff --git a/tsdb/head_test.go b/tsdb/head_test.go index 41c2e062f..750c8a11e 100644 --- a/tsdb/head_test.go +++ b/tsdb/head_test.go @@ -5819,3 +5819,16 @@ func TestHeadAppender_AppendCTZeroSample(t *testing.T) { require.Equal(t, chunkenc.ValNone, it.Next()) } } + +func TestHeadCompactableDoesNotCompactEmptyHead(t *testing.T) { + // Use a chunk range of 1 here so that if we attempted to determine if the head + // was compactable using default values for min and max times, `Head.compactable()` + // would return true which is incorrect. This test verifies that we short-circuit + // the check when the head has not yet had any samples added. + head, _ := newTestHead(t, 1, wlog.CompressionNone, false) + defer func() { + require.NoError(t, head.Close()) + }() + + require.False(t, head.compactable()) +}