Files
metrics/lib/storage/storage_synctest_test.go
NikolayandGitHub 11226e60f9 lib/storage: properly report metrics for the last partition
Previously, on the last day of a month, storage could report empty
metrics for the last partition. This could happen if a new empty
partition was created in updateNextDayMetricIDs or if time series with
future timestamps were ingested.

This commit adds a check to ensure the last partition belongs to the
current month. Since this is typically the most actively used partition,
it should be treated as the last one.

Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10387
2026-02-13 11:21:20 +01:00

704 lines
26 KiB
Go

//go:build synctest
package storage
import (
"fmt"
"math/rand"
"testing"
"testing/synctest"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
"github.com/google/go-cmp/cmp"
)
func TestStorageSearchTSIDs_CorruptedIndex(t *testing.T) {
defer testRemoveAll(t)
synctest.Test(t, func(t *testing.T) {
s := MustOpenStorage(t.Name(), OpenOptions{})
defer s.MustClose()
now := time.Now().UTC()
tr := TimeRange{
MinTimestamp: time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
const numMetrics = 10
date := uint64(tr.MinTimestamp) / msecPerDay
ptw := s.tb.MustGetPartition(tr.MinTimestamp)
idb := ptw.pt.idb
defer s.tb.PutPartition(ptw)
var wantMetricIDs []uint64
// Simulate corrupted index by not creating nsPrefixMetricIDToTSID
// index entries.
for i := range numMetrics {
mn := MetricName{
MetricGroup: []byte(fmt.Sprintf("metric_%d", i)),
}
var tsid TSID
generateTSID(&tsid, &mn)
wantMetricIDs = append(wantMetricIDs, tsid.MetricID)
ii := testCreateIndexItems(date, &tsid, &mn, testIndexItemOpts{
skipMetricIDToTSID: true,
})
idb.tb.AddItems(ii.Items)
}
idb.tb.DebugFlush()
tfsAll := NewTagFilters()
if err := tfsAll.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
panic(fmt.Sprintf("unexpected error in TagFilters.Add: %v", err))
}
tfssAll := []*TagFilters{tfsAll}
searchMetricIDs := func() []uint64 {
metricIDs, err := idb.searchMetricIDs(nil, tfssAll, tr, 1e9, noDeadline)
if err != nil {
panic(fmt.Sprintf("searchMetricIDs() failed unexpectedly: %v", err))
}
return metricIDs.AppendTo(nil)
}
searchTSIDs := func() []TSID {
tsids, err := s.SearchTSIDs(nil, tfssAll, tr, 1e9, noDeadline)
if err != nil {
panic(fmt.Sprintf("SearchTSIDs() failed unexpectedly: %v", err))
}
return tsids
}
// Ensure that metricIDs can be searched.
if diff := cmp.Diff(wantMetricIDs, searchMetricIDs()); diff != "" {
t.Fatalf("unexpected metricIDs (-want, +got):\n%s", diff)
}
// Ensure that Storage.SearchTSIDs() returns empty result.
// The corrupted index lets to find metricIDs by tag (`__name__` tag in
// our case) but it lacks metricID->TSID mapping and hence the
// empty search result.
// The code detects this and puts such metricIDs into a special cache.
if diff := cmp.Diff([]TSID(nil), searchTSIDs()); diff != "" {
t.Fatalf("unexpected TSIDs (-want, +got):\n%s", diff)
}
// Ensure that the metricIDs still can be searched.
if diff := cmp.Diff(wantMetricIDs, searchMetricIDs()); diff != "" {
t.Fatalf("unexpected metricIDs (-want, +got):\n%s", diff)
}
// Ensure the metric that counts metricIDs for which no TSIDs were found
// is not incremented yet.
var m Metrics
s.UpdateMetrics(&m)
if got, want := m.TableMetrics.IndexDBMetrics.MissingTSIDsForMetricID, uint64(0); got != want {
t.Fatalf("unexpected MissingTSIDsForMetricID: got %d, want %d", got, want)
}
time.Sleep(61 * time.Second)
synctest.Wait()
// If the same search is repeated after 1 minute, the metricIDs are
// marked as deleted.
if diff := cmp.Diff([]TSID(nil), searchTSIDs()); diff != "" {
t.Fatalf("unexpected metric names (-want, +got):\n%s", diff)
}
// As a result they cannot be searched anymore.
if diff := cmp.Diff([]uint64(nil), searchMetricIDs()); diff != "" {
t.Fatalf("unexpected metricIDs (-want, +got):\n%s", diff)
}
// Ensure the metric that counts metricIDs for which no TSIDs were found
// is incremented after the metricID deletion.
s.UpdateMetrics(&m)
if got, want := m.TableMetrics.IndexDBMetrics.MissingTSIDsForMetricID, uint64(numMetrics); got != want {
t.Fatalf("unexpected MissingTSIDsForMetricID: got %d, want %d", got, want)
}
})
}
func TestStorageSearchMetricNames_CorruptedIndex(t *testing.T) {
defer testRemoveAll(t)
synctest.Test(t, func(t *testing.T) {
s := MustOpenStorage(t.Name(), OpenOptions{})
defer s.MustClose()
now := time.Now().UTC()
tr := TimeRange{
MinTimestamp: time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(now.Year(), now.Month(), now.Day(), 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
const numMetrics = 10
date := uint64(tr.MinTimestamp) / msecPerDay
ptw := s.tb.MustGetPartition(tr.MinTimestamp)
idb := ptw.pt.idb
defer s.tb.PutPartition(ptw)
var wantMetricIDs []uint64
// Simulate corrupted index by not creating nsPrefixMetricIDToMetricName
// index entries.
for i := range numMetrics {
mn := MetricName{
MetricGroup: []byte(fmt.Sprintf("metric_%d", i)),
}
var tsid TSID
generateTSID(&tsid, &mn)
wantMetricIDs = append(wantMetricIDs, tsid.MetricID)
ii := testCreateIndexItems(date, &tsid, &mn, testIndexItemOpts{
skipMetricIDToMetricName: true,
})
idb.tb.AddItems(ii.Items)
}
idb.tb.DebugFlush()
tfsAll := NewTagFilters()
if err := tfsAll.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
panic(fmt.Sprintf("unexpected error in TagFilters.Add: %v", err))
}
tfssAll := []*TagFilters{tfsAll}
searchMetricIDs := func() []uint64 {
metricIDs, err := idb.searchMetricIDs(nil, tfssAll, tr, 1e9, noDeadline)
if err != nil {
panic(fmt.Sprintf("searchMetricIDs() failed unexpectedly: %v", err))
}
return metricIDs.AppendTo(nil)
}
searchMetricNames := func() []string {
metricNames, err := s.SearchMetricNames(nil, tfssAll, tr, 1e9, noDeadline)
if err != nil {
panic(fmt.Sprintf("SearchMetricNames() failed unexpectedly: %v", err))
}
return metricNames
}
// Ensure that metricIDs can be searched.
if diff := cmp.Diff(wantMetricIDs, searchMetricIDs()); diff != "" {
t.Fatalf("unexpected metricIDs (-want, +got):\n%s", diff)
}
// Ensure that Storage.SearchMetricNames() returns empty result.
// The corrupted index lets to find metricIDs by tag (`__name__` tag in
// our case) but it lacks metricID->metricName mapping and hence the
// empty search result.
// The code detects this and puts such metricIDs into a special cache.
if diff := cmp.Diff([]string{}, searchMetricNames()); diff != "" {
t.Fatalf("unexpected metric names (-want, +got):\n%s", diff)
}
// Ensure that the metricIDs still can be searched.
if diff := cmp.Diff(wantMetricIDs, searchMetricIDs()); diff != "" {
t.Fatalf("unexpected metricIDs (-want, +got):\n%s", diff)
}
// Ensure the metric that counts metricIDs for which no metric names
// were found is not incremented yet.
var m Metrics
s.UpdateMetrics(&m)
if got, want := m.TableMetrics.IndexDBMetrics.MissingMetricNamesForMetricID, uint64(0); got != want {
t.Fatalf("unexpected MissingMetricNamesForMetricID: got %d, want %d", got, want)
}
time.Sleep(61 * time.Second)
synctest.Wait()
// If the same search is repeated after 1 minute, the metricIDs are
// marked as deleted.
if diff := cmp.Diff([]string{}, searchMetricNames()); diff != "" {
t.Fatalf("unexpected metric names (-want, +got):\n%s", diff)
}
// As a result they cannot be searched anymore.
if diff := cmp.Diff([]uint64(nil), searchMetricIDs()); diff != "" {
t.Fatalf("unexpected metricIDs (-want, +got):\n%s", diff)
}
// Ensure the metric that counts metricIDs for which no metric names
// were found is incremented after the metricID deletion.
s.UpdateMetrics(&m)
if got, want := m.TableMetrics.IndexDBMetrics.MissingMetricNamesForMetricID, uint64(numMetrics); got != want {
t.Fatalf("unexpected MissingMetricNamesForMetricID: got %d, want %d", got, want)
}
})
}
type testIndexItemOpts struct {
skipMetricIDToMetricName bool
skipMetricIDToTSID bool
skipTagToMetricIDs bool
skipDateToMetricID bool
skipDateMetricNameToTSID bool
skipDateTagToMetricIDs bool
}
func testCreateIndexItems(date uint64, tsid *TSID, mn *MetricName, opts testIndexItemOpts) *indexItems {
var ii indexItems
if !opts.skipMetricIDToMetricName {
// Create metricID -> metricName entry.
ii.B = marshalCommonPrefix(ii.B, nsPrefixMetricIDToMetricName)
ii.B = encoding.MarshalUint64(ii.B, tsid.MetricID)
ii.B = mn.Marshal(ii.B)
ii.Next()
}
if !opts.skipMetricIDToTSID {
// Create metricID -> TSID entry.
ii.B = marshalCommonPrefix(ii.B, nsPrefixMetricIDToTSID)
ii.B = encoding.MarshalUint64(ii.B, tsid.MetricID)
ii.B = tsid.Marshal(ii.B)
ii.Next()
}
if !opts.skipTagToMetricIDs {
// Create tag -> metricID entries for every tag in mn.
kb := kbPool.Get()
kb.B = marshalCommonPrefix(kb.B[:0], nsPrefixTagToMetricIDs)
ii.registerTagIndexes(kb.B, mn, tsid.MetricID)
ii.Next()
kbPool.Put(kb)
}
if !opts.skipDateToMetricID {
// Create date -> metricID entry.
ii.B = marshalCommonPrefix(ii.B, nsPrefixDateToMetricID)
ii.B = encoding.MarshalUint64(ii.B, date)
ii.B = encoding.MarshalUint64(ii.B, tsid.MetricID)
ii.Next()
}
if !opts.skipDateMetricNameToTSID {
// Create metricName -> TSID entry.
ii.B = marshalCommonPrefix(ii.B, nsPrefixDateMetricNameToTSID)
ii.B = encoding.MarshalUint64(ii.B, date)
ii.B = mn.Marshal(ii.B)
ii.B = append(ii.B, kvSeparatorChar)
ii.B = tsid.Marshal(ii.B)
ii.Next()
}
if !opts.skipDateTagToMetricIDs {
// Create per-day tag -> metricID entries for every tag in mn.
kb := kbPool.Get()
kb.B = marshalCommonPrefix(kb.B[:0], nsPrefixDateTagToMetricIDs)
kb.B = encoding.MarshalUint64(kb.B, date)
ii.registerTagIndexes(kb.B, mn, tsid.MetricID)
kbPool.Put(kb)
}
return &ii
}
func TestStorageRotateIndexDBPrefill(t *testing.T) {
defer testRemoveAll(t)
f := func(t *testing.T, opts OpenOptions, prefillStart time.Duration) {
t.Helper()
synctest.Test(t, func(t *testing.T) {
// Prefill of the next partition indexDB happens during the
// (nextMonth-prefillStart, nextMonth] time interval.
// Advance current time right before the the beginning of that interval.
ct := time.Now().UTC()
nextMonth := time.Date(ct.Year(), ct.Month()+1, 1, 0, 0, 0, 0, time.UTC)
time.Sleep(nextMonth.Sub(ct.Add(prefillStart)))
s := MustOpenStorage(t.Name(), opts)
defer s.MustClose()
const numSeries = 1000
addRows := func() {
t.Helper()
rng := rand.New(rand.NewSource(1))
ct := time.Now().UTC()
tr := TimeRange{
MinTimestamp: ct.Add(-prefillStart).UnixMilli(),
MaxTimestamp: ct.UnixMilli(),
}
mrs := testGenerateMetricRowsWithPrefix(rng, numSeries, "metric.", tr)
s.AddRows(mrs, 1)
s.DebugFlush()
}
// Insert metrics into the empty storage right before the prefill
// interval starts.
addRows()
if got, want := s.newTimeseriesCreated.Load(), uint64(numSeries); got != want {
t.Fatalf("unexpected number of new timeseries: got %d, want %d", got, want)
}
if got, want := s.timeseriesPreCreated.Load(), uint64(0); got != want {
t.Fatalf("unexpected number of pre-created timeseries: got %d, want %d", got, want)
}
// Sleep until half of the prefill interval has elapsed,
// then verify that some time series have been pre-created.
time.Sleep(prefillStart / 2)
addRows()
if got, want := s.timeseriesPreCreated.Load(), uint64(0); got <= want {
t.Fatalf("unexpected number of pre-created timeseries: got %d, want > %d", got, want)
}
// Sleep until a minute before the next partition transition, verify
// that almost all time series have been pre-created.
ct = time.Now().UTC()
time.Sleep(nextMonth.Sub(ct.Add(time.Minute)))
addRows()
if got, want := s.timeseriesPreCreated.Load(), uint64(numSeries/2); got <= want {
t.Fatalf("unexpected number of pre-created timeseries: got %d, want > %d", got, want)
}
// Align the time with the start of the next month.
time.Sleep(time.Minute)
// Sleep until the transition to the next partition is over, verify
// that the rest of time series have been re-created
time.Sleep(prefillStart)
newCreated := s.newTimeseriesCreated.Load()
addRows()
newCreated = s.newTimeseriesCreated.Load() - newCreated
// If jump in time is bigger than 1h, the tsidCache will be cleared
// and therefore the metrics will not be repopulated. Instead, new
// metrics will be created.
preCreated, repopulated := s.timeseriesPreCreated.Load(), s.timeseriesRepopulated.Load()
if preCreated+repopulated+newCreated != numSeries {
t.Fatalf("unexpected number of pre-populated, repopulated, and new timeseries: got %d + %d + %d, want %d", preCreated, repopulated, newCreated, numSeries)
}
})
}
// Verify an interval that is shorter than one hour.
t.Run("30m", func(t *testing.T) {
f(t, OpenOptions{IDBPrefillStart: 30 * time.Minute}, 30*time.Minute)
})
// Verify 1h inteval (which is also the default).
// tsidCache will be cleared because it will have two cache rotations (one
// every 30 mins). This means that once the new month starts the timeseries
// that waren't pre-populated will be re-created instead of being
// re-populated.
t.Run("default", func(t *testing.T) {
f(t, OpenOptions{IDBPrefillStart: 0}, time.Hour)
})
t.Run("1h", func(t *testing.T) {
f(t, OpenOptions{IDBPrefillStart: time.Hour}, time.Hour)
})
// Vefiry 2h interval. Same here, the tsidCache will be cleared.
t.Run("2h", func(t *testing.T) {
f(t, OpenOptions{IDBPrefillStart: 2 * time.Hour}, 2*time.Hour)
})
}
// TestStorageAddRows_nextDayIndexPrefill tests gradual creation of per-day
// index entries of the next day during the last hour of the current day. This
// is an optimization that mitigates the ingestion slowdown at the beginning of
// a day.
//
// Problem: as the new day begins, indexDB suddenly lacks ALL the per-day
// entries for this new day and they need to be created. If the number of active
// timeseries is high enough, this may cause serious degradation of the data
// ingestion performance.
//
// Solution: start creating the next day entries during the last hour of the
// current day. In order to avoid the same slowdown, the entries are created
// gradually. I.e. at the beginning of the last hour, a very small fraction of
// next day index entries (compared to the total number of active timeseries)
// will be created. As current time gets closer and closer to midnight, the
// fraction becomes bigger. This does not mean that the number of next day index
// entries to create becomes proportionately bigger since some of them have
// already been added earlier that hour. Finally, only active timeseries are
// registered in the next day index. An active timeseries is one for which at
// least one sample have been received during the current hour and the timestamp
// of that sample must also fall within the current hour.
func TestStorageAddRows_nextDayIndexPrefill(t *testing.T) {
defer testRemoveAll(t)
countMetricIDs := func(t *testing.T, s *Storage, prefix string, tr TimeRange) int {
t.Helper()
tfs := NewTagFilters()
if err := tfs.Add(nil, []byte(prefix+".*"), false, true); err != nil {
t.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
ids := testSearchMetricIDs(s, []*TagFilters{tfs}, tr, 1e9, noDeadline)
return len(ids)
}
synctest.Test(t, func(t *testing.T) {
// synctest starts at 2000-01-01T00:00:00Z.
today := TimeRange{
MinTimestamp: time.Now().UnixMilli(),
MaxTimestamp: time.Now().UnixMilli() + msecPerDay - 1,
}
nextDay := TimeRange{
MinTimestamp: today.MinTimestamp + msecPerDay,
MaxTimestamp: today.MaxTimestamp + msecPerDay,
}
const numSeries = 1000
rng := rand.New(rand.NewSource(1))
// Verify that prefill hasn't started yet.
// The prefill happens during the last hour of a day. At exactly
// 23:00:00, however, it must not start yet.
//
// Advance the time 1m before the last hour.
time.Sleep(23*time.Hour - 1*time.Minute) // 2000-01-01T22:59:00Z
mrs0 := testGenerateMetricRowsWithPrefix(rng, numSeries, "metric0", TimeRange{
MinTimestamp: time.Now().Add(-15 * time.Minute).UnixMilli(),
MaxTimestamp: time.Now().Add(+15 * time.Minute).UnixMilli(),
})
s := MustOpenStorage(t.Name(), OpenOptions{})
defer s.MustClose()
s.AddRows(mrs0, defaultPrecisionBits)
s.DebugFlush()
if got, want := countMetricIDs(t, s, "metric0", today), numSeries; got != want {
t.Fatalf("unexpected metric id count for today: got %d, want %d", got, want)
}
if got, want := countMetricIDs(t, s, "metric0", nextDay), 0; got != want {
t.Fatalf("unexpected metric id count for next day: got %d, want %d", got, want)
}
// Again, at 23:00:00 the prefill must not start yet even for timestamps
// beyond that time.
time.Sleep(1 * time.Minute) // 2000-01-01T23:00:00Z
synctest.Wait()
s.AddRows(mrs0, defaultPrecisionBits)
s.DebugFlush()
if got, want := countMetricIDs(t, s, "metric0", today), numSeries; got != want {
t.Fatalf("unexpected metric id count for today: got %d, want %d", got, want)
}
if got, want := countMetricIDs(t, s, "metric0", nextDay), 0; got != want {
t.Fatalf("unexpected metric id count for next day: got %d, want %d", got, want)
}
// At 23:15 the prefill must work.
//
// However, the mrs1 timestamps are not within the current hour and
// therefore the next day will not be prefilled with the corresponding
// timeseries.
//
// The mrs2 timestamps are within the current hour so some next day index
// entries will be created.
time.Sleep(15 * time.Minute) // 2000-01-01T23:15:00Z
mrs1 := testGenerateMetricRowsWithPrefix(rng, numSeries, "metric1", TimeRange{
MinTimestamp: time.Now().Add(-30 * time.Minute).UnixMilli(),
MaxTimestamp: time.Now().Add(-15 * time.Minute).UnixMilli(),
})
mrs2 := testGenerateMetricRowsWithPrefix(rng, numSeries, "metric2", TimeRange{
MinTimestamp: time.Now().Add(-15 * time.Minute).UnixMilli(),
MaxTimestamp: time.Now().UnixMilli(),
})
s.AddRows(mrs1, defaultPrecisionBits)
s.AddRows(mrs2, defaultPrecisionBits)
s.DebugFlush()
if got, want := countMetricIDs(t, s, "metric1", today), numSeries; got != want {
t.Fatalf("unexpected metric id count for today: got %d, want %d", got, want)
}
if got, want := countMetricIDs(t, s, "metric1", nextDay), 0; got != want {
t.Fatalf("unexpected metric id count for next day: got %d, want %d", got, want)
}
if got, want := countMetricIDs(t, s, "metric2", today), numSeries; got != want {
t.Fatalf("unexpected metric id count for today: got %d, want %d", got, want)
}
got15min := countMetricIDs(t, s, "metric2", nextDay)
if got15min == 0 {
t.Fatalf("unexpected metric id count for next day: got 0, want > 0")
}
time.Sleep(15 * time.Minute) // 2000-01-01T23:30:00Z
mrs3 := testGenerateMetricRowsWithPrefix(rng, numSeries, "metric3", TimeRange{
MinTimestamp: time.Now().Add(-15 * time.Minute).UnixMilli(),
MaxTimestamp: time.Now().UnixMilli(),
})
s.AddRows(mrs3, defaultPrecisionBits)
s.DebugFlush()
if got, want := countMetricIDs(t, s, "metric3", today), numSeries; got != want {
t.Fatalf("unexpected metric id count for today: got %d, want %d", got, want)
}
got30min := countMetricIDs(t, s, "metric3", nextDay)
if got30min < got15min {
t.Fatalf("unexpected metric id count for next day: got %d, want > %d", got30min, got15min)
}
time.Sleep(15 * time.Minute) // 2000-01-01T23:45:00Z
mrs4 := testGenerateMetricRowsWithPrefix(rng, numSeries, "metric4", TimeRange{
MinTimestamp: time.Now().Add(-15 * time.Minute).UnixMilli(),
MaxTimestamp: time.Now().UnixMilli(),
})
s.AddRows(mrs4, defaultPrecisionBits)
s.DebugFlush()
got45min := countMetricIDs(t, s, "metric4", nextDay)
if got45min < got30min {
t.Fatalf("unexpected metric id count for next day: got %d, want > %d", got45min, got30min)
}
// Sleep until the next day
// do not close storage, it resets dataMetricID cache and it will result into slow inserts
// since dateMetricID cache is not persisted on-disk
time.Sleep(35 * time.Minute) // 2000-01-02T00:20:00Z
synctest.Wait()
// Ingest data for the next day, it must hit dateMetricID cache and
// do not result into significant amount of slow inserts.
var m Metrics
s.UpdateMetrics(&m)
currDaySlowInserts := m.SlowPerDayIndexInserts
mrs3NextDay := testGenerateMetricRowsWithPrefix(rng, numSeries, "metric3", TimeRange{
MinTimestamp: time.Now().Add(-5 * time.Minute).UnixMilli(),
MaxTimestamp: time.Now().UnixMilli(),
})
s.AddRows(mrs3NextDay, defaultPrecisionBits)
s.DebugFlush()
m.Reset()
s.UpdateMetrics(&m)
nextDaySlowInserts := m.SlowPerDayIndexInserts
slowInserts := nextDaySlowInserts - currDaySlowInserts
if slowInserts >= numSeries {
t.Errorf("unexpected amount of slow inserts: got %d, want < %d", slowInserts, numSeries)
}
})
}
func TestStorageMustLoadNextDayMetricIDs(t *testing.T) {
defer testRemoveAll(t)
assertNextDayMetricIDs := func(t *testing.T, gotNextDayMetricIDs *nextDayMetricIDs, wantIDBID, wantDate uint64, wantLen int) {
t.Helper()
if got, want := gotNextDayMetricIDs.idbID, wantIDBID; got != want {
t.Fatalf("unexpected nextDayMetricIDs idb id: got %d, want %d", got, want)
}
if got, want := gotNextDayMetricIDs.date, wantDate; got != want {
t.Fatalf("unexpected nextDayMetricIDs date: got %d, want %d", got, want)
}
if got, want := gotNextDayMetricIDs.metricIDs.Len(), wantLen; got != want {
t.Fatalf("unexpected nextDayMetricIDs count: got %d, want %d", got, want)
}
}
synctest.Test(t, func(t *testing.T) {
// synctest starts at 2000-01-01T00:00:00Z.
// Advance time to 23:30 to enable next day prefill.
time.Sleep(23*time.Hour + 30*time.Minute) // 2000-01-01T23:30:00Z
date := uint64(time.Now().UnixMilli()) / msecPerDay
const numSeries = 1000
s := MustOpenStorage(t.Name(), OpenOptions{})
ptw := s.tb.MustGetPartition(time.Now().UnixMilli())
idbID := ptw.pt.idb.id
s.tb.PutPartition(ptw)
rng := rand.New(rand.NewSource(1))
mrs := testGenerateMetricRowsWithPrefix(rng, numSeries, "metric", TimeRange{
MinTimestamp: time.Now().Add(-15 * time.Minute).UnixMilli(),
MaxTimestamp: time.Now().UnixMilli(),
})
s.AddRows(mrs, defaultPrecisionBits)
s.DebugFlush()
// The next day metricIDs must appear in pendingNextDayMetricIDs cache.
if s.pendingNextDayMetricIDs.Len() == 0 {
t.Fatalf("unexpected pendingNextDayMetricIDs count: got 0, want > 0")
}
numNextDayMetricIDs := s.pendingNextDayMetricIDs.Len()
// But not in the nextDayMetricIDs cache. The pending metrics will be
// moved to it by a bg process a few seconds later.
assertNextDayMetricIDs(t, s.nextDayMetricIDs.Load(), idbID, date, 0)
// Wait for nextDayMetricIDs cache to populate.
time.Sleep(15 * time.Second)
synctest.Wait()
// At this point, pending metricIDs must have been moved to
// nextDayMetricIDs cache and the pendingNextDayMetricIDs must be empty.
if got := s.pendingNextDayMetricIDs.Len(); got != 0 {
t.Fatalf("unexpected pendingNextDayMetricIDs count: got %d, want 0", got)
}
// While the actual cache, must contain the exact number of metricIDs
// that once were pending.
assertNextDayMetricIDs(t, s.nextDayMetricIDs.Load(), idbID, date, numNextDayMetricIDs)
// Close the storage to persist nextDayMetricIDs cache to a file.
s.MustClose()
// Open the storage again to ensure that the cache is populated
// correctly.
s = MustOpenStorage(t.Name(), OpenOptions{})
if got := s.pendingNextDayMetricIDs.Len(); got != 0 {
t.Fatalf("unexpected pendingNextDayMetricIDs count: got %d, want 0", got)
}
assertNextDayMetricIDs(t, s.nextDayMetricIDs.Load(), idbID, date, numNextDayMetricIDs)
s.MustClose()
// Advance the time by one day and open the storage.
// Since the current date and the date in the cache file do not match,
// nothing will be loaded into cache.
time.Sleep(24 * time.Hour)
date = uint64(time.Now().UnixMilli()) / msecPerDay
s = MustOpenStorage(t.Name(), OpenOptions{})
if got := s.pendingNextDayMetricIDs.Len(); got != 0 {
t.Fatalf("unexpected pendingNextDayMetricIDs count: got %d, want 0", got)
}
assertNextDayMetricIDs(t, s.nextDayMetricIDs.Load(), idbID, date, 0)
s.MustClose()
})
}
func TestStorageLastPartitionMetrics(t *testing.T) {
defer testRemoveAll(t)
synctest.Test(t, func(t *testing.T) {
// Advance current time to 2h before the next month, 2000-01-31T22:00:00Z.
time.Sleep(31*24*time.Hour - 2*time.Hour)
ct := time.Now().UTC()
s := MustOpenStorage(t.Name(), OpenOptions{})
defer s.MustClose()
assertLastPartitionEmpty := func() {
t.Helper()
var m Metrics
s.UpdateMetrics(&m)
lpm := m.TableMetrics.LastPartition
if lpm.SmallPartsCount != 0 {
t.Fatalf("unexpected last partition SmallPartsCount: got %d, want 0", lpm.SmallPartsCount)
}
if lpm.IndexDBMetrics.FileBlocksCount != 0 {
t.Fatalf("unexpected last partition IndexDBMetrics.FileBlocksCount: got %d, want 0", lpm.IndexDBMetrics.FileBlocksCount)
}
}
assertLastPartitionNonEmpty := func() {
t.Helper()
var m Metrics
s.UpdateMetrics(&m)
lpm := m.TableMetrics.LastPartition
if lpm.SmallPartsCount == 0 {
t.Fatalf("unexpected last partition SmallPartsCount: got 0, want > 0")
}
if lpm.IndexDBMetrics.FileBlocksCount == 0 {
t.Fatalf("unexpected last partition IndexDBMetrics.FileBlocksCount: got 0, want > 0")
}
}
// make sure last partition is empty before ingestion
assertLastPartitionEmpty()
const numSeries = 1000
rng := rand.New(rand.NewSource(1))
tr := TimeRange{
MinTimestamp: ct.Add(-time.Hour).UnixMilli(),
MaxTimestamp: ct.UnixMilli(),
}
mrs := testGenerateMetricRowsWithPrefix(rng, numSeries, "metric.", tr)
s.AddRows(mrs, 1)
s.DebugFlush()
if got, want := s.newTimeseriesCreated.Load(), uint64(numSeries); got != want {
t.Errorf("unexpected number of new timeseries: got %d, want %d", got, want)
}
// wait for merged parts to be attached to the table
time.Sleep(time.Minute)
// last created partition is empty, but we're still at current month
assertLastPartitionNonEmpty()
// Advance current time to the the next month, 2000-02-01T00:30:00Z.
// last partition must be 2000-02 now
time.Sleep(2*time.Hour + time.Minute*30)
// current month partition has no data ingested
assertLastPartitionEmpty()
})
}