mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-05-17 00:26:36 +03:00
899 lines
28 KiB
Go
899 lines
28 KiB
Go
package tests
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
at "github.com/VictoriaMetrics/VictoriaMetrics/apptest"
|
|
)
|
|
|
|
var (
|
|
legacyVmsinglePath = os.Getenv("VM_LEGACY_VMSINGLE_PATH")
|
|
legacyVmstoragePath = os.Getenv("VM_LEGACY_VMSTORAGE_PATH")
|
|
)
|
|
|
|
type testLegacyDeleteSeriesOpts struct {
|
|
startLegacySUT func() at.PrometheusWriteQuerier
|
|
startNewSUT func() at.PrometheusWriteQuerier
|
|
stopLegacySUT func()
|
|
stopNewSUT func()
|
|
}
|
|
|
|
func TestLegacySingleDeleteSeries(t *testing.T) {
|
|
tc := at.NewTestCase(t)
|
|
defer tc.Stop()
|
|
|
|
storageDataPath := filepath.Join(tc.Dir(), "vmsingle")
|
|
|
|
opts := testLegacyDeleteSeriesOpts{
|
|
startLegacySUT: func() at.PrometheusWriteQuerier {
|
|
return tc.MustStartVmsingleAt("vmsingle-legacy", legacyVmsinglePath, []string{
|
|
"-storageDataPath=" + storageDataPath,
|
|
"-retentionPeriod=100y",
|
|
"-search.maxStalenessInterval=1m",
|
|
})
|
|
},
|
|
startNewSUT: func() at.PrometheusWriteQuerier {
|
|
return tc.MustStartVmsingle("vmsingle-new", []string{
|
|
"-storageDataPath=" + storageDataPath,
|
|
"-retentionPeriod=100y",
|
|
"-search.maxStalenessInterval=1m",
|
|
})
|
|
},
|
|
stopLegacySUT: func() {
|
|
tc.StopApp("vmsingle-legacy")
|
|
},
|
|
stopNewSUT: func() {
|
|
tc.StopApp("vmsingle-new")
|
|
},
|
|
}
|
|
|
|
testLegacyDeleteSeries(tc, opts)
|
|
}
|
|
|
|
func TestLegacyClusterDeleteSeries(t *testing.T) {
|
|
tc := at.NewTestCase(t)
|
|
defer tc.Stop()
|
|
|
|
storage1DataPath := filepath.Join(tc.Dir(), "vmstorage1")
|
|
storage2DataPath := filepath.Join(tc.Dir(), "vmstorage2")
|
|
|
|
opts := testLegacyDeleteSeriesOpts{
|
|
startLegacySUT: func() at.PrometheusWriteQuerier {
|
|
return tc.MustStartCluster(&at.ClusterOptions{
|
|
Vmstorage1Instance: "vmstorage1-legacy",
|
|
Vmstorage1Binary: legacyVmstoragePath,
|
|
Vmstorage1Flags: []string{
|
|
"-storageDataPath=" + storage1DataPath,
|
|
"-retentionPeriod=100y",
|
|
},
|
|
Vmstorage2Instance: "vmstorage2-legacy",
|
|
Vmstorage2Binary: legacyVmstoragePath,
|
|
Vmstorage2Flags: []string{
|
|
"-storageDataPath=" + storage2DataPath,
|
|
"-retentionPeriod=100y",
|
|
},
|
|
VminsertInstance: "vminsert",
|
|
VminsertFlags: []string{},
|
|
VmselectInstance: "vmselect",
|
|
VmselectFlags: []string{
|
|
"-search.maxStalenessInterval=1m",
|
|
},
|
|
})
|
|
},
|
|
startNewSUT: func() at.PrometheusWriteQuerier {
|
|
return tc.MustStartCluster(&at.ClusterOptions{
|
|
Vmstorage1Instance: "vmstorage1-new",
|
|
Vmstorage1Flags: []string{
|
|
"-storageDataPath=" + storage1DataPath,
|
|
"-retentionPeriod=100y",
|
|
},
|
|
Vmstorage2Instance: "vmstorage2-new",
|
|
Vmstorage2Flags: []string{
|
|
"-storageDataPath=" + storage2DataPath,
|
|
"-retentionPeriod=100y",
|
|
},
|
|
VminsertInstance: "vminsert",
|
|
VminsertFlags: []string{},
|
|
VmselectInstance: "vmselect",
|
|
VmselectFlags: []string{
|
|
"-search.maxStalenessInterval=1m",
|
|
},
|
|
})
|
|
},
|
|
stopLegacySUT: func() {
|
|
tc.StopApp("vminsert")
|
|
tc.StopApp("vmselect")
|
|
tc.StopApp("vmstorage1-legacy")
|
|
tc.StopApp("vmstorage2-legacy")
|
|
},
|
|
stopNewSUT: func() {
|
|
tc.StopApp("vminsert")
|
|
tc.StopApp("vmselect")
|
|
tc.StopApp("vmstorage1-new")
|
|
tc.StopApp("vmstorage2-new")
|
|
},
|
|
}
|
|
|
|
testLegacyDeleteSeries(tc, opts)
|
|
}
|
|
|
|
func testLegacyDeleteSeries(tc *at.TestCase, opts testLegacyDeleteSeriesOpts) {
|
|
t := tc.T()
|
|
|
|
type want struct {
|
|
series []map[string]string
|
|
queryResults []*at.QueryResult
|
|
}
|
|
|
|
genData := func(prefix string, start, end, step int64, value float64) (recs []string, w *want) {
|
|
count := (end - start) / step
|
|
recs = make([]string, count)
|
|
w = &want{
|
|
series: make([]map[string]string, count),
|
|
queryResults: make([]*at.QueryResult, count),
|
|
}
|
|
for i := range count {
|
|
name := fmt.Sprintf("%s_%03d", prefix, i)
|
|
timestamp := start + int64(i)*step
|
|
|
|
recs[i] = fmt.Sprintf("%s %f %d", name, value, timestamp)
|
|
w.series[i] = map[string]string{"__name__": name}
|
|
w.queryResults[i] = &at.QueryResult{
|
|
Metric: map[string]string{"__name__": name},
|
|
Samples: []*at.Sample{{Timestamp: timestamp, Value: value}},
|
|
}
|
|
}
|
|
return recs, w
|
|
}
|
|
|
|
assertSearchResults := func(app at.PrometheusQuerier, query string, start, end int64, step string, want *want) {
|
|
t.Helper()
|
|
|
|
tc.Assert(&at.AssertOptions{
|
|
Msg: "unexpected /api/v1/series response",
|
|
Got: func() any {
|
|
return app.PrometheusAPIV1Series(t, query, at.QueryOpts{
|
|
Start: fmt.Sprintf("%d", start),
|
|
End: fmt.Sprintf("%d", end),
|
|
}).Sort()
|
|
},
|
|
Want: &at.PrometheusAPIV1SeriesResponse{
|
|
Status: "success",
|
|
Data: want.series,
|
|
},
|
|
FailNow: true,
|
|
})
|
|
|
|
tc.Assert(&at.AssertOptions{
|
|
Msg: "unexpected /api/v1/query_range response",
|
|
Got: func() any {
|
|
return app.PrometheusAPIV1QueryRange(t, query, at.QueryOpts{
|
|
Start: fmt.Sprintf("%d", start),
|
|
End: fmt.Sprintf("%d", end),
|
|
Step: step,
|
|
})
|
|
},
|
|
Want: &at.PrometheusAPIV1QueryResponse{
|
|
Status: "success",
|
|
Data: &at.QueryData{
|
|
ResultType: "matrix",
|
|
Result: want.queryResults,
|
|
},
|
|
},
|
|
FailNow: true,
|
|
})
|
|
}
|
|
|
|
// - start legacy vmsingle
|
|
// - insert data1
|
|
// - confirm that metric names and samples are searcheable
|
|
// - stop legacy vmsingle
|
|
const step = 24 * 3600 * 1000 // 24h
|
|
start1 := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC).UnixMilli()
|
|
end1 := time.Date(2000, 1, 10, 0, 0, 0, 0, time.UTC).UnixMilli()
|
|
data1, want1 := genData("metric", start1, end1, step, 1)
|
|
legacySUT := opts.startLegacySUT()
|
|
legacySUT.PrometheusAPIV1ImportPrometheus(t, data1, at.QueryOpts{})
|
|
legacySUT.ForceFlush(t)
|
|
assertSearchResults(legacySUT, `{__name__=~".*"}`, start1, end1, "1d", want1)
|
|
opts.stopLegacySUT()
|
|
|
|
// - start new vmsingle
|
|
// - confirm that data1 metric names and samples are searcheable
|
|
// - delete data1
|
|
// - confirm that data1 metric names and samples are not searcheable anymore
|
|
// - insert data2 (same metric names, different dates)
|
|
// - confirm that metric names become searcheable again
|
|
// - confirm that data1 samples are not searchable and data2 samples are searcheable
|
|
|
|
newSUT := opts.startNewSUT()
|
|
assertSearchResults(newSUT, `{__name__=~".*"}`, start1, end1, "1d", want1)
|
|
|
|
newSUT.PrometheusAPIV1AdminTSDBDeleteSeries(t, `{__name__=~".*"}`, at.QueryOpts{})
|
|
wantNoResults := &want{
|
|
series: []map[string]string{},
|
|
queryResults: []*at.QueryResult{},
|
|
}
|
|
assertSearchResults(newSUT, `{__name__=~".*"}`, start1, end1, "1d", wantNoResults)
|
|
|
|
start2 := time.Date(2000, 1, 11, 0, 0, 0, 0, time.UTC).UnixMilli()
|
|
end2 := time.Date(2000, 1, 20, 0, 0, 0, 0, time.UTC).UnixMilli()
|
|
data2, want2 := genData("metric", start2, end2, step, 2)
|
|
newSUT.PrometheusAPIV1ImportPrometheus(t, data2, at.QueryOpts{})
|
|
newSUT.ForceFlush(t)
|
|
assertSearchResults(newSUT, `{__name__=~".*"}`, start1, end2, "1d", want2)
|
|
|
|
// - restart new vmsingle
|
|
// - confirm that metric names still searchable, data1 samples are not
|
|
// searchable, and data2 samples are searcheable
|
|
|
|
opts.stopNewSUT()
|
|
newSUT = opts.startNewSUT()
|
|
assertSearchResults(newSUT, `{__name__=~".*"}`, start1, end2, "1d", want2)
|
|
opts.stopNewSUT()
|
|
}
|
|
|
|
type testLegacyBackupRestoreOpts struct {
|
|
startLegacySUT func() at.PrometheusWriteQuerier
|
|
startNewSUT func() at.PrometheusWriteQuerier
|
|
stopLegacySUT func()
|
|
stopNewSUT func()
|
|
storageDataPaths []string
|
|
snapshotCreateURLs func(at.PrometheusWriteQuerier) []string
|
|
}
|
|
|
|
func TestLegacySingleBackupRestore(t *testing.T) {
|
|
tc := at.NewTestCase(t)
|
|
defer tc.Stop()
|
|
|
|
storageDataPath := filepath.Join(tc.Dir(), "vmsingle")
|
|
|
|
opts := testLegacyBackupRestoreOpts{
|
|
startLegacySUT: func() at.PrometheusWriteQuerier {
|
|
return tc.MustStartVmsingleAt("vmsingle-legacy", legacyVmsinglePath, []string{
|
|
"-storageDataPath=" + storageDataPath,
|
|
"-retentionPeriod=100y",
|
|
"-search.disableCache=true",
|
|
"-search.maxStalenessInterval=1m",
|
|
})
|
|
},
|
|
startNewSUT: func() at.PrometheusWriteQuerier {
|
|
return tc.MustStartVmsingle("vmsingle-new", []string{
|
|
"-storageDataPath=" + storageDataPath,
|
|
"-retentionPeriod=100y",
|
|
"-search.disableCache=true",
|
|
"-search.maxStalenessInterval=1m",
|
|
})
|
|
},
|
|
stopLegacySUT: func() {
|
|
tc.StopApp("vmsingle-legacy")
|
|
},
|
|
stopNewSUT: func() {
|
|
tc.StopApp("vmsingle-new")
|
|
},
|
|
storageDataPaths: []string{
|
|
storageDataPath,
|
|
},
|
|
snapshotCreateURLs: func(sut at.PrometheusWriteQuerier) []string {
|
|
return []string{
|
|
sut.(*at.Vmsingle).SnapshotCreateURL(),
|
|
}
|
|
},
|
|
}
|
|
|
|
testLegacyBackupRestore(tc, opts)
|
|
}
|
|
|
|
func TestLegacyClusterBackupRestore(t *testing.T) {
|
|
tc := at.NewTestCase(t)
|
|
defer tc.Stop()
|
|
|
|
storage1DataPath := filepath.Join(tc.Dir(), "vmstorage1")
|
|
storage2DataPath := filepath.Join(tc.Dir(), "vmstorage2")
|
|
|
|
opts := testLegacyBackupRestoreOpts{
|
|
startLegacySUT: func() at.PrometheusWriteQuerier {
|
|
return tc.MustStartCluster(&at.ClusterOptions{
|
|
Vmstorage1Instance: "vmstorage1-legacy",
|
|
Vmstorage1Binary: legacyVmstoragePath,
|
|
Vmstorage1Flags: []string{
|
|
"-storageDataPath=" + storage1DataPath,
|
|
"-retentionPeriod=100y",
|
|
},
|
|
Vmstorage2Instance: "vmstorage2-legacy",
|
|
Vmstorage2Binary: legacyVmstoragePath,
|
|
Vmstorage2Flags: []string{
|
|
"-storageDataPath=" + storage2DataPath,
|
|
"-retentionPeriod=100y",
|
|
},
|
|
VminsertInstance: "vminsert",
|
|
VminsertFlags: []string{},
|
|
VmselectInstance: "vmselect",
|
|
VmselectFlags: []string{
|
|
"-search.disableCache=true",
|
|
"-search.maxStalenessInterval=1m",
|
|
},
|
|
})
|
|
},
|
|
startNewSUT: func() at.PrometheusWriteQuerier {
|
|
return tc.MustStartCluster(&at.ClusterOptions{
|
|
Vmstorage1Instance: "vmstorage1-new",
|
|
Vmstorage1Flags: []string{
|
|
"-storageDataPath=" + storage1DataPath,
|
|
"-retentionPeriod=100y",
|
|
},
|
|
Vmstorage2Instance: "vmstorage2-new",
|
|
Vmstorage2Flags: []string{
|
|
"-storageDataPath=" + storage2DataPath,
|
|
"-retentionPeriod=100y",
|
|
},
|
|
VminsertInstance: "vminsert",
|
|
VmselectInstance: "vmselect",
|
|
VmselectFlags: []string{
|
|
"-search.disableCache=true",
|
|
"-search.maxStalenessInterval=1m",
|
|
},
|
|
})
|
|
},
|
|
stopLegacySUT: func() {
|
|
tc.StopApp("vminsert")
|
|
tc.StopApp("vmselect")
|
|
tc.StopApp("vmstorage1-legacy")
|
|
tc.StopApp("vmstorage2-legacy")
|
|
},
|
|
stopNewSUT: func() {
|
|
tc.StopApp("vminsert")
|
|
tc.StopApp("vmselect")
|
|
tc.StopApp("vmstorage1-new")
|
|
tc.StopApp("vmstorage2-new")
|
|
},
|
|
storageDataPaths: []string{
|
|
storage1DataPath,
|
|
storage2DataPath,
|
|
},
|
|
snapshotCreateURLs: func(sut at.PrometheusWriteQuerier) []string {
|
|
c := sut.(*at.Vmcluster)
|
|
return []string{
|
|
c.Vmstorages[0].SnapshotCreateURL(),
|
|
c.Vmstorages[1].SnapshotCreateURL(),
|
|
}
|
|
},
|
|
}
|
|
|
|
testLegacyBackupRestore(tc, opts)
|
|
}
|
|
|
|
func testLegacyBackupRestore(tc *at.TestCase, opts testLegacyBackupRestoreOpts) {
|
|
t := tc.T()
|
|
|
|
const msecPerMinute = 60 * 1000
|
|
// Use the same number of metrics and time range for all the data ingestions
|
|
// below.
|
|
const numMetrics = 1000
|
|
start := time.Date(2025, 3, 1, 10, 0, 0, 0, time.UTC).Add(-numMetrics * time.Minute).UnixMilli()
|
|
end := time.Date(2025, 3, 1, 10, 0, 0, 0, time.UTC).UnixMilli()
|
|
genData := func(prefix string) (recs []string, wantSeries []map[string]string, wantQueryResults []*at.QueryResult) {
|
|
recs = make([]string, numMetrics)
|
|
wantSeries = make([]map[string]string, numMetrics)
|
|
wantQueryResults = make([]*at.QueryResult, numMetrics)
|
|
for i := range numMetrics {
|
|
name := fmt.Sprintf("%s_%03d", prefix, i)
|
|
value := float64(i)
|
|
timestamp := start + int64(i)*msecPerMinute
|
|
|
|
recs[i] = fmt.Sprintf("%s %f %d", name, value, timestamp)
|
|
wantSeries[i] = map[string]string{"__name__": name}
|
|
wantQueryResults[i] = &at.QueryResult{
|
|
Metric: map[string]string{"__name__": name},
|
|
Samples: []*at.Sample{{Timestamp: timestamp, Value: value}},
|
|
}
|
|
}
|
|
return recs, wantSeries, wantQueryResults
|
|
}
|
|
|
|
backupBaseDir, err := filepath.Abs(filepath.Join(tc.Dir(), "backups"))
|
|
if err != nil {
|
|
t.Fatalf("could not get absolute path for the backup base dir")
|
|
}
|
|
|
|
// assertSeries issues various queries to the app and compares the query
|
|
// results with the expected ones.
|
|
assertQueries := func(app at.PrometheusQuerier, query string, wantSeries []map[string]string, wantQueryResults []*at.QueryResult) {
|
|
t.Helper()
|
|
tc.Assert(&at.AssertOptions{
|
|
Msg: "unexpected /api/v1/series response",
|
|
Got: func() any {
|
|
return app.PrometheusAPIV1Series(t, query, at.QueryOpts{
|
|
Start: fmt.Sprintf("%d", start),
|
|
End: fmt.Sprintf("%d", end),
|
|
}).Sort()
|
|
},
|
|
Want: &at.PrometheusAPIV1SeriesResponse{
|
|
Status: "success",
|
|
Data: wantSeries,
|
|
},
|
|
FailNow: true,
|
|
})
|
|
|
|
tc.Assert(&at.AssertOptions{
|
|
Msg: "unexpected /api/v1/query_range response",
|
|
Got: func() any {
|
|
return app.PrometheusAPIV1QueryRange(t, query, at.QueryOpts{
|
|
Start: fmt.Sprintf("%d", start),
|
|
End: fmt.Sprintf("%d", end),
|
|
Step: "60s",
|
|
})
|
|
},
|
|
Want: &at.PrometheusAPIV1QueryResponse{
|
|
Status: "success",
|
|
Data: &at.QueryData{
|
|
ResultType: "matrix",
|
|
Result: wantQueryResults,
|
|
},
|
|
},
|
|
Retries: 300,
|
|
FailNow: true,
|
|
})
|
|
}
|
|
|
|
createBackup := func(sut at.PrometheusWriteQuerier, name string) {
|
|
t.Helper()
|
|
for i, storageDataPath := range opts.storageDataPaths {
|
|
replica := fmt.Sprintf("replica-%d", i)
|
|
instance := fmt.Sprintf("vmbackup-%s-%s", name, replica)
|
|
snapshotCreateURL := opts.snapshotCreateURLs(sut)[i]
|
|
backupPath := "fs://" + filepath.Join(backupBaseDir, name, replica)
|
|
tc.MustStartVmbackup(instance, storageDataPath, snapshotCreateURL, backupPath)
|
|
}
|
|
}
|
|
|
|
restoreFromBackup := func(name string) {
|
|
t.Helper()
|
|
for i, storageDataPath := range opts.storageDataPaths {
|
|
replica := fmt.Sprintf("replica-%d", i)
|
|
instance := fmt.Sprintf("vmrestore-%s-%s", name, replica)
|
|
backupPath := "fs://" + filepath.Join(backupBaseDir, name, replica)
|
|
tc.MustStartVmrestore(instance, backupPath, storageDataPath)
|
|
}
|
|
}
|
|
|
|
legacy1Data, wantLegacy1Series, wantLegacy1QueryResults := genData("legacy1")
|
|
legacy2Data, wantLegacy2Series, wantLegacy2QueryResults := genData("legacy2")
|
|
new1Data, wantNew1Series, wantNew1QueryResults := genData("new1")
|
|
new2Data, wantNew2Series, wantNew2QueryResults := genData("new2")
|
|
wantLegacy12Series := slices.Concat(wantLegacy1Series, wantLegacy2Series)
|
|
wantLegacy12QueryResults := slices.Concat(wantLegacy1QueryResults, wantLegacy2QueryResults)
|
|
wantLegacy1New1Series := slices.Concat(wantLegacy1Series, wantNew1Series)
|
|
wantLegacy1New1QueryResults := slices.Concat(wantLegacy1QueryResults, wantNew1QueryResults)
|
|
wantLegacy1New12Series := slices.Concat(wantLegacy1New1Series, wantNew2Series)
|
|
wantLegacy1New12QueryResults := slices.Concat(wantLegacy1New1QueryResults, wantNew2QueryResults)
|
|
var legacySUT, newSUT at.PrometheusWriteQuerier
|
|
|
|
// Verify backup/restore with legacy SUT.
|
|
|
|
// Start legacy SUT with empty storage data dir.
|
|
legacySUT = opts.startLegacySUT()
|
|
|
|
// Ingest legacy1 records, ensure the queries return legacy1, and create
|
|
// legacy1 backup.
|
|
legacySUT.PrometheusAPIV1ImportPrometheus(t, legacy1Data, at.QueryOpts{})
|
|
legacySUT.ForceFlush(t)
|
|
assertQueries(legacySUT, `{__name__=~".*"}`, wantLegacy1Series, wantLegacy1QueryResults)
|
|
createBackup(legacySUT, "legacy1")
|
|
|
|
// Ingest legacy2 records, ensure the queries return legacy1+legacy2, and
|
|
// create legacy1+legacy2 backup.
|
|
legacySUT.PrometheusAPIV1ImportPrometheus(t, legacy2Data, at.QueryOpts{})
|
|
legacySUT.ForceFlush(t)
|
|
assertQueries(legacySUT, `{__name__=~"legacy.*"}`, wantLegacy12Series, wantLegacy12QueryResults)
|
|
createBackup(legacySUT, "legacy12")
|
|
|
|
// Stop legacy SUT and restore legacy1 data.
|
|
// Start legacy SUT and ensure the queries return legacy1.
|
|
opts.stopLegacySUT()
|
|
restoreFromBackup("legacy1")
|
|
legacySUT = opts.startLegacySUT()
|
|
assertQueries(legacySUT, `{__name__=~".*"}`, wantLegacy1Series, wantLegacy1QueryResults)
|
|
|
|
opts.stopLegacySUT()
|
|
|
|
// Verify backup/restore with new SUT.
|
|
|
|
// Start new SUT (with partition indexDBs) with storage containing legacy1
|
|
// data and Ensure that queries return legacy1 data.
|
|
newSUT = opts.startNewSUT()
|
|
assertQueries(newSUT, `{__name__=~".*"}`, wantLegacy1Series, wantLegacy1QueryResults)
|
|
|
|
// Ingest new1 records, ensure that queries now return legacy1+new1, and
|
|
// create the legacy1+new1 backup.
|
|
newSUT.PrometheusAPIV1ImportPrometheus(t, new1Data, at.QueryOpts{})
|
|
newSUT.ForceFlush(t)
|
|
assertQueries(newSUT, `{__name__=~"(legacy|new).*"}`, wantLegacy1New1Series, wantLegacy1New1QueryResults)
|
|
createBackup(newSUT, "legacy1-new1")
|
|
|
|
// Ingest new2 records, ensure that queries now return legacy1+new1+new2,
|
|
// and create the legacy1+new1+new2 backup.
|
|
newSUT.PrometheusAPIV1ImportPrometheus(t, new2Data, at.QueryOpts{})
|
|
newSUT.ForceFlush(t)
|
|
assertQueries(newSUT, `{__name__=~"(legacy|new1|new2).*"}`, wantLegacy1New12Series, wantLegacy1New12QueryResults)
|
|
createBackup(newSUT, "legacy1-new12")
|
|
|
|
// Stop new SUT and restore legacy1+new1 data.
|
|
// Start new SUT and ensure queries return legacy1+new1 data.
|
|
opts.stopNewSUT()
|
|
restoreFromBackup("legacy1-new1")
|
|
newSUT = opts.startNewSUT()
|
|
assertQueries(newSUT, `{__name__=~".*"}`, wantLegacy1New1Series, wantLegacy1New1QueryResults)
|
|
|
|
opts.stopNewSUT()
|
|
|
|
// Verify backup/restore with legacy SUT again.
|
|
|
|
// Start legacy SUT with storage containing legacy1+new1 data.
|
|
//
|
|
// Ensure that the /series and /query_range queries return legacy1 data only.
|
|
// new1 data is not returned because legacy vmsingle does not know about
|
|
// partition indexDBs.
|
|
legacySUT = opts.startLegacySUT()
|
|
assertQueries(legacySUT, `{__name__=~".*"}`, wantLegacy1Series, wantLegacy1QueryResults)
|
|
|
|
// Stop legacy SUT and restore legacy1+legacy2 data.
|
|
// Start legacy SUT and ensure that queries now return legacy1+legacy2 data.
|
|
opts.stopLegacySUT()
|
|
restoreFromBackup("legacy12")
|
|
legacySUT = opts.startLegacySUT()
|
|
assertQueries(legacySUT, `{__name__=~".*"}`, wantLegacy12Series, wantLegacy12QueryResults)
|
|
|
|
opts.stopLegacySUT()
|
|
|
|
// Verify backup/restore with new vmsingle again.
|
|
|
|
// Start new vmsingle with storage containing legacy1+legacy2 data and
|
|
// ensure that queries return legacy1+legacy2 data.
|
|
newSUT = opts.startNewSUT()
|
|
assertQueries(newSUT, `{__name__=~".*"}`, wantLegacy12Series, wantLegacy12QueryResults)
|
|
|
|
// Stop new SUT and restore legacy1+new1+new2 data.
|
|
// Start new SUT and ensure that queries return legacy1+new1+new2 data.
|
|
opts.stopNewSUT()
|
|
restoreFromBackup("legacy1-new12")
|
|
newSUT = opts.startNewSUT()
|
|
assertQueries(newSUT, `{__name__=~"(legacy|new).*"}`, wantLegacy1New12Series, wantLegacy1New12QueryResults)
|
|
|
|
opts.stopNewSUT()
|
|
}
|
|
|
|
type testLegacyDowngradeOpts struct {
|
|
startLegacySUT func() at.PrometheusWriteQuerier
|
|
startNewSUT func() at.PrometheusWriteQuerier
|
|
stopLegacySUT func()
|
|
stopNewSUT func()
|
|
}
|
|
|
|
func TestLegacySingleDowngrade(t *testing.T) {
|
|
tc := at.NewTestCase(t)
|
|
defer tc.Stop()
|
|
|
|
storageDataPath := filepath.Join(tc.Dir(), "vmsingle")
|
|
|
|
opts := testLegacyDowngradeOpts{
|
|
startLegacySUT: func() at.PrometheusWriteQuerier {
|
|
return tc.MustStartVmsingleAt("vmsingle-legacy", legacyVmsinglePath, []string{
|
|
"-storageDataPath=" + storageDataPath,
|
|
"-retentionPeriod=100y",
|
|
"-search.disableCache=true",
|
|
"-search.maxStalenessInterval=1m",
|
|
})
|
|
},
|
|
startNewSUT: func() at.PrometheusWriteQuerier {
|
|
return tc.MustStartVmsingle("vmsingle-new", []string{
|
|
"-storageDataPath=" + storageDataPath,
|
|
"-retentionPeriod=100y",
|
|
"-search.disableCache=true",
|
|
"-search.maxStalenessInterval=1m",
|
|
})
|
|
},
|
|
stopLegacySUT: func() {
|
|
tc.StopApp("vmsingle-legacy")
|
|
},
|
|
stopNewSUT: func() {
|
|
tc.StopApp("vmsingle-new")
|
|
},
|
|
}
|
|
|
|
testLegacyDowngrade(tc, opts)
|
|
}
|
|
|
|
func TestLegacyClusterDowngrade(t *testing.T) {
|
|
tc := at.NewTestCase(t)
|
|
defer tc.Stop()
|
|
|
|
storage1DataPath := filepath.Join(tc.Dir(), "vmstorage1")
|
|
storage2DataPath := filepath.Join(tc.Dir(), "vmstorage2")
|
|
|
|
opts := testLegacyDowngradeOpts{
|
|
startLegacySUT: func() at.PrometheusWriteQuerier {
|
|
return tc.MustStartCluster(&at.ClusterOptions{
|
|
Vmstorage1Instance: "vmstorage1-legacy",
|
|
Vmstorage1Binary: legacyVmstoragePath,
|
|
Vmstorage1Flags: []string{
|
|
"-storageDataPath=" + storage1DataPath,
|
|
"-retentionPeriod=100y",
|
|
},
|
|
Vmstorage2Instance: "vmstorage2-legacy",
|
|
Vmstorage2Binary: legacyVmstoragePath,
|
|
Vmstorage2Flags: []string{
|
|
"-storageDataPath=" + storage2DataPath,
|
|
"-retentionPeriod=100y",
|
|
},
|
|
VminsertInstance: "vminsert",
|
|
VminsertFlags: []string{},
|
|
VmselectInstance: "vmselect",
|
|
VmselectFlags: []string{
|
|
"-search.disableCache=true",
|
|
"-search.maxStalenessInterval=1m",
|
|
},
|
|
})
|
|
},
|
|
startNewSUT: func() at.PrometheusWriteQuerier {
|
|
return tc.MustStartCluster(&at.ClusterOptions{
|
|
Vmstorage1Instance: "vmstorage1-new",
|
|
Vmstorage1Flags: []string{
|
|
"-storageDataPath=" + storage1DataPath,
|
|
"-retentionPeriod=100y",
|
|
},
|
|
Vmstorage2Instance: "vmstorage2-new",
|
|
Vmstorage2Flags: []string{
|
|
"-storageDataPath=" + storage2DataPath,
|
|
"-retentionPeriod=100y",
|
|
},
|
|
VminsertInstance: "vminsert",
|
|
VminsertFlags: []string{},
|
|
VmselectInstance: "vmselect",
|
|
VmselectFlags: []string{
|
|
"-search.disableCache=true",
|
|
"-search.maxStalenessInterval=1m",
|
|
},
|
|
})
|
|
},
|
|
stopLegacySUT: func() {
|
|
tc.StopApp("vminsert")
|
|
tc.StopApp("vmselect")
|
|
tc.StopApp("vmstorage1-legacy")
|
|
tc.StopApp("vmstorage2-legacy")
|
|
},
|
|
stopNewSUT: func() {
|
|
tc.StopApp("vminsert")
|
|
tc.StopApp("vmselect")
|
|
tc.StopApp("vmstorage1-new")
|
|
tc.StopApp("vmstorage2-new")
|
|
},
|
|
}
|
|
|
|
testLegacyDowngrade(tc, opts)
|
|
}
|
|
|
|
func testLegacyDowngrade(tc *at.TestCase, opts testLegacyDowngradeOpts) {
|
|
t := tc.T()
|
|
|
|
type want struct {
|
|
series []map[string]string
|
|
labels []string
|
|
labelValues []string
|
|
queryResults []*at.QueryResult
|
|
queryRangeResults []*at.QueryResult
|
|
}
|
|
|
|
uniq := func(s []string) []string {
|
|
slices.Sort(s)
|
|
return slices.Compact(s)
|
|
}
|
|
|
|
mergeWant := func(want1, want2 want) want {
|
|
var result want
|
|
result.series = slices.Concat(want1.series, want2.series)
|
|
result.labels = uniq(slices.Concat(want1.labels, want2.labels))
|
|
result.labelValues = slices.Concat(want1.labelValues, want2.labelValues)
|
|
result.queryResults = slices.Concat(want1.queryResults, want2.queryResults)
|
|
result.queryRangeResults = slices.Concat(want1.queryRangeResults, want2.queryRangeResults)
|
|
|
|
return result
|
|
}
|
|
|
|
// Use the same number of metrics and time range for all the data batches below.
|
|
const numMetrics = 1000
|
|
const labelName = "prefix"
|
|
start := time.Date(2025, 3, 1, 10, 0, 0, 0, time.UTC).UnixMilli()
|
|
end := start
|
|
genData := func(prefix string) (recs []string, want want) {
|
|
labelValue := prefix
|
|
recs = make([]string, numMetrics)
|
|
want.series = make([]map[string]string, numMetrics)
|
|
want.labels = []string{"__name__", labelName}
|
|
want.labelValues = []string{labelValue}
|
|
want.queryResults = make([]*at.QueryResult, numMetrics)
|
|
want.queryRangeResults = make([]*at.QueryResult, numMetrics)
|
|
for i := range numMetrics {
|
|
name := fmt.Sprintf("%s_%03d", prefix, i)
|
|
value := float64(i)
|
|
timestamp := start
|
|
|
|
recs[i] = fmt.Sprintf("%s{%s=\"%s\"} %f %d", name, labelName, labelValue, value, timestamp)
|
|
want.series[i] = map[string]string{"__name__": name, labelName: labelValue}
|
|
want.queryResults[i] = &at.QueryResult{
|
|
Metric: map[string]string{"__name__": name, labelName: labelValue},
|
|
Sample: &at.Sample{Timestamp: timestamp, Value: value},
|
|
}
|
|
want.queryRangeResults[i] = &at.QueryResult{
|
|
Metric: map[string]string{"__name__": name, labelName: labelValue},
|
|
Samples: []*at.Sample{{Timestamp: timestamp, Value: value}},
|
|
}
|
|
}
|
|
return recs, want
|
|
}
|
|
|
|
// assertSeries issues various queries to the app and compares the query
|
|
// results with the expected ones.
|
|
assertQueries := func(app at.PrometheusQuerier, query string, want want, wantSeriesCount uint64) {
|
|
t.Helper()
|
|
tc.Assert(&at.AssertOptions{
|
|
Msg: "unexpected /api/v1/series response",
|
|
Got: func() any {
|
|
return app.PrometheusAPIV1Series(t, query, at.QueryOpts{
|
|
Start: fmt.Sprintf("%d", start),
|
|
End: fmt.Sprintf("%d", end),
|
|
}).Sort()
|
|
},
|
|
Want: &at.PrometheusAPIV1SeriesResponse{
|
|
Status: "success",
|
|
Data: want.series,
|
|
},
|
|
FailNow: true,
|
|
})
|
|
|
|
tc.Assert(&at.AssertOptions{
|
|
Msg: "unexpected /api/v1/series/count response",
|
|
Got: func() any {
|
|
return app.PrometheusAPIV1SeriesCount(t, at.QueryOpts{
|
|
Start: fmt.Sprintf("%d", start),
|
|
End: fmt.Sprintf("%d", end),
|
|
})
|
|
},
|
|
Want: &at.PrometheusAPIV1SeriesCountResponse{
|
|
Status: "success",
|
|
Data: []uint64{wantSeriesCount},
|
|
},
|
|
FailNow: true,
|
|
})
|
|
|
|
tc.Assert(&at.AssertOptions{
|
|
Msg: "unexpected /api/v1/labels response",
|
|
Got: func() any {
|
|
return app.PrometheusAPIV1Labels(t, query, at.QueryOpts{
|
|
Start: fmt.Sprintf("%d", start),
|
|
End: fmt.Sprintf("%d", end),
|
|
})
|
|
},
|
|
Want: &at.PrometheusAPIV1LabelsResponse{
|
|
Status: "success",
|
|
Data: want.labels,
|
|
},
|
|
FailNow: true,
|
|
})
|
|
|
|
tc.Assert(&at.AssertOptions{
|
|
Msg: "unexpected /api/v1/label/../values response",
|
|
Got: func() any {
|
|
return app.PrometheusAPIV1LabelValues(t, labelName, query, at.QueryOpts{
|
|
Start: fmt.Sprintf("%d", start),
|
|
End: fmt.Sprintf("%d", end),
|
|
})
|
|
},
|
|
Want: &at.PrometheusAPIV1LabelValuesResponse{
|
|
Status: "success",
|
|
Data: want.labelValues,
|
|
},
|
|
FailNow: true,
|
|
})
|
|
|
|
tc.Assert(&at.AssertOptions{
|
|
Msg: "unexpected /api/v1/query response",
|
|
Got: func() any {
|
|
return app.PrometheusAPIV1Query(t, query, at.QueryOpts{
|
|
Time: fmt.Sprintf("%d", start),
|
|
Step: "10m",
|
|
})
|
|
},
|
|
Want: &at.PrometheusAPIV1QueryResponse{
|
|
Status: "success",
|
|
Data: &at.QueryData{
|
|
ResultType: "vector",
|
|
Result: want.queryResults,
|
|
},
|
|
},
|
|
Retries: 300,
|
|
FailNow: true,
|
|
})
|
|
|
|
tc.Assert(&at.AssertOptions{
|
|
Msg: "unexpected /api/v1/query_range response",
|
|
Got: func() any {
|
|
return app.PrometheusAPIV1QueryRange(t, query, at.QueryOpts{
|
|
Start: fmt.Sprintf("%d", start),
|
|
End: fmt.Sprintf("%d", end),
|
|
Step: "60s",
|
|
})
|
|
},
|
|
Want: &at.PrometheusAPIV1QueryResponse{
|
|
Status: "success",
|
|
Data: &at.QueryData{
|
|
ResultType: "matrix",
|
|
Result: want.queryRangeResults,
|
|
},
|
|
},
|
|
Retries: 300,
|
|
FailNow: true,
|
|
})
|
|
}
|
|
|
|
wantEmpty := want{
|
|
series: []map[string]string{},
|
|
labels: []string{"__name__"},
|
|
labelValues: []string{},
|
|
queryResults: []*at.QueryResult{},
|
|
queryRangeResults: []*at.QueryResult{},
|
|
}
|
|
|
|
legacy1Data, wantLegacy1 := genData("legacy1")
|
|
legacy2Data, wantLegacy2 := genData("legacy2")
|
|
new1Data, wantNew1 := genData("new1")
|
|
wantLegacy1New1 := mergeWant(wantLegacy1, wantNew1)
|
|
wantLegacy2New1 := mergeWant(wantLegacy2, wantNew1)
|
|
var legacySUT, newSUT at.PrometheusWriteQuerier
|
|
|
|
// Start legacy SUT with empty storage data dir.
|
|
// Ingest legacy1 records, ensure the queries return legacy1
|
|
legacySUT = opts.startLegacySUT()
|
|
legacySUT.PrometheusAPIV1ImportPrometheus(t, legacy1Data, at.QueryOpts{})
|
|
legacySUT.ForceFlush(t)
|
|
assertQueries(legacySUT, `{__name__=~".*"}`, wantLegacy1, numMetrics)
|
|
opts.stopLegacySUT()
|
|
|
|
// Start new SUT (with partition indexDBs) with storage containing legacy1
|
|
// data and ensure that queries return new1 and legacy1 data.
|
|
newSUT = opts.startNewSUT()
|
|
newSUT.PrometheusAPIV1ImportPrometheus(t, new1Data, at.QueryOpts{})
|
|
newSUT.ForceFlush(t)
|
|
assertQueries(newSUT, `{__name__=~".*"}`, wantLegacy1New1, 2*numMetrics)
|
|
opts.stopNewSUT()
|
|
|
|
// Downgrade to legacy SUT, ensure the queries return only legacy1.
|
|
// Delete all series, ensure that queries return no series.
|
|
// Ingest legacy2 records, ensure the queries return only legacy2.
|
|
legacySUT = opts.startLegacySUT()
|
|
assertQueries(legacySUT, `{__name__=~".*"}`, wantLegacy1, numMetrics)
|
|
legacySUT.PrometheusAPIV1AdminTSDBDeleteSeries(t, `{__name__=~".*"}`, at.QueryOpts{})
|
|
assertQueries(legacySUT, `{__name__=~".*"}`, wantEmpty, numMetrics)
|
|
legacySUT.PrometheusAPIV1ImportPrometheus(t, legacy2Data, at.QueryOpts{})
|
|
legacySUT.ForceFlush(t)
|
|
// series count includes deleted metrics
|
|
assertQueries(legacySUT, `{__name__=~".*"}`, wantLegacy2, 2*numMetrics)
|
|
opts.stopLegacySUT()
|
|
|
|
// Upgrade to new SUT, ensure the queries return recently ingested legacy2 and new1
|
|
// since legacy SUT cannot delete them.
|
|
// Delete all series, ensure that queries return no series.
|
|
newSUT = opts.startNewSUT()
|
|
// series count includes deleted metrics
|
|
assertQueries(newSUT, `{__name__=~".*"}`, wantLegacy2New1, 3*numMetrics)
|
|
newSUT.PrometheusAPIV1AdminTSDBDeleteSeries(t, `{__name__=~".*"}`, at.QueryOpts{})
|
|
// series count includes deleted metrics
|
|
assertQueries(newSUT, `{__name__=~".*"}`, wantEmpty, 3*numMetrics)
|
|
opts.stopNewSUT()
|
|
}
|