Files
metrics/lib/storage/storage_legacy_test.go

1431 lines
48 KiB
Go

package storage
import (
"fmt"
"math"
"math/rand"
"os"
"path/filepath"
"slices"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/VictoriaMetrics/VictoriaMetrics/lib/fs"
"github.com/google/go-cmp/cmp"
)
func TestLegacyStorage_SearchMetricNames(t *testing.T) {
genData := func(numMetrics int, prefix string, tr TimeRange) ([]MetricRow, []string) {
mrs := make([]MetricRow, numMetrics)
want := make([]string, numMetrics)
for i := range numMetrics {
name := fmt.Sprintf("%s_metric_%03d", prefix, i)
mn := MetricName{
MetricGroup: []byte(name),
}
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
mrs[i].Timestamp = rand.Int63n(tr.MaxTimestamp-tr.MinTimestamp) + tr.MinTimestamp
mrs[i].Value = float64(i)
want[i] = name
}
return mrs, want
}
const numMetrics = 1000
tr := TimeRange{
MinTimestamp: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
newData, wantNew := genData(numMetrics, "new", tr)
wantNew = append(wantNew, wantLegacy...)
slices.Sort(wantNew)
assertSearchResults := func(s *Storage, want []string) {
t.Helper()
tfsAll := NewTagFilters()
if err := tfsAll.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
t.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
tfssAll := []*TagFilters{tfsAll}
got, err := s.SearchMetricNames(nil, tfssAll, tr, 1e9, noDeadline)
if err != nil {
t.Fatalf("SearchMetricNames() failed unexpectedly: %v", err)
}
for i, name := range got {
var mn MetricName
if err := mn.UnmarshalString(name); err != nil {
t.Fatalf("Could not unmarshal metric name %q: %v", name, err)
}
got[i] = string(mn.MetricGroup)
}
slices.Sort(got)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("unexpected metric names (-want, +got):\n%s", diff)
}
}
assertLegacyData := func(s *Storage) {
assertSearchResults(s, wantLegacy)
}
assertNewData := func(s *Storage) {
assertSearchResults(s, wantNew)
}
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
}
func TestLegacyStorage_SearchLabelNames(t *testing.T) {
genData := func(numMetrics int, prefix string, tr TimeRange) ([]MetricRow, []string) {
mrs := make([]MetricRow, numMetrics)
want := make([]string, numMetrics)
for i := range numMetrics {
name := fmt.Sprintf("%s_label_%03d", prefix, i)
mn := MetricName{
MetricGroup: []byte("metric"),
Tags: []Tag{
{[]byte(name), []byte("value")},
},
}
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
mrs[i].Timestamp = rand.Int63n(tr.MaxTimestamp-tr.MinTimestamp) + tr.MinTimestamp
mrs[i].Value = float64(i)
want[i] = name
}
return mrs, want
}
const numMetrics = 1000
tr := TimeRange{
MinTimestamp: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
newData, wantNew := genData(numMetrics, "new", tr)
wantNew = append(wantNew, wantLegacy...)
assertSearchResults := func(s *Storage, want []string) {
t.Helper()
got, err := s.SearchLabelNames(nil, nil, tr, 1e9, 1e9, noDeadline)
if err != nil {
t.Fatalf("SearchLabelNames() failed unexpectedly: %v", err)
}
slices.Sort(got)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("unexpected label names (-want, +got):\n%s", diff)
}
}
assertLegacyData := func(s *Storage) {
want := append(wantLegacy, "__name__")
slices.Sort(want)
assertSearchResults(s, want)
}
assertNewData := func(s *Storage) {
want := append(wantNew, "__name__")
slices.Sort(want)
assertSearchResults(s, want)
}
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
}
func TestLegacyStorage_SearchLabelValues(t *testing.T) {
genData := func(numMetrics int, prefix string, tr TimeRange) ([]MetricRow, []string) {
mrs := make([]MetricRow, numMetrics)
want := make([]string, numMetrics)
for i := range numMetrics {
value := fmt.Sprintf("%s_value_%03d", prefix, i)
mn := MetricName{
MetricGroup: []byte("metric"),
Tags: []Tag{
{[]byte("label"), []byte(value)},
},
}
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
mrs[i].Timestamp = rand.Int63n(tr.MaxTimestamp-tr.MinTimestamp) + tr.MinTimestamp
mrs[i].Value = float64(i)
want[i] = value
}
return mrs, want
}
const numMetrics = 1000
tr := TimeRange{
MinTimestamp: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
newData, wantNew := genData(numMetrics, "new", tr)
wantNew = append(wantNew, wantLegacy...)
slices.Sort(wantNew)
assertSearchResults := func(s *Storage, want []string) {
t.Helper()
got, err := s.SearchLabelValues(nil, "label", nil, tr, 1e9, 1e9, noDeadline)
if err != nil {
t.Fatalf("SearchLabelValues() failed unexpectedly: %v", err)
}
slices.Sort(got)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("unexpected label values (-want, +got):\n%s", diff)
}
}
assertLegacyData := func(s *Storage) {
t.Helper()
assertSearchResults(s, wantLegacy)
}
assertNewData := func(s *Storage) {
t.Helper()
assertSearchResults(s, wantNew)
}
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
}
func TestLegacyStorage_SearchTagValueSuffixes(t *testing.T) {
genData := func(numMetrics int, prefix string, tr TimeRange) ([]MetricRow, []string) {
mrs := make([]MetricRow, numMetrics)
want := make([]string, numMetrics)
for i := range numMetrics {
name := fmt.Sprintf("%s_metric_%03d", prefix, i)
mn := MetricName{
MetricGroup: []byte("prefix." + name),
}
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
mrs[i].Timestamp = rand.Int63n(tr.MaxTimestamp-tr.MinTimestamp) + tr.MinTimestamp
mrs[i].Value = float64(i)
want[i] = name
}
return mrs, want
}
const numMetrics = 1000
tr := TimeRange{
MinTimestamp: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
newData, wantNew := genData(numMetrics, "new", tr)
wantNew = append(wantNew, wantLegacy...)
slices.Sort(wantNew)
assertSearchResults := func(s *Storage, want []string) {
t.Helper()
got, err := s.SearchTagValueSuffixes(nil, tr, "", "prefix.", '.', 1e9, noDeadline)
if err != nil {
t.Fatalf("SearchTagValueSuffixes() failed unexpectedly: %v", err)
}
slices.Sort(got)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("unexpected tag value suffixes (-want, +got):\n%s", diff)
}
}
assertLegacyData := func(s *Storage) {
t.Helper()
assertSearchResults(s, wantLegacy)
}
assertNewData := func(s *Storage) {
t.Helper()
assertSearchResults(s, wantNew)
}
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
}
func TestLegacyStorage_SearchGraphitePaths(t *testing.T) {
genData := func(numMetrics int, prefix string, tr TimeRange) ([]MetricRow, []string) {
mrs := make([]MetricRow, numMetrics)
want := make([]string, numMetrics)
for i := range numMetrics {
name := fmt.Sprintf("prefix.%s_metric_%03d", prefix, i)
mn := MetricName{
MetricGroup: []byte(name),
}
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
mrs[i].Timestamp = rand.Int63n(tr.MaxTimestamp-tr.MinTimestamp) + tr.MinTimestamp
mrs[i].Value = float64(i)
want[i] = name
}
return mrs, want
}
const numMetrics = 1000
tr := TimeRange{
MinTimestamp: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
legacyData, wantLegacy := genData(numMetrics, "legacy", tr)
newData, wantNew := genData(numMetrics, "new", tr)
wantNew = append(wantNew, wantLegacy...)
slices.Sort(wantNew)
assertSearchResults := func(s *Storage, want []string) {
t.Helper()
got, err := s.SearchGraphitePaths(nil, tr, []byte("*.*"), 1e9, noDeadline)
if err != nil {
t.Fatalf("SearchTagGraphitePaths() failed unexpectedly: %v", err)
}
slices.Sort(got)
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("unexpected graphite paths (-want, +got):\n%s", diff)
}
}
assertLegacyData := func(s *Storage) {
t.Helper()
assertSearchResults(s, wantLegacy)
}
assertNewData := func(s *Storage) {
t.Helper()
assertSearchResults(s, wantNew)
}
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
}
func TestLegacyStorage_Search(t *testing.T) {
genData := func(numMetrics int, prefix string, tr TimeRange) []MetricRow {
mrs := make([]MetricRow, numMetrics)
for i := range numMetrics {
name := fmt.Sprintf("%s_metric_%03d", prefix, i)
mn := MetricName{
MetricGroup: []byte(name),
}
mrs[i].MetricNameRaw = mn.marshalRaw(nil)
mrs[i].Timestamp = rand.Int63n(tr.MaxTimestamp-tr.MinTimestamp) + tr.MinTimestamp
mrs[i].Value = float64(i)
}
return mrs
}
const numMetrics = 1000
tr := TimeRange{
MinTimestamp: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
legacyData := genData(numMetrics, "legacy", tr)
newData := genData(numMetrics, "new", tr)
assertSearchResults := func(s *Storage, want []MetricRow) {
tfsAll := NewTagFilters()
if err := tfsAll.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
t.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
if err := testAssertSearchResult(s, tr, tfsAll, want); err != nil {
t.Fatalf("unexpected search results: %v", err)
}
}
assertLegacyData := func(s *Storage) {
t.Helper()
want := legacyData
assertSearchResults(s, want)
}
assertNewData := func(s *Storage) {
t.Helper()
want := slices.Concat(legacyData, newData)
assertSearchResults(s, want)
}
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
}
func TestLegacyStorage_GetSeriesCount(t *testing.T) {
const numMetrics = 1000
tr := TimeRange{
MinTimestamp: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
rng := rand.New(rand.NewSource(1))
legacyData := testGenerateMetricRowsWithPrefix(rng, numMetrics, "legacy", tr)
newData := testGenerateMetricRowsWithPrefix(rng, numMetrics, "new", tr)
assertSearchResults := func(s *Storage, want uint64) {
t.Helper()
got, err := s.GetSeriesCount(noDeadline)
if err != nil {
t.Fatalf("GetSeriesCount() failed unexpectedly: %v", err)
}
if got != want {
t.Fatalf("unexpected metric count: got %d, want %d", got, want)
}
}
assertLegacyData := func(s *Storage) {
t.Helper()
want := uint64(len(legacyData))
assertSearchResults(s, want)
}
assertNewData := func(s *Storage) {
t.Helper()
want := uint64(len(legacyData) + len(newData))
assertSearchResults(s, want)
}
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
}
func TestLegacyStorage_DeleteSeries(t *testing.T) {
const numMetrics = 1000
tr := TimeRange{
MinTimestamp: time.Date(2023, 6, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 5, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
rng := rand.New(rand.NewSource(1))
legacyData := testGenerateMetricRowsWithPrefix(rng, numMetrics, "legacy", tr)
newData := testGenerateMetricRowsWithPrefix(rng, numMetrics, "new", tr)
tfsAll := NewTagFilters()
if err := tfsAll.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
t.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
tfssAll := []*TagFilters{tfsAll}
assertSeriesCount := func(s *Storage, want int) {
t.Helper()
got, err := s.SearchMetricNames(nil, tfssAll, tr, 1e9, noDeadline)
if err != nil {
t.Fatalf("SearchMetricNames() failed unexpectedly: %v", err)
}
if len(got) != want {
t.Fatalf("unexpected metric count: got %d, want %d", len(got), want)
}
}
assertLegacyData := func(s *Storage) {
t.Helper()
want := len(legacyData)
assertSeriesCount(s, want)
}
assertNewData := func(s *Storage) {
t.Helper()
want := len(legacyData) + len(newData)
assertSeriesCount(s, want)
got, err := s.DeleteSeries(nil, tfssAll, 1e9)
if err != nil {
t.Fatalf("DeleteSeries() failed unexpectedly: %v", err)
}
if got != want {
t.Fatalf("Unexpected number of deleted series: got %d, want %d", got, want)
}
assertSeriesCount(s, 0)
}
testSearchOpWithLegacyIndexDBs(t, legacyData, newData, assertLegacyData, assertNewData)
}
// testSearchWithLegacyIndexDBs a search operation when the index data
// is located both partition and legacy indexDBs.
func testSearchOpWithLegacyIndexDBs(t *testing.T, legacyData, newData []MetricRow, assertLegacyData, assertNewData func(s *Storage)) {
defer testRemoveAll(t)
s := MustOpenStorage(t.Name(), OpenOptions{})
s.AddRows(legacyData, defaultPrecisionBits)
s.DebugFlush()
assertLegacyData(s)
s = mustConvertToLegacy(s)
assertLegacyData(s)
s.AddRows(newData, defaultPrecisionBits)
s.DebugFlush()
assertNewData(s)
s.MustClose()
}
func TestLegacyStorageSnapshots_CreateListDelete(t *testing.T) {
defer testRemoveAll(t)
rng := rand.New(rand.NewSource(1))
const numRows = 10000
minTimestamp := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli()
maxTimestamp := time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC).UnixMilli()
mrs := testGenerateMetricRows(rng, numRows, minTimestamp, maxTimestamp)
root := t.Name()
s := MustOpenStorage(root, OpenOptions{})
s.AddRows(mrs, defaultPrecisionBits)
s.DebugFlush()
// Convert to legacy 2 times in order to have both prev and curr legacy idbs.
s = mustConvertToLegacy(s)
s.AddRows(mrs, defaultPrecisionBits)
s.DebugFlush()
s = mustConvertToLegacy(s)
defer s.MustClose()
s.AddRows(mrs, defaultPrecisionBits)
s.DebugFlush()
var (
data = filepath.Join(root, dataDirname)
smallData = filepath.Join(data, smallDirname)
bigData = filepath.Join(data, bigDirname)
indexData = filepath.Join(data, indexdbDirname)
smallSnapshots = filepath.Join(smallData, snapshotsDirname)
bigSnapshots = filepath.Join(bigData, snapshotsDirname)
indexSnapshots = filepath.Join(indexData, snapshotsDirname)
legacyIndexData = filepath.Join(root, indexdbDirname)
legacyIndexSnapshots = filepath.Join(legacyIndexData, snapshotsDirname)
)
snapshot1Name := s.MustCreateSnapshot()
assertListSnapshots := func(want []string) {
got := s.MustListSnapshots()
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("unexpected snapshot list (-want, +got):\n%s", diff)
}
}
assertListSnapshots([]string{snapshot1Name})
var (
snapshot1 = filepath.Join(root, snapshotsDirname, snapshot1Name)
smallSnapshot1 = filepath.Join(smallSnapshots, snapshot1Name)
smallSymlink1 = filepath.Join(snapshot1, dataDirname, smallDirname)
bigSnapshot1 = filepath.Join(bigSnapshots, snapshot1Name)
bigSymlink1 = filepath.Join(snapshot1, dataDirname, bigDirname)
indexSnapshot1 = filepath.Join(indexSnapshots, snapshot1Name)
indexSymlink1 = filepath.Join(snapshot1, dataDirname, indexdbDirname)
legacyIndexSnapshot1 = filepath.Join(legacyIndexSnapshots, snapshot1Name)
legacyIndexSymlink1 = filepath.Join(snapshot1, indexdbDirname)
)
// Check snapshot1 dir entries
assertDirEntries := func(srcDir, snapshotDir string, excludePath ...string) {
t.Helper()
dataDirEntries := testListDirEntries(t, srcDir, excludePath...)
snapshotDirEntries := testListDirEntries(t, snapshotDir)
if diff := cmp.Diff(dataDirEntries, snapshotDirEntries); diff != "" {
t.Fatalf("unexpected snapshot dir entries (-want, +got):\n%s", diff)
}
}
assertDirEntries(smallData, smallSnapshot1, smallSnapshots)
assertDirEntries(bigData, bigSnapshot1, bigSnapshots)
assertDirEntries(indexData, indexSnapshot1, indexSnapshots)
assertDirEntries(legacyIndexData, legacyIndexSnapshot1, legacyIndexSnapshots)
// Check snapshot1 symlinks
assertSymlink := func(symlink string, wantRealpath string) {
t.Helper()
gotRealpath, err := filepath.EvalSymlinks(symlink)
if err != nil {
t.Fatalf("Could not evaluate symlink %q: %v", symlink, err)
}
if gotRealpath != wantRealpath {
t.Fatalf("unexpected realpath for symlink %q: got %q, want %q", symlink, gotRealpath, wantRealpath)
}
}
assertSymlink(bigSymlink1, bigSnapshot1)
assertSymlink(smallSymlink1, smallSnapshot1)
assertSymlink(indexSymlink1, indexSnapshot1)
assertSymlink(legacyIndexSymlink1, legacyIndexSnapshot1)
// Rotate indexdb. Only one legacy indexDB must remain.
s.legacyMustRotateIndexDB(time.Now().UTC())
// Create snapshot2
snapshot2Name := s.MustCreateSnapshot()
assertListSnapshots([]string{snapshot1Name, snapshot2Name})
var (
snapshot2 = filepath.Join(root, snapshotsDirname, snapshot2Name)
smallSnapshot2 = filepath.Join(smallSnapshots, snapshot2Name)
smallSymlink2 = filepath.Join(snapshot2, dataDirname, smallDirname)
bigSnapshot2 = filepath.Join(bigSnapshots, snapshot2Name)
bigSymlink2 = filepath.Join(snapshot2, dataDirname, bigDirname)
indexSnapshot2 = filepath.Join(indexSnapshots, snapshot2Name)
indexSymlink2 = filepath.Join(snapshot2, dataDirname, indexdbDirname)
legacyIndexSnapshot2 = filepath.Join(legacyIndexSnapshots, snapshot2Name)
legacyIndexSymlink2 = filepath.Join(snapshot2, indexdbDirname)
)
// Check snapshot2 dir entries
assertDirEntries(smallData, smallSnapshot2, smallSnapshots)
assertDirEntries(bigData, bigSnapshot2, bigSnapshots)
assertDirEntries(indexData, indexSnapshot2, indexSnapshots)
assertDirEntries(legacyIndexData, legacyIndexSnapshot2, legacyIndexSnapshots)
// Check snapshot2 symlinks
assertSymlink(bigSymlink2, bigSnapshot2)
assertSymlink(smallSymlink2, smallSnapshot2)
assertSymlink(indexSymlink2, indexSnapshot2)
assertSymlink(legacyIndexSymlink2, legacyIndexSnapshot2)
// Rotate indexdb once again. There shouldn't be any legacy indexDBs left.
s.legacyMustRotateIndexDB(time.Now().UTC())
// Create snapshot3
snapshot3Name := s.MustCreateSnapshot()
assertListSnapshots([]string{snapshot1Name, snapshot2Name, snapshot3Name})
var (
snapshot3 = filepath.Join(root, snapshotsDirname, snapshot3Name)
smallSnapshot3 = filepath.Join(smallSnapshots, snapshot3Name)
smallSymlink3 = filepath.Join(snapshot3, dataDirname, smallDirname)
bigSnapshot3 = filepath.Join(bigSnapshots, snapshot3Name)
bigSymlink3 = filepath.Join(snapshot3, dataDirname, bigDirname)
indexSnapshot3 = filepath.Join(indexSnapshots, snapshot3Name)
indexSymlink3 = filepath.Join(snapshot3, dataDirname, indexdbDirname)
legacyIndexSnapshot3 = filepath.Join(legacyIndexSnapshots, snapshot3Name)
legacyIndexSymlink3 = filepath.Join(snapshot3, indexdbDirname)
)
assertPathDoesNotExist := func(path string) {
t.Helper()
if fs.IsPathExist(path) {
t.Fatalf("path was not expected to exist: %q", path)
}
}
// Check snapshot3 dir entries
assertDirEntries(smallData, smallSnapshot3, smallSnapshots)
assertDirEntries(bigData, bigSnapshot3, bigSnapshots)
assertDirEntries(indexData, indexSnapshot3, indexSnapshots)
assertPathDoesNotExist(legacyIndexSnapshot3)
// Check snapshot3 symlinks
assertSymlink(bigSymlink3, bigSnapshot3)
assertSymlink(smallSymlink3, smallSnapshot3)
assertSymlink(indexSymlink3, indexSnapshot3)
assertPathDoesNotExist(legacyIndexSymlink3)
// Check snapshot deletion.
for _, name := range []string{snapshot1Name, snapshot2Name, snapshot3Name} {
if err := s.DeleteSnapshot(name); err != nil {
t.Fatalf("could not delete snapshot %q: %v", name, err)
}
}
assertListSnapshots([]string{})
assertPathDoesNotExist(snapshot1)
assertPathDoesNotExist(snapshot2)
assertPathDoesNotExist(snapshot3)
assertPathDoesNotExist(bigSnapshot1)
assertPathDoesNotExist(bigSnapshot2)
assertPathDoesNotExist(bigSnapshot3)
assertPathDoesNotExist(smallSnapshot1)
assertPathDoesNotExist(smallSnapshot2)
assertPathDoesNotExist(smallSnapshot3)
assertPathDoesNotExist(indexSnapshot1)
assertPathDoesNotExist(indexSnapshot2)
assertPathDoesNotExist(indexSnapshot3)
assertPathDoesNotExist(legacyIndexSnapshot1)
assertPathDoesNotExist(legacyIndexSnapshot2)
}
func TestStorageConvertToLegacy(t *testing.T) {
defer testRemoveAll(t)
assertMetricNames := func(s *Storage, tr TimeRange, wantMRs []MetricRow) {
t.Helper()
tfs := NewTagFilters()
if err := tfs.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
t.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
got, err := s.SearchMetricNames(nil, []*TagFilters{tfs}, tr, 1e9, noDeadline)
if err != nil {
t.Fatalf("SearchMetricNames() failed unexpectedly: %v", err)
}
var mn MetricName
for i, name := range got {
if err := mn.UnmarshalString(name); err != nil {
t.Fatalf("could not unmarshal metric name %q: %v", name, err)
}
got[i] = string(mn.MetricGroup)
}
slices.Sort(got)
want := make([]string, len(wantMRs))
for i, mr := range wantMRs {
if err := mn.UnmarshalRaw(mr.MetricNameRaw); err != nil {
t.Fatalf("could not unmarshal raw metric name %v: %v", mr.MetricNameRaw, err)
}
want[i] = string(mn.MetricGroup)
}
slices.Sort(want)
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("unexpected metric names (-want, +got):\n%s", diff)
}
}
rng := rand.New(rand.NewSource(1))
const numSeries = 10
tr1 := TimeRange{
MinTimestamp: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2025, 1, 1, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
tr2 := TimeRange{
MinTimestamp: tr1.MinTimestamp + msecPerDay,
MaxTimestamp: tr1.MaxTimestamp + msecPerDay,
}
tr3 := TimeRange{
MinTimestamp: tr2.MinTimestamp + msecPerDay,
MaxTimestamp: tr2.MaxTimestamp + msecPerDay,
}
tr4 := TimeRange{
MinTimestamp: tr3.MinTimestamp + msecPerDay,
MaxTimestamp: tr3.MaxTimestamp + msecPerDay,
}
trAll := TimeRange{
MinTimestamp: tr1.MinTimestamp,
MaxTimestamp: tr4.MaxTimestamp,
}
mrs1 := testGenerateMetricRowsWithPrefix(rng, numSeries, "generation1", tr1)
mrs2 := testGenerateMetricRowsWithPrefix(rng, numSeries, "generation2", tr2)
mrs3 := testGenerateMetricRowsWithPrefix(rng, numSeries, "generation3", tr3)
mrs4 := testGenerateMetricRowsWithPrefix(rng, numSeries, "generation4", tr4)
s := MustOpenStorage(t.Name(), OpenOptions{})
s.AddRows(mrs1, defaultPrecisionBits)
s.DebugFlush()
s = mustConvertToLegacy(s)
assertMetricNames(s, trAll, mrs1)
s.AddRows(mrs2, defaultPrecisionBits)
s.DebugFlush()
s = mustConvertToLegacy(s)
assertMetricNames(s, trAll, slices.Concat(mrs1, mrs2))
s.AddRows(mrs3, defaultPrecisionBits)
s.DebugFlush()
s = mustConvertToLegacy(s)
assertMetricNames(s, trAll, slices.Concat(mrs2, mrs3))
s.AddRows(mrs4, defaultPrecisionBits)
s.DebugFlush()
s = mustConvertToLegacy(s)
assertMetricNames(s, trAll, slices.Concat(mrs3, mrs4))
s.MustClose()
}
// mustConvertToLegacy converts the storage partition indexDBs into a
// legacy indexDB. The original partition indexDBs are removed.
//
// Each invocation of this function will a new legacy indexDB in
// storageDataPath/indexdb dir. The function will keep only 2 most recent
// indexDBs under that path.
//
// The function also deteles all persistent caches.
func mustConvertToLegacy(s *Storage) *Storage {
// Stop storage, move legacy idbs to tmp dir, delete all caches,
// re-open storage with pt index only.
storageDataPath := s.path
s.MustClose()
legacyIDBsPathOrig := filepath.Join(s.path, indexdbDirname)
fs.MustMkdirIfNotExist(legacyIDBsPathOrig)
legacyIDBsPathTmp := filepath.Join(s.path, "indexdb-legacy")
if err := os.Rename(legacyIDBsPathOrig, legacyIDBsPathTmp); err != nil {
panic(fmt.Sprintf("could not rename %q to %q: %v", legacyIDBsPathOrig, legacyIDBsPathTmp, err))
}
fs.MustRemoveDir(filepath.Join(storageDataPath, cacheDirname))
s = MustOpenStorage(storageDataPath, OpenOptions{})
legacyIDBID := uint64(time.Now().UnixNano())
legacyIDBName := fmt.Sprintf("%016X", legacyIDBID)
legacyIDBPath := filepath.Join(legacyIDBsPathTmp, legacyIDBName)
fs.MustMkdirFailIfExist(legacyIDBPath)
legacyIDBPartsFile := filepath.Join(legacyIDBPath, partsFilename)
fs.MustWriteAtomic(legacyIDBPartsFile, []byte("[]"), true)
legacyIDBTimeRange := TimeRange{
MinTimestamp: 0,
MaxTimestamp: math.MaxInt64,
}
var isReadOnly atomic.Bool
isReadOnly.Store(false)
legacyIDB := mustOpenIndexDB(legacyIDBID, legacyIDBTimeRange, legacyIDBName, legacyIDBPath, s, &isReadOnly, false)
// Read index items from the partition indexDBs and write them to the legacy
// indexDB.
ptws := s.tb.GetPartitions(legacyIDBTimeRange)
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}
seenGlobalIndexEntries := make(map[uint64]bool)
type dateMetricID struct {
date uint64
metricID uint64
}
seenPerDayIndexEntries := make(map[dateMetricID]bool)
for _, ptw := range ptws {
idb := ptw.pt.idb
for ts := idb.tr.MinTimestamp; ts < idb.tr.MaxTimestamp; ts += msecPerDay {
day := TimeRange{
MinTimestamp: ts,
MaxTimestamp: ts + msecPerDay - 1,
}
date := uint64(ts / msecPerDay)
tsids, err := idb.SearchTSIDs(nil, tfssAll, day, 1e9, noDeadline)
if err != nil {
panic(fmt.Sprintf("could not get TSIDs: %v", err))
}
for _, tsid := range tsids {
metricID := tsid.MetricID
mnBytes, ok := idb.searchMetricName(nil, metricID, false)
if !ok {
panic(fmt.Sprintf("could not get metric name for metricID %d", metricID))
}
var mn MetricName
if err := mn.Unmarshal(mnBytes); err != nil {
panic(fmt.Sprintf("Could not unmarshal metric name from bytes %q: %v", string(mnBytes), err))
}
if !seenGlobalIndexEntries[metricID] {
legacyIDB.createGlobalIndexes(&tsid, &mn)
seenGlobalIndexEntries[metricID] = true
}
dateMetricID := dateMetricID{
date: date,
metricID: metricID,
}
if !seenPerDayIndexEntries[dateMetricID] {
legacyIDB.createPerDayIndexes(date, &tsid, &mn)
seenPerDayIndexEntries[dateMetricID] = true
}
}
}
is := idb.getIndexSearch(noDeadline)
dmis, err := is.loadDeletedMetricIDs()
idb.putIndexSearch(is)
if err != nil {
panic(fmt.Sprintf("cannot load deleted metricIDs for indexDB %q: %v", idb.name, err))
}
legacyIDB.saveDeletedMetricIDs(dmis)
}
s.tb.PutPartitions(ptws)
legacyIDB.MustClose()
// Stop storage, delete partition idbs, remove caches, move legacy idb dir
// to its original location, keep only 2 recent legacy idbs.
s.MustClose()
fs.MustRemoveDir(filepath.Join(storageDataPath, dataDirname, indexdbDirname))
fs.MustRemoveDir(filepath.Join(storageDataPath, cacheDirname))
if err := os.Rename(legacyIDBsPathTmp, legacyIDBsPathOrig); err != nil {
panic(fmt.Sprintf("could not rename %q to %q: %v", legacyIDBsPathTmp, legacyIDBsPathOrig, err))
}
entries := fs.MustReadDir(legacyIDBsPathOrig)
var names []string
for _, e := range entries {
names = append(names, e.Name())
}
slices.Sort(names)
if len(names) > 2 {
for _, name := range names[:len(names)-2] {
p := filepath.Join(legacyIDBsPathOrig, name)
fs.MustRemoveDir(p)
}
}
return MustOpenStorage(s.path, OpenOptions{})
}
func TestLegacyNextRetentionDeadlineSeconds(t *testing.T) {
f := func(currentTime string, retention, offset time.Duration, deadlineExpected string) {
t.Helper()
now, err := time.Parse(time.RFC3339, currentTime)
if err != nil {
t.Fatalf("cannot parse currentTime=%q: %s", currentTime, err)
}
d := legacyNextRetentionDeadlineSeconds(now.Unix(), int64(retention.Seconds()), int64(offset.Seconds()))
deadline := time.Unix(d, 0).UTC().Format(time.RFC3339)
if deadline != deadlineExpected {
t.Fatalf("unexpected deadline; got %s; want %s", deadline, deadlineExpected)
}
}
f("2023-07-22T12:44:35Z", 24*time.Hour, 0, "2023-07-23T04:00:00Z")
f("2023-07-22T03:44:35Z", 24*time.Hour, 0, "2023-07-22T04:00:00Z")
f("2023-07-22T04:44:35Z", 24*time.Hour, 0, "2023-07-23T04:00:00Z")
f("2023-07-22T23:44:35Z", 24*time.Hour, 0, "2023-07-23T04:00:00Z")
f("2023-07-23T03:59:35Z", 24*time.Hour, 0, "2023-07-23T04:00:00Z")
f("2023-07-22T12:44:35Z", 24*time.Hour, 2*time.Hour, "2023-07-23T02:00:00Z")
f("2023-07-22T01:44:35Z", 24*time.Hour, 2*time.Hour, "2023-07-22T02:00:00Z")
f("2023-07-22T02:44:35Z", 24*time.Hour, 2*time.Hour, "2023-07-23T02:00:00Z")
f("2023-07-22T23:44:35Z", 24*time.Hour, 2*time.Hour, "2023-07-23T02:00:00Z")
f("2023-07-23T01:59:35Z", 24*time.Hour, 2*time.Hour, "2023-07-23T02:00:00Z")
f("2023-07-22T12:44:35Z", 24*time.Hour, -5*time.Hour, "2023-07-23T09:00:00Z")
f("2023-07-22T08:44:35Z", 24*time.Hour, -5*time.Hour, "2023-07-22T09:00:00Z")
f("2023-07-22T09:44:35Z", 24*time.Hour, -5*time.Hour, "2023-07-23T09:00:00Z")
f("2023-07-22T12:44:35Z", 24*time.Hour, -12*time.Hour, "2023-07-22T16:00:00Z")
f("2023-07-22T15:44:35Z", 24*time.Hour, -12*time.Hour, "2023-07-22T16:00:00Z")
f("2023-07-22T16:44:35Z", 24*time.Hour, -12*time.Hour, "2023-07-23T16:00:00Z")
f("2023-07-22T12:44:35Z", 24*time.Hour, -18*time.Hour, "2023-07-22T22:00:00Z")
f("2023-07-22T21:44:35Z", 24*time.Hour, -18*time.Hour, "2023-07-22T22:00:00Z")
f("2023-07-22T22:44:35Z", 24*time.Hour, -18*time.Hour, "2023-07-23T22:00:00Z")
f("2023-07-22T12:44:35Z", 24*time.Hour, 18*time.Hour, "2023-07-23T10:00:00Z")
f("2023-07-22T09:44:35Z", 24*time.Hour, 18*time.Hour, "2023-07-22T10:00:00Z")
f("2023-07-22T10:44:35Z", 24*time.Hour, 18*time.Hour, "2023-07-23T10:00:00Z")
f("2023-07-22T12:44:35Z", 24*time.Hour, 37*time.Hour, "2023-07-22T15:00:00Z")
f("2023-07-22T14:44:35Z", 24*time.Hour, 37*time.Hour, "2023-07-22T15:00:00Z")
f("2023-07-22T15:44:35Z", 24*time.Hour, 37*time.Hour, "2023-07-23T15:00:00Z")
// The test cases below confirm that it is possible to pick a retention
// period such that the previous IndexDB may be removed earlier than it should be.
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/7609
// Cluster is configured with 12 month retentionPeriod on 2023-01-01.
f("2023-01-01T00:00:00Z", 365*24*time.Hour, 0, "2023-12-19T04:00:00Z")
// Restarts during that period do not change the retention deadline:
f("2023-03-01T00:00:00Z", 365*24*time.Hour, 0, "2023-12-19T04:00:00Z")
f("2023-06-01T00:00:00Z", 365*24*time.Hour, 0, "2023-12-19T04:00:00Z")
f("2023-09-01T00:00:00Z", 365*24*time.Hour, 0, "2023-12-19T04:00:00Z")
f("2023-12-01T00:00:00Z", 365*24*time.Hour, 0, "2023-12-19T04:00:00Z")
f("2023-12-19T03:59:59Z", 365*24*time.Hour, 0, "2023-12-19T04:00:00Z")
// At 2023-12-19T04:00:00Z the rotation occurs. New deadline is
// 2024-12-18T04:00:00Z. Restarts during that period do not change the
// new deadline:
f("2023-12-19T04:00:01Z", 365*24*time.Hour, 0, "2024-12-18T04:00:00Z")
f("2024-01-01T00:00:00Z", 365*24*time.Hour, 0, "2024-12-18T04:00:00Z")
f("2024-03-01T00:00:00Z", 365*24*time.Hour, 0, "2024-12-18T04:00:00Z")
f("2024-04-29T00:00:00Z", 365*24*time.Hour, 0, "2024-12-18T04:00:00Z")
// Now restart again but with the new retention period of 451d and the
// rotation time becomes 2024-05-01T04:00:00Z.
//
// At 2024-05-01T04:00:00Z, a new IndexDB is created and the current
// IndexDB (currently applicable to only ~4 months of data) becomes the
// previous IndexDB. The preceding IndexDB is deleted despite possibly
// being related to ~8 months of data that is still within retention.
f("2024-04-29T00:00:00Z", 451*24*time.Hour, 0, "2024-05-01T04:00:00Z")
}
func TestLegacyStorageRotateIndexDB_AddRows(t *testing.T) {
rng := rand.New(rand.NewSource(1))
tr := TimeRange{
MinTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 1, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
mrs := testGenerateMetricRowsWithPrefix(rng, 1000, "metric", tr)
op := func(s *Storage) {
s.AddRows(mrs, defaultPrecisionBits)
s.DebugFlush()
}
testLegacyRotateIndexDB(t, mrs, op)
}
func TestLegacyStorageRotateIndexDB_RegisterMetricNames(t *testing.T) {
rng := rand.New(rand.NewSource(1))
tr := TimeRange{
MinTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 1, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
mrs := testGenerateMetricRowsWithPrefix(rng, 1000, "metric", tr)
op := func(s *Storage) {
s.RegisterMetricNames(nil, mrs)
s.DebugFlush()
}
testLegacyRotateIndexDB(t, mrs, op)
}
func TestLegacyStorageRotateIndexDB_DeleteSeries(t *testing.T) {
rng := rand.New(rand.NewSource(1))
tr := TimeRange{
MinTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 1, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
mrs := testGenerateMetricRowsWithPrefix(rng, 1000, "metric", tr)
tfs := NewTagFilters()
if err := tfs.Add(nil, []byte("metric.*"), false, true); err != nil {
t.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
op := func(s *Storage) {
_, err := s.DeleteSeries(nil, []*TagFilters{tfs}, 1e9)
if err != nil {
panic(fmt.Sprintf("DeleteSeries() failed unexpectedly: %v", err))
}
}
testLegacyRotateIndexDB(t, mrs, op)
}
func TestLegacyStorageRotateIndexDB_CreateSnapshot(t *testing.T) {
rng := rand.New(rand.NewSource(1))
tr := TimeRange{
MinTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 1, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
mrs := testGenerateMetricRowsWithPrefix(rng, 1000, "metric", tr)
op := func(s *Storage) {
_ = s.MustCreateSnapshot()
}
testLegacyRotateIndexDB(t, mrs, op)
}
func TestLegacyStorageRotateIndexDB_SearchMetricNames(t *testing.T) {
rng := rand.New(rand.NewSource(1))
tr := TimeRange{
MinTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 1, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
mrs := testGenerateMetricRowsWithPrefix(rng, 1000, "metric", tr)
tfs := NewTagFilters()
if err := tfs.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
t.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
tfss := []*TagFilters{tfs}
op := func(s *Storage) {
_, err := s.SearchMetricNames(nil, tfss, tr, 1e9, noDeadline)
if err != nil {
panic(fmt.Sprintf("SearchMetricNames() failed unexpectedly: %v", err))
}
}
testLegacyRotateIndexDB(t, mrs, op)
}
func TestLegacyStorageRotateIndexDB_SearchLabelNames(t *testing.T) {
rng := rand.New(rand.NewSource(1))
tr := TimeRange{
MinTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 1, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
mrs := testGenerateMetricRowsWithPrefix(rng, 1000, "metric", tr)
testLegacyRotateIndexDB(t, mrs, func(s *Storage) {
_, err := s.SearchLabelNames(nil, []*TagFilters{}, tr, 1e6, 1e6, noDeadline)
if err != nil {
panic(fmt.Sprintf("SearchLabelNames() failed unexpectedly: %v", err))
}
})
}
func TestLegacyStorageRotateIndexDB_SearchLabelValues(t *testing.T) {
rng := rand.New(rand.NewSource(1))
tr := TimeRange{
MinTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 1, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
mrs := testGenerateMetricRowsWithPrefix(rng, 1000, "metric", tr)
testLegacyRotateIndexDB(t, mrs, func(s *Storage) {
_, err := s.SearchLabelValues(nil, "__name__", []*TagFilters{}, tr, 1e6, 1e6, noDeadline)
if err != nil {
panic(fmt.Sprintf("SearchLabelValues() failed unexpectedly: %v", err))
}
})
}
func TestLegacyStorageRotateIndexDB_SearchTagValueSuffixes(t *testing.T) {
rng := rand.New(rand.NewSource(1))
tr := TimeRange{
MinTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 1, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
mrs := testGenerateMetricRowsWithPrefix(rng, 1000, "metric.", tr)
testLegacyRotateIndexDB(t, mrs, func(s *Storage) {
_, err := s.SearchTagValueSuffixes(nil, tr, "", "metric.", '.', 1e6, noDeadline)
if err != nil {
panic(fmt.Sprintf("SearchTagValueSuffixes() failed unexpectedly: %v", err))
}
})
}
func TestLegacyStorageRotateIndexDB_SearchGraphitePaths(t *testing.T) {
rng := rand.New(rand.NewSource(1))
tr := TimeRange{
MinTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 1, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
mrs := testGenerateMetricRowsWithPrefix(rng, 1000, "metric.", tr)
testLegacyRotateIndexDB(t, mrs, func(s *Storage) {
_, err := s.SearchGraphitePaths(nil, tr, []byte("*.*"), 1e6, noDeadline)
if err != nil {
panic(fmt.Sprintf("SearchGraphitePaths() failed unexpectedly: %v", err))
}
})
}
func TestLegacyStorageRotateIndexDB_GetSeriesCount(t *testing.T) {
rng := rand.New(rand.NewSource(1))
tr := TimeRange{
MinTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 1, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
mrs := testGenerateMetricRowsWithPrefix(rng, 1000, "metric", tr)
testLegacyRotateIndexDB(t, mrs, func(s *Storage) {
_, err := s.GetSeriesCount(noDeadline)
if err != nil {
panic(fmt.Sprintf("GetSeriesCount() failed unexpectedly: %v", err))
}
})
}
func TestLegacyStorageRotateIndexDB_GetTSDBStatus(t *testing.T) {
rng := rand.New(rand.NewSource(1))
tr := TimeRange{
MinTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 1, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
mrs := testGenerateMetricRowsWithPrefix(rng, 1000, "metric", tr)
date := uint64(tr.MinTimestamp) / msecPerDay
testLegacyRotateIndexDB(t, mrs, func(s *Storage) {
_, err := s.GetTSDBStatus(nil, nil, date, "", 10, 1e6, noDeadline)
if err != nil {
panic(fmt.Sprintf("GetTSDBStatus failed unexpectedly: %v", err))
}
})
}
func TestLegacyStorageRotateIndexDB_NotifyReadWriteMode(t *testing.T) {
op := func(s *Storage) {
// Set readonly so that the background workers started by
// notifyReadWriteMode exit early.
s.isReadOnly.Store(true)
s.notifyReadWriteMode()
}
testLegacyRotateIndexDB(t, []MetricRow{}, op)
}
func TestLegacyStorageRotateIndexDB_UpdateMetrics(t *testing.T) {
op := func(s *Storage) {
s.UpdateMetrics(&Metrics{})
}
testLegacyRotateIndexDB(t, []MetricRow{}, op)
}
func TestLegacyStorageRotateIndexDB_Search(t *testing.T) {
rng := rand.New(rand.NewSource(1))
tr := TimeRange{
MinTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 1, 31, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
mrs := testGenerateMetricRowsWithPrefix(rng, 1000, "metric", tr)
tfs := NewTagFilters()
if err := tfs.Add([]byte("__name__"), []byte(".*"), false, true); err != nil {
t.Fatalf("unexpected error in TagFilters.Add: %v", err)
}
tfss := []*TagFilters{tfs}
testLegacyRotateIndexDB(t, mrs, func(s *Storage) {
var search Search
search.Init(nil, s, tfss, tr, 1e5, noDeadline)
for search.NextMetricBlock() {
var b Block
search.MetricBlockRef.BlockRef.MustReadBlock(&b)
}
if err := search.Error(); err != nil {
panic(fmt.Sprintf("search error: %v", err))
}
search.MustClose()
})
}
// testLegacyRotateIndexDB checks that storage handles gracefully indexDB rotation
// that happens concurrently with some operation (ingestion or search). The
// operation is expected to finish successfully and there must be no panics.
func testLegacyRotateIndexDB(t *testing.T, mrs []MetricRow, op func(s *Storage)) {
defer testRemoveAll(t)
s := MustOpenStorage(t.Name(), OpenOptions{})
s.AddRows(mrs, defaultPrecisionBits)
s.DebugFlush()
// Convert to legacy 2 times in order to have both prev and curr legacy idbs.
s = mustConvertToLegacy(s)
s.AddRows(mrs, defaultPrecisionBits)
s.DebugFlush()
s = mustConvertToLegacy(s)
defer s.MustClose()
var wg sync.WaitGroup
stop := make(chan struct{})
for range 100 {
wg.Go(func() {
for {
select {
case <-stop:
return
default:
}
op(s)
}
})
}
for range 10 {
s.legacyMustRotateIndexDB(time.Now())
}
close(stop)
wg.Wait()
}
func TestMustOpenLegacyIndexDBTables_noTables(t *testing.T) {
defer testRemoveAll(t)
storageDataPath := t.Name()
s := MustOpenStorage(storageDataPath, OpenOptions{})
defer s.MustClose()
legacyIDBs := s.legacyIndexDBs.Load()
assertIndexDBIsNil(t, legacyIDBs.getIDBPrev())
assertIndexDBIsNil(t, legacyIDBs.getIDBCurr())
}
func TestMustOpenLegacyIndexDBTables_prevOnly(t *testing.T) {
defer testRemoveAll(t)
storageDataPath := t.Name()
idbPath := filepath.Join(storageDataPath, indexdbDirname)
prevName := "123456789ABCDEF0"
prevPath := filepath.Join(idbPath, prevName)
createEmptyIndexdb(prevPath)
assertPathsExist(t, prevPath)
s := MustOpenStorage(storageDataPath, OpenOptions{})
defer s.MustClose()
legacyIDBs := s.legacyIndexDBs.Load()
assertIndexDBName(t, legacyIDBs.getIDBPrev(), prevName)
assertIndexDBIsNil(t, legacyIDBs.getIDBCurr())
}
func TestMustOpenLegacyIndexDBTables_currAndPrev(t *testing.T) {
defer testRemoveAll(t)
storageDataPath := t.Name()
idbPath := filepath.Join(storageDataPath, indexdbDirname)
prevName := "123456789ABCDEF0"
prevPath := filepath.Join(idbPath, prevName)
createEmptyIndexdb(prevPath)
currName := "123456789ABCDEF1"
currPath := filepath.Join(idbPath, currName)
createEmptyIndexdb(currPath)
assertPathsExist(t, prevPath, currPath)
s := MustOpenStorage(storageDataPath, OpenOptions{})
defer s.MustClose()
legacyIDBs := s.legacyIndexDBs.Load()
assertIndexDBName(t, legacyIDBs.getIDBPrev(), prevName)
assertIndexDBName(t, legacyIDBs.getIDBCurr(), currName)
}
func TestMustOpenLegacyIndexDBTables_nextIsRemoved(t *testing.T) {
defer testRemoveAll(t)
storageDataPath := t.Name()
idbPath := filepath.Join(storageDataPath, indexdbDirname)
prevName := "123456789ABCDEF0"
prevPath := filepath.Join(idbPath, prevName)
createEmptyIndexdb(prevPath)
currName := "123456789ABCDEF1"
currPath := filepath.Join(idbPath, currName)
createEmptyIndexdb(currPath)
nextName := "123456789ABCDEF2"
nextPath := filepath.Join(idbPath, nextName)
createEmptyIndexdb(nextPath)
assertPathsExist(t, prevPath, currPath, nextPath)
s := MustOpenStorage(storageDataPath, OpenOptions{})
defer s.MustClose()
legacyIDBs := s.legacyIndexDBs.Load()
assertIndexDBName(t, legacyIDBs.getIDBPrev(), prevName)
assertIndexDBName(t, legacyIDBs.getIDBCurr(), currName)
assertPathsDoNotExist(t, nextPath)
}
func TestMustOpenLegacyIndexDBTables_nextAndObsoleteDirsAreRemoved(t *testing.T) {
defer testRemoveAll(t)
storageDataPath := t.Name()
idbPath := filepath.Join(storageDataPath, indexdbDirname)
obsolete1Name := "123456789ABCDEEE"
obsolete1Path := filepath.Join(idbPath, obsolete1Name)
createEmptyIndexdb(obsolete1Path)
obsolete2Name := "123456789ABCDEEF"
obsolete2Path := filepath.Join(idbPath, obsolete2Name)
createEmptyIndexdb(obsolete2Path)
prevName := "123456789ABCDEF0"
prevPath := filepath.Join(idbPath, prevName)
createEmptyIndexdb(prevPath)
currName := "123456789ABCDEF1"
currPath := filepath.Join(idbPath, currName)
createEmptyIndexdb(currPath)
nextName := "123456789ABCDEF2"
nextPath := filepath.Join(idbPath, nextName)
createEmptyIndexdb(nextPath)
assertPathsExist(t, obsolete1Path, obsolete2Path, prevPath, currPath, nextPath)
s := MustOpenStorage(storageDataPath, OpenOptions{})
defer s.MustClose()
legacyIDBs := s.legacyIndexDBs.Load()
assertIndexDBName(t, legacyIDBs.getIDBPrev(), prevName)
assertIndexDBName(t, legacyIDBs.getIDBCurr(), currName)
assertPathsDoNotExist(t, obsolete1Path, obsolete2Path, nextPath)
}
func TestLegacyMustRotateIndexDBs_dirNames(t *testing.T) {
defer testRemoveAll(t)
storageDataPath := t.Name()
idbPath := filepath.Join(storageDataPath, indexdbDirname)
prevName := "123456789ABCDEF0"
prevPath := filepath.Join(idbPath, prevName)
createEmptyIndexdb(prevPath)
currName := "123456789ABCDEF1"
currPath := filepath.Join(idbPath, currName)
createEmptyIndexdb(currPath)
assertPathsExist(t, prevPath, currPath)
s := MustOpenStorage(storageDataPath, OpenOptions{})
defer s.MustClose()
legacyIDBs := s.legacyIndexDBs.Load()
assertIndexDBName(t, legacyIDBs.getIDBPrev(), prevName)
assertIndexDBName(t, legacyIDBs.getIDBCurr(), currName)
assertPathsExist(t, prevPath, currPath)
s.legacyMustRotateIndexDB(time.Now())
legacyIDBs = s.legacyIndexDBs.Load()
assertIndexDBName(t, legacyIDBs.getIDBPrev(), currName)
assertIndexDBIsNil(t, legacyIDBs.getIDBCurr())
assertPathsDoNotExist(t, prevPath)
assertPathsExist(t, currPath)
s.legacyMustRotateIndexDB(time.Now())
legacyIDBs = s.legacyIndexDBs.Load()
assertIndexDBIsNil(t, legacyIDBs.getIDBPrev())
assertIndexDBIsNil(t, legacyIDBs.getIDBCurr())
assertPathsDoNotExist(t, prevPath, currPath)
}
func createEmptyIndexdb(path string) {
fs.MustMkdirIfNotExist(path)
partsFilePath := filepath.Join(path, "parts.json")
fs.MustWriteAtomic(partsFilePath, []byte("[]"), false)
}
func assertPathsExist(t *testing.T, paths ...string) {
t.Helper()
for _, path := range paths {
if !fs.IsPathExist(path) {
t.Fatalf("path does not exist: %s", path)
}
}
}
func assertPathsDoNotExist(t *testing.T, paths ...string) {
t.Helper()
for _, path := range paths {
if fs.IsPathExist(path) {
t.Fatalf("path exists: %s", path)
}
}
}
func assertIndexDBName(t *testing.T, idb *indexDB, want string) {
t.Helper()
if idb == nil {
t.Fatalf("unexpected idb: got nil, want non-nil")
}
if got := idb.name; got != want {
t.Errorf("unexpected idb name: got %s, want %s", got, want)
}
}
func assertIndexDBIsNil(t *testing.T, idb *indexDB) {
t.Helper()
if idb != nil {
t.Fatalf("unexpected idb: got %s, want nil", idb.name)
}
}
func TestLegacyStorageGetTSDBStatus(t *testing.T) {
defer testRemoveAll(t)
rng := rand.New(rand.NewSource(1))
s := MustOpenStorage(t.Name(), OpenOptions{})
// fill previous legacy indexDB
const numMetricsPrev = 1234
trPrev := TimeRange{
MinTimestamp: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 1, 1, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
datePrev := uint64(trPrev.MinTimestamp / msecPerDay)
mrsPrev := testGenerateMetricRowsWithPrefix(rng, numMetricsPrev, "legacy_prev", trPrev)
s.AddRows(mrsPrev, defaultPrecisionBits)
s.DebugFlush()
s = mustConvertToLegacy(s)
// fill current legacy indexDB
const numMetricsCurr = 2345
trCurr := TimeRange{
MinTimestamp: time.Date(2024, 2, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 2, 1, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
dateCurr := uint64(trCurr.MinTimestamp / msecPerDay)
mrsCurr := testGenerateMetricRowsWithPrefix(rng, numMetricsCurr, "legacy_curr", trCurr)
s.AddRows(mrsCurr, defaultPrecisionBits)
s.DebugFlush()
s = mustConvertToLegacy(s)
defer s.MustClose()
// fill partitioned index with data
const numMetricsPt = 3456
trPt := TimeRange{
MinTimestamp: time.Date(2024, 3, 1, 0, 0, 0, 0, time.UTC).UnixMilli(),
MaxTimestamp: time.Date(2024, 3, 1, 23, 59, 59, 999_999_999, time.UTC).UnixMilli(),
}
datePt := uint64(trPt.MinTimestamp / msecPerDay)
mrsPt := testGenerateMetricRowsWithPrefix(rng, numMetricsPt, "pt", trPt)
s.AddRows(mrsPt, defaultPrecisionBits)
s.DebugFlush()
assertTSDBStatus := func(date uint64, want *TSDBStatus) {
t.Helper()
got, err := s.GetTSDBStatus(nil, nil, date, "", 10, 1e9, noDeadline)
if err != nil {
t.Fatalf("GetTSDBStatus failed unexpectedly: %v", err)
}
if got.TotalSeries != want.TotalSeries {
t.Fatalf("unexpected TotalSeries: got %d, want %d", got.TotalSeries, want.TotalSeries)
}
if got.TotalLabelValuePairs != want.TotalLabelValuePairs {
t.Fatalf("unexpected TotalLabelValuePairs: got %d, want %d", got.TotalLabelValuePairs, want.TotalLabelValuePairs)
}
}
// check legacy prev indexDB date
assertTSDBStatus(datePrev, &TSDBStatus{
TotalSeries: numMetricsPrev,
TotalLabelValuePairs: numMetricsPrev * 3,
})
// check legacy curr indexDB date
assertTSDBStatus(dateCurr, &TSDBStatus{
TotalSeries: numMetricsCurr,
TotalLabelValuePairs: numMetricsCurr * 3,
})
// check partition indexDB date
assertTSDBStatus(datePt, &TSDBStatus{
TotalSeries: numMetricsPt,
TotalLabelValuePairs: numMetricsPt * 3,
})
}