influxdb/tsdb/tsm1/file_store_array_test.go

370 lines
6.8 KiB
Go

package tsm1_test
import (
"context"
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/influxdata/influxdb/tsdb"
"github.com/influxdata/influxdb/tsdb/tsm1"
)
func TestFileStore_Array(t *testing.T) {
makeFile := func(d ...interface{}) keyValues {
t.Helper()
if len(d)&1 == 1 {
panic("input should be even")
}
vals := make([]tsm1.Value, len(d)/2)
for i := 0; i < len(d); i += 2 {
vals[i/2] = tsm1.NewFloatValue(int64(d[i].(int)), d[i+1].(float64))
}
return keyValues{key: "cpu", values: vals}
}
// sel selects files and values from the keyValues slice
// and used to build the expected output.
type sel struct {
// f is the index of the tsm file
f int
// i is the index of the value to select from the file
i int
}
// del represents a file delete in order to generate tombstones
type del struct {
// f is the index of the tsm file to perform a delete.
// Specifying -1 will perform a delete over the entire FileStore.
f int
min, max int64
}
type read []sel
cases := []struct {
name string
data []keyValues
time int64
asc bool
deletes []del
reads []read
}{
{
name: "SeekToAsc_FromStart",
data: []keyValues{
makeFile(0, 1.0),
makeFile(1, 2.0),
makeFile(2, 3.0),
},
time: 0,
asc: true,
reads: []read{
[]sel{{0, 0}},
},
},
{
name: "SeekToAsc_BeforeStart",
data: []keyValues{
makeFile(1, 1.0),
makeFile(2, 2.0),
makeFile(3, 3.0),
},
time: 0,
asc: true,
reads: []read{
[]sel{{0, 0}},
},
},
{
// Tests that seeking and reading all blocks that contain overlapping points does
// not skip any blocks.
name: "SeekToAsc_BeforeStart_OverlapFloat",
data: []keyValues{
makeFile(0, 0.0, 1, 1.0),
makeFile(2, 2.0),
makeFile(3, 3.0),
makeFile(0, 4.0, 2, 7.0),
},
time: 0,
asc: true,
reads: []read{
[]sel{{3, 0}, {0, 1}, {3, 1}},
[]sel{{2, 0}},
},
},
{
// Tests that blocks with a lower min time in later files are not returned
// more than once causing unsorted results.
name: "SeekToAsc_OverlapMinFloat",
data: []keyValues{
makeFile(1, 1.0, 3, 3.0),
makeFile(2, 2.0, 4, 4.0),
makeFile(0, 0.0, 1, 1.1),
makeFile(2, 2.2),
},
time: 0,
asc: true,
reads: []read{
[]sel{{2, 0}, {2, 1}, {3, 0}, {0, 1}},
[]sel{{1, 1}},
[]sel{},
},
},
{
name: "SeekToAsc_Middle",
data: []keyValues{
makeFile(1, 1.0, 2, 2.0, 3, 3.0),
makeFile(4, 4.0),
},
time: 3,
asc: true,
reads: []read{
[]sel{{0, 2}},
[]sel{{1, 0}},
},
},
{
name: "SeekToAsc_End",
data: []keyValues{
makeFile(0, 1.0),
makeFile(1, 2.0),
makeFile(2, 3.0),
},
time: 2,
asc: true,
reads: []read{
[]sel{{2, 0}},
},
},
// descending cursor tests
{
name: "SeekToDesc_FromStart",
data: []keyValues{
makeFile(0, 1.0),
makeFile(1, 2.0),
makeFile(2, 3.0),
},
time: 0,
asc: false,
reads: []read{
[]sel{{0, 0}},
},
},
{
name: "SeekToDesc_Duplicate",
data: []keyValues{
makeFile(0, 4.0),
makeFile(0, 1.0),
makeFile(2, 2.0),
makeFile(2, 3.0),
},
time: 2,
asc: false,
reads: []read{
[]sel{{3, 0}},
[]sel{{1, 0}},
},
},
{
name: "SeekToDesc_OverlapMaxFloat",
data: []keyValues{
makeFile(1, 1.0, 3, 3.0),
makeFile(2, 2.0, 4, 4.0),
makeFile(0, 0.0, 1, 1.1),
makeFile(2, 2.2),
},
time: 5,
asc: false,
reads: []read{
[]sel{{3, 0}, {0, 1}, {1, 1}},
[]sel{{2, 0}, {2, 1}},
},
},
{
name: "SeekToDesc_AfterEnd",
data: []keyValues{
makeFile(1, 1.0),
makeFile(2, 2.0),
makeFile(3, 3.0),
},
time: 4,
asc: false,
reads: []read{
[]sel{{2, 0}},
},
},
{
name: "SeekToDesc_AfterEnd_OverlapFloat",
data: []keyValues{
makeFile(8, 0.0, 9, 1.0),
makeFile(2, 2.0),
makeFile(3, 3.0),
makeFile(3, 4.0, 7, 7.0),
},
time: 10,
asc: false,
reads: []read{
[]sel{{0, 0}, {0, 1}},
[]sel{{3, 0}, {3, 1}},
[]sel{{1, 0}},
[]sel{},
},
},
{
name: "SeekToDesc_Middle",
data: []keyValues{
makeFile(1, 1.0),
makeFile(2, 2.0, 3, 3.0, 4, 4.0),
},
time: 3,
asc: false,
reads: []read{
[]sel{{1, 0}, {1, 1}},
},
},
{
name: "SeekToDesc_End",
data: []keyValues{
makeFile(0, 1.0),
makeFile(1, 2.0),
makeFile(2, 3.0),
},
time: 2,
asc: false,
reads: []read{
[]sel{{2, 0}},
},
},
// tombstone tests
{
name: "TombstoneRange",
data: []keyValues{
makeFile(0, 1.0),
makeFile(1, 2.0),
makeFile(2, 3.0),
},
time: 0,
asc: true,
deletes: []del{
{-1, 1, 1},
},
reads: []read{
[]sel{{0, 0}},
[]sel{{2, 0}},
[]sel{},
},
},
{
name: "TombstoneRange_PartialFirst",
data: []keyValues{
makeFile(0, 0.0, 1, 1.0),
makeFile(2, 2.0),
},
time: 0,
asc: true,
deletes: []del{
{0, 1, 3},
},
reads: []read{
[]sel{{0, 0}},
[]sel{{1, 0}},
[]sel{},
},
},
{
name: "TombstoneRange_PartialFloat",
data: []keyValues{
makeFile(0, 0.0, 1, 1.0, 2, 2.0),
},
time: 0,
asc: true,
deletes: []del{
{-1, 1, 1},
},
reads: []read{
[]sel{{0, 0}, {0, 2}},
[]sel{},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
dir := MustTempDir()
defer os.RemoveAll(dir)
fs := tsm1.NewFileStore(dir)
files, err := newFiles(dir, tc.data...)
if err != nil {
t.Fatalf("unexpected error creating files: %v", err)
}
for _, del := range tc.deletes {
if del.f > -1 {
// Delete part of the block in the first file.
r := MustOpenTSMReader(files[del.f])
r.DeleteRange([][]byte{[]byte("cpu")}, del.min, del.max)
}
}
fs.Replace(nil, files)
for _, del := range tc.deletes {
if del.f == -1 {
if err := fs.DeleteRange([][]byte{[]byte("cpu")}, del.min, del.max); err != nil {
t.Fatalf("unexpected error delete range: %v", err)
}
}
}
buf := tsdb.NewFloatArrayLen(1000)
c := fs.KeyCursor(context.Background(), []byte("cpu"), tc.time, tc.asc)
for i, read := range tc.reads {
// Search for an entry that exists in the second file
values, err := c.ReadFloatArrayBlock(buf)
if err != nil {
t.Fatalf("read %d failed: unexpected error reading values: %v", i, err)
}
exp := &tsdb.FloatArray{}
for _, s := range read {
vals := tc.data[s.f].values
exp.Timestamps = append(exp.Timestamps, vals[s.i].UnixNano())
exp.Values = append(exp.Values, vals[s.i].Value().(float64))
}
if len(read) == 0 {
exp = tsdb.NewFloatArrayLen(0)
}
if !cmp.Equal(values, exp) {
t.Fatalf("read %d failed: unexpected values -got/+exp\n%s", i, cmp.Diff(values, exp))
}
c.Next()
}
})
}
}