diff --git a/tsdb/docs/format/index.md b/tsdb/docs/format/index.md index 6279d34f6..53b77d9ab 100644 --- a/tsdb/docs/format/index.md +++ b/tsdb/docs/format/index.md @@ -82,6 +82,10 @@ Each series section is aligned to 16 bytes. The ID for a series is the `offset/1 Every series entry first holds its number of labels, followed by tuples of symbol table references that contain the label name and value. The label pairs are lexicographically sorted. After the labels, the number of indexed chunks is encoded, followed by a sequence of metadata entries containing the chunks minimum (`mint`) and maximum (`maxt`) timestamp and a reference to its position in the chunk file. The `mint` is the time of the first sample and `maxt` is the time of the last sample in the chunk. Holding the time range data in the index allows dropping chunks irrelevant to queried time ranges without accessing them directly. +Chunk references within single series must be increasing, and chunk references for `series_(N+1)` must be higher than chunk references for `series_N`. +This property guarantees that chunks that belong to the same series are grouped together in the segment files. +Furthermore chunk `mint` must be less or equal than `maxt`, and subsequent chunks within single series must have increasing `mint` and `maxt` and not overlap. + `mint` of the first chunk is stored, it's `maxt` is stored as a delta and the `mint` and `maxt` are encoded as deltas to the previous time for subsequent chunks. Similarly, the reference of the first chunk is stored and the next ref is stored as a delta to the previous one. ``` diff --git a/tsdb/index/index.go b/tsdb/index/index.go index 2856fc78f..84c771684 100644 --- a/tsdb/index/index.go +++ b/tsdb/index/index.go @@ -146,8 +146,11 @@ type Writer struct { labelNames map[string]uint64 // Label names, and their usage. // Hold last series to validate that clients insert new series in order. - lastSeries labels.Labels - lastRef storage.SeriesRef + lastSeries labels.Labels + lastSeriesRef storage.SeriesRef + + // Hold last added chunk reference to make sure that chunks are ordered properly. + lastChunkRef chunks.ChunkRef crc32 hash.Hash @@ -433,9 +436,27 @@ func (w *Writer) AddSeries(ref storage.SeriesRef, lset labels.Labels, chunks ... return fmt.Errorf("out-of-order series added with label set %q", lset) } - if ref < w.lastRef && !w.lastSeries.IsEmpty() { + if ref < w.lastSeriesRef && !w.lastSeries.IsEmpty() { return fmt.Errorf("series with reference greater than %d already added", ref) } + + lastChunkRef := w.lastChunkRef + lastMaxT := int64(0) + for ix, c := range chunks { + if c.Ref < lastChunkRef { + return fmt.Errorf("unsorted chunk reference: %d, previous: %d", c.Ref, lastChunkRef) + } + lastChunkRef = c.Ref + + if ix > 0 && c.MinTime <= lastMaxT { + return fmt.Errorf("chunk minT %d is not higher than previous chunk maxT %d", c.MinTime, lastMaxT) + } + if c.MaxTime < c.MinTime { + return fmt.Errorf("chunk maxT %d is less than minT %d", c.MaxTime, c.MinTime) + } + lastMaxT = c.MaxTime + } + // We add padding to 16 bytes to increase the addressable space we get through 4 byte // series references. if err := w.addPadding(seriesByteAlign); err != nil { @@ -510,7 +531,8 @@ func (w *Writer) AddSeries(ref storage.SeriesRef, lset labels.Labels, chunks ... } w.lastSeries.CopyFrom(lset) - w.lastRef = ref + w.lastSeriesRef = ref + w.lastChunkRef = lastChunkRef return nil } diff --git a/tsdb/index/index_test.go b/tsdb/index/index_test.go index ef8887035..5ff48ef19 100644 --- a/tsdb/index/index_test.go +++ b/tsdb/index/index_test.go @@ -18,7 +18,6 @@ import ( "errors" "fmt" "hash/crc32" - "math/rand" "os" "path/filepath" "sort" @@ -407,15 +406,17 @@ func TestPersistence_index_e2e(t *testing.T) { var input indexWriterSeriesSlice + ref := uint64(0) // Generate ChunkMetas for every label set. for i, lset := range lbls { var metas []chunks.Meta for j := 0; j <= (i % 20); j++ { + ref++ metas = append(metas, chunks.Meta{ MinTime: int64(j * 10000), - MaxTime: int64((j + 1) * 10000), - Ref: chunks.ChunkRef(rand.Uint64()), + MaxTime: int64((j+1)*10000) - 1, + Ref: chunks.ChunkRef(ref), Chunk: chunkenc.NewXORChunk(), }) } @@ -670,3 +671,51 @@ func TestDecoder_Postings_WrongInput(t *testing.T) { _, _, err := (&Decoder{}).Postings([]byte("the cake is a lie")) require.Error(t, err) } + +func TestChunksRefOrdering(t *testing.T) { + dir := t.TempDir() + + idx, err := NewWriter(context.Background(), filepath.Join(dir, "index")) + require.NoError(t, err) + + require.NoError(t, idx.AddSymbol("1")) + require.NoError(t, idx.AddSymbol("2")) + require.NoError(t, idx.AddSymbol("__name__")) + + c50 := chunks.Meta{Ref: 50} + c100 := chunks.Meta{Ref: 100} + c200 := chunks.Meta{Ref: 200} + + require.NoError(t, idx.AddSeries(1, labels.FromStrings("__name__", "1"), c100)) + require.EqualError(t, idx.AddSeries(2, labels.FromStrings("__name__", "2"), c50), "unsorted chunk reference: 50, previous: 100") + require.NoError(t, idx.AddSeries(2, labels.FromStrings("__name__", "2"), c200)) + require.NoError(t, idx.Close()) +} + +func TestChunksTimeOrdering(t *testing.T) { + dir := t.TempDir() + + idx, err := NewWriter(context.Background(), filepath.Join(dir, "index")) + require.NoError(t, err) + + require.NoError(t, idx.AddSymbol("1")) + require.NoError(t, idx.AddSymbol("2")) + require.NoError(t, idx.AddSymbol("__name__")) + + require.NoError(t, idx.AddSeries(1, labels.FromStrings("__name__", "1"), + chunks.Meta{Ref: 1, MinTime: 0, MaxTime: 10}, // Also checks that first chunk can have MinTime: 0. + chunks.Meta{Ref: 2, MinTime: 11, MaxTime: 20}, + chunks.Meta{Ref: 3, MinTime: 21, MaxTime: 30}, + )) + + require.EqualError(t, idx.AddSeries(1, labels.FromStrings("__name__", "2"), + chunks.Meta{Ref: 10, MinTime: 0, MaxTime: 10}, + chunks.Meta{Ref: 20, MinTime: 10, MaxTime: 20}, + ), "chunk minT 10 is not higher than previous chunk maxT 10") + + require.EqualError(t, idx.AddSeries(1, labels.FromStrings("__name__", "2"), + chunks.Meta{Ref: 10, MinTime: 100, MaxTime: 30}, + ), "chunk maxT 30 is less than minT 100") + + require.NoError(t, idx.Close()) +}