influxdb/tsdb/engine/bz1/bz1_test.go

487 lines
15 KiB
Go

package bz1_test
import (
"encoding/binary"
"errors"
"io/ioutil"
"math"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"github.com/influxdb/influxdb/influxql"
"github.com/influxdb/influxdb/models"
"github.com/influxdb/influxdb/tsdb"
"github.com/influxdb/influxdb/tsdb/engine/bz1"
)
// Ensure the engine can write series metadata and reload it.
func TestEngine_LoadMetadataIndex_Series(t *testing.T) {
e := OpenDefaultEngine()
defer e.Close()
// Setup mock that writes the index
seriesToCreate := []*tsdb.SeriesCreate{
{Series: tsdb.NewSeries(string(models.MakeKey([]byte("cpu"), map[string]string{"host": "server0"})), map[string]string{"host": "server0"})},
{Series: tsdb.NewSeries(string(models.MakeKey([]byte("cpu"), map[string]string{"host": "server1"})), map[string]string{"host": "server1"})},
{Series: tsdb.NewSeries("series with spaces", nil)},
}
e.PointsWriter.WritePointsFn = func(a []models.Point) error { return e.WriteIndex(nil, nil, seriesToCreate) }
// Write series metadata.
if err := e.WritePoints(nil, nil, seriesToCreate); err != nil {
t.Fatal(err)
}
// Load metadata index.
index := tsdb.NewDatabaseIndex()
if err := e.LoadMetadataIndex(nil, index, make(map[string]*tsdb.MeasurementFields)); err != nil {
t.Fatal(err)
}
// Verify index is correct.
if m := index.Measurement("cpu"); m == nil {
t.Fatal("measurement not found")
} else if s := m.SeriesByID(1); s.Key != "cpu,host=server0" || !reflect.DeepEqual(s.Tags, map[string]string{"host": "server0"}) {
t.Fatalf("unexpected series: %q / %#v", s.Key, s.Tags)
} else if s = m.SeriesByID(2); s.Key != "cpu,host=server1" || !reflect.DeepEqual(s.Tags, map[string]string{"host": "server1"}) {
t.Fatalf("unexpected series: %q / %#v", s.Key, s.Tags)
}
if m := index.Measurement("series with spaces"); m == nil {
t.Fatal("measurement not found")
} else if s := m.SeriesByID(3); s.Key != "series with spaces" {
t.Fatalf("unexpected series: %q", s.Key)
}
}
// Ensure the engine can write field metadata and reload it.
func TestEngine_LoadMetadataIndex_Fields(t *testing.T) {
e := OpenDefaultEngine()
defer e.Close()
// Setup mock that writes the index
fields := map[string]*tsdb.MeasurementFields{
"cpu": &tsdb.MeasurementFields{
Fields: map[string]*tsdb.Field{
"value": &tsdb.Field{ID: 0, Name: "value"},
},
},
}
e.PointsWriter.WritePointsFn = func(a []models.Point) error { return e.WriteIndex(nil, fields, nil) }
// Write series metadata.
if err := e.WritePoints(nil, fields, nil); err != nil {
t.Fatal(err)
}
// Load metadata index.
mfs := make(map[string]*tsdb.MeasurementFields)
if err := e.LoadMetadataIndex(nil, tsdb.NewDatabaseIndex(), mfs); err != nil {
t.Fatal(err)
}
// Verify measurement field is correct.
if mf := mfs["cpu"]; mf == nil {
t.Fatal("measurement fields not found")
} else if !reflect.DeepEqual(mf.Fields, map[string]*tsdb.Field{"value": &tsdb.Field{ID: 0, Name: "value"}}) {
t.Fatalf("unexpected fields: %#v", mf.Fields)
}
}
// Ensure the engine can write points to storage.
func TestEngine_WritePoints_PointsWriter(t *testing.T) {
e := OpenDefaultEngine()
defer e.Close()
// Points to be inserted.
points := []models.Point{
models.MustNewPoint("cpu", models.Tags{}, models.Fields{"foo": "bar"}, time.Unix(0, 1)),
models.MustNewPoint("cpu", models.Tags{}, models.Fields{"foo": "bar"}, time.Unix(0, 0)),
models.MustNewPoint("cpu", models.Tags{}, models.Fields{"foo": "bar"}, time.Unix(1, 0)),
models.MustNewPoint("cpu", models.Tags{"host": "serverA"}, models.Fields{"foo": "bar"}, time.Unix(0, 0)),
}
// Mock points writer to ensure points are passed through.
var invoked bool
e.PointsWriter.WritePointsFn = func(a []models.Point) error {
invoked = true
if !reflect.DeepEqual(points, a) {
t.Fatalf("unexpected points: %#v", a)
}
return nil
}
// Write points against two separate series.
if err := e.WritePoints(points, nil, nil); err != nil {
t.Fatal(err)
} else if !invoked {
t.Fatal("PointsWriter.WritePoints() not called")
}
}
// Ensure the engine can return errors from the points writer.
func TestEngine_WritePoints_ErrPointsWriter(t *testing.T) {
e := OpenDefaultEngine()
defer e.Close()
// Ensure points writer returns an error.
e.PointsWriter.WritePointsFn = func(a []models.Point) error { return errors.New("marker") }
// Write to engine.
if err := e.WritePoints(nil, nil, nil); err == nil || err.Error() != `write points: marker` {
t.Fatal(err)
}
}
// Ensure the engine can write points to the index.
func TestEngine_WriteIndex_Append(t *testing.T) {
e := OpenDefaultEngine()
defer e.Close()
// Create codec.
codec := tsdb.NewFieldCodec(map[string]*tsdb.Field{
"value": {ID: uint8(1), Name: "value", Type: influxql.Float},
})
// Append points to index.
if err := e.WriteIndex(map[string][][]byte{
"cpu": [][]byte{
append(u64tob(1), MustEncodeFields(codec, models.Fields{"value": float64(10)})...),
append(u64tob(2), MustEncodeFields(codec, models.Fields{"value": float64(20)})...),
},
"mem": [][]byte{
append(u64tob(0), MustEncodeFields(codec, models.Fields{"value": float64(30)})...),
},
}, nil, nil); err != nil {
t.Fatal(err)
}
// Start transaction.
tx := e.MustBegin(false)
defer tx.Rollback()
// Iterate over "cpu" series.
c := tx.Cursor("cpu", []string{"value"}, codec, true)
if k, v := c.SeekTo(0); k != 1 || v.(float64) != float64(10) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
} else if k, v = c.Next(); k != 2 || v.(float64) != float64(20) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
} else if k, _ = c.Next(); k != tsdb.EOF {
t.Fatalf("unexpected key/value: %x / %x", k, v)
}
// Iterate over "mem" series.
c = tx.Cursor("mem", []string{"value"}, codec, true)
if k, v := c.SeekTo(0); k != 0 || v.(float64) != float64(30) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
} else if k, _ = c.Next(); k != tsdb.EOF {
t.Fatalf("unexpected key/value: %x / %x", k, v)
}
}
// Ensure the engine can rewrite blocks that contain the new point range.
func TestEngine_WriteIndex_Insert(t *testing.T) {
e := OpenDefaultEngine()
defer e.Close()
// Create codec.
codec := tsdb.NewFieldCodec(map[string]*tsdb.Field{
"value": {ID: uint8(1), Name: "value", Type: influxql.Float},
})
// Write initial points to index.
if err := e.WriteIndex(map[string][][]byte{
"cpu": [][]byte{
append(u64tob(10), MustEncodeFields(codec, models.Fields{"value": float64(10)})...),
append(u64tob(20), MustEncodeFields(codec, models.Fields{"value": float64(20)})...),
append(u64tob(30), MustEncodeFields(codec, models.Fields{"value": float64(30)})...),
},
}, nil, nil); err != nil {
t.Fatal(err)
}
// Write overlapping points to index.
if err := e.WriteIndex(map[string][][]byte{
"cpu": [][]byte{
append(u64tob(9), MustEncodeFields(codec, models.Fields{"value": float64(9)})...),
append(u64tob(10), MustEncodeFields(codec, models.Fields{"value": float64(255)})...),
append(u64tob(25), MustEncodeFields(codec, models.Fields{"value": float64(25)})...),
append(u64tob(31), MustEncodeFields(codec, models.Fields{"value": float64(31)})...),
},
}, nil, nil); err != nil {
t.Fatal(err)
}
// Write overlapping points to index again.
if err := e.WriteIndex(map[string][][]byte{
"cpu": [][]byte{
append(u64tob(31), MustEncodeFields(codec, models.Fields{"value": float64(255)})...),
},
}, nil, nil); err != nil {
t.Fatal(err)
}
// Start transaction.
tx := e.MustBegin(false)
defer tx.Rollback()
// Iterate over "cpu" series.
c := tx.Cursor("cpu", []string{"value"}, codec, true)
if k, v := c.SeekTo(0); k != 9 || v.(float64) != float64(9) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
} else if k, v = c.Next(); k != 10 || v.(float64) != float64(255) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
} else if k, v = c.Next(); k != 20 || v.(float64) != float64(20) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
} else if k, v = c.Next(); k != 25 || v.(float64) != float64(25) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
} else if k, v = c.Next(); k != 30 || v.(float64) != float64(30) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
} else if k, v = c.Next(); k != 31 || v.(float64) != float64(255) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
}
}
// Ensure the engine can rewrite blocks that contain the new point range.
func TestEngine_Cursor_Reverse(t *testing.T) {
e := OpenDefaultEngine()
defer e.Close()
// Create codec.
codec := tsdb.NewFieldCodec(map[string]*tsdb.Field{
"value": {ID: uint8(1), Name: "value", Type: influxql.Float},
})
// Write initial points to index.
if err := e.WriteIndex(map[string][][]byte{
"cpu": [][]byte{
append(u64tob(10), MustEncodeFields(codec, models.Fields{"value": float64(10)})...),
append(u64tob(20), MustEncodeFields(codec, models.Fields{"value": float64(20)})...),
append(u64tob(30), MustEncodeFields(codec, models.Fields{"value": float64(30)})...),
},
}, nil, nil); err != nil {
t.Fatal(err)
}
// Write overlapping points to index.
if err := e.WriteIndex(map[string][][]byte{
"cpu": [][]byte{
append(u64tob(9), MustEncodeFields(codec, models.Fields{"value": float64(9)})...),
append(u64tob(10), MustEncodeFields(codec, models.Fields{"value": float64(255)})...),
append(u64tob(25), MustEncodeFields(codec, models.Fields{"value": float64(25)})...),
append(u64tob(31), MustEncodeFields(codec, models.Fields{"value": float64(31)})...),
},
}, nil, nil); err != nil {
t.Fatal(err)
}
// Write overlapping points to index again.
if err := e.WriteIndex(map[string][][]byte{
"cpu": [][]byte{
append(u64tob(31), MustEncodeFields(codec, models.Fields{"value": float64(255)})...),
},
}, nil, nil); err != nil {
t.Fatal(err)
}
// Start transaction.
tx := e.MustBegin(false)
defer tx.Rollback()
// Iterate over "cpu" series.
c := tx.Cursor("cpu", []string{"value"}, codec, false)
if k, v := c.SeekTo(math.MaxInt64); k != 31 || v.(float64) != float64(255) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
} else if k, v = c.Next(); k != 30 || v.(float64) != float64(30) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
} else if k, v = c.Next(); k != 25 || v.(float64) != float64(25) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
} else if k, v = c.Next(); k != 20 || v.(float64) != float64(20) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
} else if k, v = c.Next(); k != 10 || v.(float64) != float64(255) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
} else if k, v = c.SeekTo(0); k != 9 || v.(float64) != float64(9) {
t.Fatalf("unexpected key/value: %x / %x", k, v)
}
}
// Ensure that the engine properly seeks to a block when the seek value is in the middle.
func TestEngine_WriteIndex_SeekAgainstInBlockValue(t *testing.T) {
e := OpenDefaultEngine()
defer e.Close()
// Create codec.
codec := tsdb.NewFieldCodec(map[string]*tsdb.Field{
"value": {ID: uint8(1), Name: "value", Type: influxql.String},
})
// make sure we have data split across two blocks
dataSize := (bz1.DefaultBlockSize - 16) / 2
data := strings.Repeat("*", dataSize)
// Write initial points to index.
if err := e.WriteIndex(map[string][][]byte{
"cpu": [][]byte{
append(u64tob(10), MustEncodeFields(codec, models.Fields{"value": data})...),
append(u64tob(20), MustEncodeFields(codec, models.Fields{"value": data})...),
append(u64tob(30), MustEncodeFields(codec, models.Fields{"value": data})...),
append(u64tob(40), MustEncodeFields(codec, models.Fields{"value": data})...),
},
}, nil, nil); err != nil {
t.Fatal(err)
}
// Start transaction.
tx := e.MustBegin(false)
defer tx.Rollback()
// Ensure that we can seek to a block in the middle
c := tx.Cursor("cpu", []string{"value"}, codec, true)
if k, _ := c.SeekTo(15); k != 20 {
t.Fatalf("expected to seek to time 20, but got %d", k)
}
// Ensure that we can seek to the block on the end
if k, _ := c.SeekTo(35); k != 40 {
t.Fatalf("expected to seek to time 40, but got %d", k)
}
}
// Ensure the engine ignores writes without keys.
func TestEngine_WriteIndex_NoKeys(t *testing.T) {
e := OpenDefaultEngine()
defer e.Close()
if err := e.WriteIndex(nil, nil, nil); err != nil {
t.Fatal(err)
}
}
// Ensure the engine ignores writes without points in a key.
func TestEngine_WriteIndex_NoPoints(t *testing.T) {
e := OpenDefaultEngine()
defer e.Close()
if err := e.WriteIndex(map[string][][]byte{"cpu": nil}, nil, nil); err != nil {
t.Fatal(err)
}
}
// Engine represents a test wrapper for bz1.Engine.
type Engine struct {
*bz1.Engine
PointsWriter EnginePointsWriter
}
// NewEngine returns a new instance of Engine.
func NewEngine(opt tsdb.EngineOptions) *Engine {
// Generate temporary file.
f, _ := ioutil.TempFile("", "bz1-")
f.Close()
os.Remove(f.Name())
walPath := filepath.Join(f.Name(), "wal")
// Create test wrapper and attach mocks.
e := &Engine{
Engine: bz1.NewEngine(f.Name(), walPath, opt).(*bz1.Engine),
}
e.Engine.WAL = &e.PointsWriter
return e
}
// OpenEngine returns an opened instance of Engine. Panic on error.
func OpenEngine(opt tsdb.EngineOptions) *Engine {
e := NewEngine(opt)
if err := e.Open(); err != nil {
panic(err)
}
return e
}
// OpenDefaultEngine returns an open Engine with default options.
func OpenDefaultEngine() *Engine { return OpenEngine(tsdb.NewEngineOptions()) }
// Close closes the engine and removes all data.
func (e *Engine) Close() error {
e.Engine.Close()
os.RemoveAll(e.Path())
return nil
}
// MustBegin returns a new tranaction. Panic on error.
func (e *Engine) MustBegin(writable bool) tsdb.Tx {
tx, err := e.Begin(writable)
if err != nil {
panic(err)
}
return tx
}
// EnginePointsWriter represents a mock that implements Engine.PointsWriter.
type EnginePointsWriter struct {
WritePointsFn func(points []models.Point) error
}
func (w *EnginePointsWriter) WritePoints(points []models.Point, measurementFieldsToSave map[string]*tsdb.MeasurementFields, seriesToCreate []*tsdb.SeriesCreate) error {
return w.WritePointsFn(points)
}
func (w *EnginePointsWriter) LoadMetadataIndex(index *tsdb.DatabaseIndex, measurementFields map[string]*tsdb.MeasurementFields) error {
return nil
}
func (w *EnginePointsWriter) DeleteSeries(keys []string) error { return nil }
func (w *EnginePointsWriter) Open() error { return nil }
func (w *EnginePointsWriter) Close() error { return nil }
func (w *EnginePointsWriter) Cursor(series string, fields []string, dec *tsdb.FieldCodec, ascending bool) tsdb.Cursor {
return &Cursor{ascending: ascending}
}
func (w *EnginePointsWriter) Flush() error { return nil }
// Cursor represents a mock that implements tsdb.Curosr.
type Cursor struct {
ascending bool
}
func (c *Cursor) Ascending() bool { return c.ascending }
func (c *Cursor) SeekTo(key int64) (int64, interface{}) { return tsdb.EOF, nil }
func (c *Cursor) Next() (int64, interface{}) { return tsdb.EOF, nil }
// MustEncodeFields encodes fields with codec. Panic on error.
func MustEncodeFields(codec *tsdb.FieldCodec, fields models.Fields) []byte {
b, err := codec.EncodeFields(fields)
if err != nil {
panic(err)
}
return b
}
// copyBytes returns a copy of a byte slice.
func copyBytes(b []byte) []byte {
if b == nil {
return nil
}
other := make([]byte, len(b))
copy(other, b)
return other
}
// u64tob converts a uint64 into an 8-byte slice.
func u64tob(v uint64) []byte {
b := make([]byte, 8)
binary.BigEndian.PutUint64(b, v)
return b
}
// btou64 converts an 8-byte slice into an uint64.
func btou64(b []byte) uint64 { return binary.BigEndian.Uint64(b) }