package tsdb_test import ( "bytes" "fmt" "os" "path" "testing" "github.com/influxdata/influxdb/v2/models" "github.com/influxdata/influxdb/v2/tsdb" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" ) func TestParseSeriesKeyInto(t *testing.T) { name := []byte("cpu") tags := models.NewTags(map[string]string{"region": "east", "server": "a"}) key := tsdb.AppendSeriesKey(nil, name, tags) dst := make(models.Tags, 0) gotName, gotTags := tsdb.ParseSeriesKeyInto(key, dst) if !bytes.Equal(gotName, name) { t.Fatalf("got %q, expected %q", gotName, name) } if got, exp := len(gotTags), 2; got != exp { t.Fatalf("got tags length %d, expected %d", got, exp) } else if got, exp := gotTags, tags; !got.Equal(exp) { t.Fatalf("got tags %v, expected %v", got, exp) } dst = make(models.Tags, 0, 5) _, gotTags = tsdb.ParseSeriesKeyInto(key, dst) if got, exp := len(gotTags), 2; got != exp { t.Fatalf("got tags length %d, expected %d", got, exp) } else if got, exp := cap(gotTags), 5; got != exp { t.Fatalf("got tags capacity %d, expected %d", got, exp) } else if got, exp := gotTags, tags; !got.Equal(exp) { t.Fatalf("got tags %v, expected %v", got, exp) } dst = make(models.Tags, 1) _, gotTags = tsdb.ParseSeriesKeyInto(key, dst) if got, exp := len(gotTags), 2; got != exp { t.Fatalf("got tags length %d, expected %d", got, exp) } else if got, exp := gotTags, tags; !got.Equal(exp) { t.Fatalf("got tags %v, expected %v", got, exp) } } // Ensure that broken series files are closed func TestSeriesFile_Open_WhenFileCorrupt_ShouldReturnErr(t *testing.T) { f := NewBrokenSeriesFile([]byte{0, 0, 0, 0, 0}) defer f.Close() f.Logger = zaptest.NewLogger(t) err := f.Open() if err == nil { t.Fatalf("should report error") } } // Ensure series file contains the correct set of series. func TestSeriesFile_Series(t *testing.T) { sfile := MustOpenSeriesFile(t) defer sfile.Close() series := []Series{ {Name: []byte("cpu"), Tags: models.NewTags(map[string]string{"region": "east"})}, {Name: []byte("cpu"), Tags: models.NewTags(map[string]string{"region": "west"})}, {Name: []byte("mem"), Tags: models.NewTags(map[string]string{"region": "east"})}, } for _, s := range series { if _, err := sfile.CreateSeriesListIfNotExists([][]byte{[]byte(s.Name)}, []models.Tags{s.Tags}); err != nil { t.Fatal(err) } } // Verify total number of series is correct. if n := sfile.SeriesCount(); n != 3 { t.Fatalf("unexpected series count: %d", n) } // Verify all series exist. for i, s := range series { if seriesID := sfile.SeriesID(s.Name, s.Tags, nil); seriesID == 0 { t.Fatalf("series does not exist: i=%d", i) } } // Verify non-existent series doesn't exist. if sfile.HasSeries([]byte("foo"), models.NewTags(map[string]string{"region": "north"}), nil) { t.Fatal("series should not exist") } } // Ensure series file can be compacted. func TestSeriesFileCompactor(t *testing.T) { sfile := MustOpenSeriesFile(t) defer sfile.Close() // Disable automatic compactions. for _, p := range sfile.Partitions() { p.CompactThreshold = 0 } var names [][]byte var tagsSlice []models.Tags for i := 0; i < 10000; i++ { names = append(names, []byte(fmt.Sprintf("m%d", i))) tagsSlice = append(tagsSlice, models.NewTags(map[string]string{"foo": "bar"})) } if _, err := sfile.CreateSeriesListIfNotExists(names, tagsSlice); err != nil { t.Fatal(err) } // Verify total number of series is correct. if n := sfile.SeriesCount(); n != uint64(len(names)) { t.Fatalf("unexpected series count: %d", n) } // Compact in-place for each partition. for _, p := range sfile.Partitions() { compactor := tsdb.NewSeriesPartitionCompactor() if err := compactor.Compact(p); err != nil { t.Fatal(err) } } // Verify all series exist. for i := range names { if seriesID := sfile.SeriesID(names[i], tagsSlice[i], nil); seriesID == 0 { t.Fatalf("series does not exist: %s,%s", names[i], tagsSlice[i].String()) } } } // Ensure series file deletions persist across compactions. func TestSeriesFile_DeleteSeriesID(t *testing.T) { sfile := MustOpenSeriesFile(t) defer sfile.Close() ids0, err := sfile.CreateSeriesListIfNotExists([][]byte{[]byte("m1")}, []models.Tags{nil}) if err != nil { t.Fatal(err) } else if _, err := sfile.CreateSeriesListIfNotExists([][]byte{[]byte("m2")}, []models.Tags{nil}); err != nil { t.Fatal(err) } else if err := sfile.ForceCompact(); err != nil { t.Fatal(err) } // Delete and ensure deletion. if err := sfile.DeleteSeriesID(ids0[0]); err != nil { t.Fatal(err) } else if _, err := sfile.CreateSeriesListIfNotExists([][]byte{[]byte("m1")}, []models.Tags{nil}); err != nil { t.Fatal(err) } else if !sfile.IsDeleted(ids0[0]) { t.Fatal("expected deletion before compaction") } if err := sfile.ForceCompact(); err != nil { t.Fatal(err) } else if !sfile.IsDeleted(ids0[0]) { t.Fatal("expected deletion after compaction") } if err := sfile.Reopen(); err != nil { t.Fatal(err) } else if !sfile.IsDeleted(ids0[0]) { t.Fatal("expected deletion after reopen") } } func TestSeriesFile_Compaction(t *testing.T) { sfile := MustOpenSeriesFile(t) defer sfile.Close() var segmentPaths []string for _, p := range sfile.Partitions() { for _, ss := range p.Segments() { segmentPaths = append(segmentPaths, ss.Path()) } } sfileSize := func() (res int64) { for _, p := range segmentPaths { fi, err := os.Stat(p) require.NoError(t, err) res += fi.Size() } return } // Generate a bunch of keys. var mms [][]byte var tagSets []models.Tags for i := 0; i < 1000; i++ { mms = append(mms, []byte("cpu")) tagSets = append(tagSets, models.NewTags(map[string]string{"region": fmt.Sprintf("r%d", i)})) } // Add all to the series file. ids, err := sfile.CreateSeriesListIfNotExists(mms, tagSets) require.NoError(t, err) // Delete a subset of keys. for i, id := range ids { if i%10 == 0 { require.NoError(t, sfile.DeleteSeriesID(id)) } } // Check total series count. require.Equal(t, 1000, int(sfile.SeriesCount())) // Compact all segments. var paths []string for _, p := range sfile.Partitions() { for _, ss := range p.Segments() { require.NoError(t, ss.CompactToPath(ss.Path()+".tmp", p.Index())) paths = append(paths, ss.Path()) } } // Close index. require.NoError(t, sfile.SeriesFile.Close()) // Compute total size of all series data. origSize := sfileSize() // Overwrite files. for _, path := range paths { require.NoError(t, os.Rename(path+".tmp", path)) } // Check size of compacted series data. // We do this before reopening the index because on Windows, opening+mmap'ing the series // file will cause the file to grow back to its original size. newSize := sfileSize() // Verify new size is smaller. require.Greater(t, origSize, newSize) // Reopen index. sfile.SeriesFile = tsdb.NewSeriesFile(sfile.SeriesFile.Path()) require.NoError(t, sfile.SeriesFile.Open()) // Ensure series status is correct. for i, id := range ids { require.Equal(t, (i%10) == 0, sfile.IsDeleted(id)) } // Check total series count. require.Equal(t, 900, int(sfile.SeriesCount())) } var cachedCompactionSeriesFile *SeriesFile func BenchmarkSeriesFile_Compaction(b *testing.B) { const n = 1000000 if cachedCompactionSeriesFile == nil { sfile := MustOpenSeriesFile(b) // Generate a bunch of keys. var ids []uint64 for i := 0; i < n; i++ { tmp, err := sfile.CreateSeriesListIfNotExists([][]byte{[]byte("cpu")}, []models.Tags{models.NewTags(map[string]string{"region": fmt.Sprintf("r%d", i)})}) if err != nil { b.Fatal(err) } ids = append(ids, tmp...) } // Delete a subset of keys. for i := 0; i < len(ids); i += 10 { if err := sfile.DeleteSeriesID(ids[i]); err != nil { b.Fatal(err) } } cachedCompactionSeriesFile = sfile } b.ResetTimer() for i := 0; i < b.N; i++ { // Compact all segments in parallel. var g errgroup.Group for _, p := range cachedCompactionSeriesFile.Partitions() { for _, segment := range p.Segments() { p, segment := p, segment g.Go(func() error { return segment.CompactToPath(segment.Path()+".tmp", p.Index()) }) } } if err := g.Wait(); err != nil { b.Fatal(err) } } } // Series represents name/tagset pairs that are used in testing. type Series struct { Name []byte Tags models.Tags Deleted bool } // SeriesFile is a test wrapper for tsdb.SeriesFile. type SeriesFile struct { *tsdb.SeriesFile } // NewSeriesFile returns a new instance of SeriesFile with a temporary file path. func NewSeriesFile() *SeriesFile { dir, err := os.MkdirTemp("", "tsdb-series-file-") if err != nil { panic(err) } return &SeriesFile{SeriesFile: tsdb.NewSeriesFile(dir)} } func NewBrokenSeriesFile(content []byte) *SeriesFile { sFile := NewSeriesFile() fPath := sFile.Path() sFile.Open() sFile.SeriesFile.Close() segPath := path.Join(fPath, "00", "0000") if _, err := os.Stat(segPath); os.IsNotExist(err) { panic(err) } err := os.WriteFile(segPath, content, 0777) if err != nil { panic(err) } return sFile } // MustOpenSeriesFile returns a new, open instance of SeriesFile. Panic on error. func MustOpenSeriesFile(tb testing.TB) *SeriesFile { tb.Helper() f := NewSeriesFile() f.Logger = zaptest.NewLogger(tb) if err := f.Open(); err != nil { panic(err) } return f } // Close closes the log file and removes it from disk. func (f *SeriesFile) Close() error { defer os.RemoveAll(f.Path()) return f.SeriesFile.Close() } // Reopen close & reopens the series file. func (f *SeriesFile) Reopen() error { if err := f.SeriesFile.Close(); err != nil { return err } f.SeriesFile = tsdb.NewSeriesFile(f.SeriesFile.Path()) return f.SeriesFile.Open() } // ForceCompact executes an immediate compaction across all partitions. func (f *SeriesFile) ForceCompact() error { for _, p := range f.Partitions() { if err := tsdb.NewSeriesPartitionCompactor().Compact(p); err != nil { return err } } return nil }