mirror of
				https://github.com/prometheus/prometheus.git
				synced 2025-11-04 02:11:01 +01:00 
			
		
		
		
	Instead of passing in a `ScratchBuilder` and `Labels`, just pass the builder and the caller can extract labels from it. In many cases the caller didn't use the Labels value anyway. Now in `Labels.ScratchBuilder` we need a slightly different API: one to assign what will be the result, instead of overwriting some other `Labels`. This is safer and easier to reason about. Signed-off-by: Bryan Boreham <bjboreham@gmail.com>
		
			
				
	
	
		
			1200 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1200 lines
		
	
	
		
			44 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2022 The Prometheus Authors
 | 
						|
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
// you may not use this file except in compliance with the License.
 | 
						|
// You may obtain a copy of the License at
 | 
						|
//
 | 
						|
// http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
//
 | 
						|
// Unless required by applicable law or agreed to in writing, software
 | 
						|
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
// See the License for the specific language governing permissions and
 | 
						|
// limitations under the License.
 | 
						|
 | 
						|
package tsdb
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"fmt"
 | 
						|
	"math"
 | 
						|
	"sort"
 | 
						|
	"testing"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/stretchr/testify/require"
 | 
						|
 | 
						|
	"github.com/prometheus/prometheus/model/labels"
 | 
						|
	"github.com/prometheus/prometheus/storage"
 | 
						|
	"github.com/prometheus/prometheus/tsdb/chunkenc"
 | 
						|
	"github.com/prometheus/prometheus/tsdb/chunks"
 | 
						|
	"github.com/prometheus/prometheus/tsdb/tsdbutil"
 | 
						|
)
 | 
						|
 | 
						|
type chunkInterval struct {
 | 
						|
	// because we permutate the order of chunks, we cannot determine at test declaration time which chunkRefs we expect in the Output.
 | 
						|
	// This ID matches expected output chunks against test input chunks, the test runner will assert the chunkRef for the matching chunk
 | 
						|
	ID   int
 | 
						|
	mint int64
 | 
						|
	maxt int64
 | 
						|
}
 | 
						|
 | 
						|
// permutateChunkIntervals returns all possible orders of the given chunkIntervals
 | 
						|
func permutateChunkIntervals(in []chunkInterval, out [][]chunkInterval, left, right int) [][]chunkInterval {
 | 
						|
	if left == right {
 | 
						|
		inCopy := make([]chunkInterval, len(in))
 | 
						|
		copy(inCopy, in)
 | 
						|
		return append(out, inCopy)
 | 
						|
	}
 | 
						|
	for i := left; i <= right; i++ {
 | 
						|
		in[left], in[i] = in[i], in[left]
 | 
						|
		out = permutateChunkIntervals(in, out, left+1, right)
 | 
						|
		in[left], in[i] = in[i], in[left]
 | 
						|
	}
 | 
						|
	return out
 | 
						|
}
 | 
						|
 | 
						|
// TestOOOHeadIndexReader_Series tests that the Series method works as expected.
 | 
						|
// However it does so by creating chunks and memory mapping them unlike other
 | 
						|
// tests of the head where samples are appended and we let the head memory map.
 | 
						|
// We do this because the ingestion path and the appender for out of order
 | 
						|
// samples are not ready yet.
 | 
						|
func TestOOOHeadIndexReader_Series(t *testing.T) {
 | 
						|
	tests := []struct {
 | 
						|
		name                string
 | 
						|
		queryMinT           int64
 | 
						|
		queryMaxT           int64
 | 
						|
		inputChunkIntervals []chunkInterval
 | 
						|
		expChunks           []chunkInterval
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:      "Empty result and no error when head is empty",
 | 
						|
			queryMinT: 0,
 | 
						|
			queryMaxT: 100,
 | 
						|
			expChunks: nil,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "If query interval is bigger than the existing chunks nothing is returned",
 | 
						|
			queryMinT: 500,
 | 
						|
			queryMaxT: 700,
 | 
						|
			inputChunkIntervals: []chunkInterval{
 | 
						|
				{0, 100, 400},
 | 
						|
			},
 | 
						|
			// ts                    0       100       150       200       250       300       350       400       450       500       550       600       650       700
 | 
						|
			// Query Interval                                                                                                  [---------------------------------------]
 | 
						|
			// Chunk 0                         [-----------------------------------------------------------]
 | 
						|
			expChunks: nil,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "If query interval is smaller than the existing chunks nothing is returned",
 | 
						|
			queryMinT: 100,
 | 
						|
			queryMaxT: 400,
 | 
						|
			inputChunkIntervals: []chunkInterval{
 | 
						|
				{0, 500, 700},
 | 
						|
			},
 | 
						|
			// ts                    0       100       150       200       250       300       350       400       450       500       550       600       650       700
 | 
						|
			// Query Interval                [-----------------------------------------------------------]
 | 
						|
			// Chunk 0:                                                                                                        [---------------------------------------]
 | 
						|
			expChunks: nil,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "If query interval exceeds the existing chunk, it is returned",
 | 
						|
			queryMinT: 100,
 | 
						|
			queryMaxT: 400,
 | 
						|
			inputChunkIntervals: []chunkInterval{
 | 
						|
				{0, 150, 350},
 | 
						|
			},
 | 
						|
			// ts                    0       100       150       200       250       300       350       400       450       500       550       600       650       700
 | 
						|
			// Query Interval                [-----------------------------------------------------------]
 | 
						|
			// Chunk 0:                                 [---------------------------------------]
 | 
						|
			expChunks: []chunkInterval{
 | 
						|
				{0, 150, 350},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "If chunk exceeds the query interval, it is returned",
 | 
						|
			queryMinT: 150,
 | 
						|
			queryMaxT: 350,
 | 
						|
			inputChunkIntervals: []chunkInterval{
 | 
						|
				{0, 100, 400},
 | 
						|
			},
 | 
						|
			// ts                    0       100       150       200       250       300       350       400       450       500       550       600       650       700
 | 
						|
			// Query Interval:                          [---------------------------------------]
 | 
						|
			// Chunk 0:                       [-----------------------------------------------------------]
 | 
						|
			expChunks: []chunkInterval{
 | 
						|
				{0, 100, 400},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "Pairwise overlaps should return the references of the first of each pair",
 | 
						|
			queryMinT: 0,
 | 
						|
			queryMaxT: 700,
 | 
						|
			inputChunkIntervals: []chunkInterval{
 | 
						|
				{0, 100, 200},
 | 
						|
				{1, 500, 600},
 | 
						|
				{2, 150, 250},
 | 
						|
				{3, 550, 650},
 | 
						|
			},
 | 
						|
			// ts                    0       100       150       200       250       300       350       400       450       500       550       600       650       700
 | 
						|
			// Query Interval        [---------------------------------------------------------------------------------------------------------------------------------]
 | 
						|
			// Chunk 0:                        [-------------------]
 | 
						|
			// Chunk 1:                                                                                                        [-------------------]
 | 
						|
			// Chunk 2:                                  [-------------------]
 | 
						|
			// Chunk 3:                                                                                                                  [-------------------]
 | 
						|
			// Output Graphically              [-----------------------------]                                                 [-----------------------------]
 | 
						|
			expChunks: []chunkInterval{
 | 
						|
				{0, 100, 250},
 | 
						|
				{1, 500, 650},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "If all chunks overlap, single big chunk is returned",
 | 
						|
			queryMinT: 0,
 | 
						|
			queryMaxT: 700,
 | 
						|
			inputChunkIntervals: []chunkInterval{
 | 
						|
				{0, 100, 200},
 | 
						|
				{1, 200, 300},
 | 
						|
				{2, 300, 400},
 | 
						|
				{3, 400, 500},
 | 
						|
			},
 | 
						|
			// ts                    0       100       150       200       250       300       350       400       450       500       550       600       650       700
 | 
						|
			// Query Interval        [---------------------------------------------------------------------------------------------------------------------------------]
 | 
						|
			// Chunk 0:                        [-------------------]
 | 
						|
			// Chunk 1:                                            [-------------------]
 | 
						|
			// Chunk 2:                                                                [-------------------]
 | 
						|
			// Chunk 3:                                                                                    [------------------]
 | 
						|
			// Output Graphically              [------------------------------------------------------------------------------]
 | 
						|
			expChunks: []chunkInterval{
 | 
						|
				{0, 100, 500},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "If no chunks overlap, all chunks are returned",
 | 
						|
			queryMinT: 0,
 | 
						|
			queryMaxT: 700,
 | 
						|
			inputChunkIntervals: []chunkInterval{
 | 
						|
				{0, 100, 199},
 | 
						|
				{1, 200, 299},
 | 
						|
				{2, 300, 399},
 | 
						|
				{3, 400, 499},
 | 
						|
			},
 | 
						|
			// ts                    0       100       150       200       250       300       350       400       450       500       550       600       650       700
 | 
						|
			// Query Interval        [---------------------------------------------------------------------------------------------------------------------------------]
 | 
						|
			// Chunk 0:                        [------------------]
 | 
						|
			// Chunk 1:                                            [------------------]
 | 
						|
			// Chunk 2:                                                                [------------------]
 | 
						|
			// Chunk 3:                                                                                    [------------------]
 | 
						|
			// Output Graphically              [------------------][------------------][------------------][------------------]
 | 
						|
			expChunks: []chunkInterval{
 | 
						|
				{0, 100, 199},
 | 
						|
				{1, 200, 299},
 | 
						|
				{2, 300, 399},
 | 
						|
				{3, 400, 499},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "Triplet with pairwise overlaps, query range covers all, and distractor extra chunk",
 | 
						|
			queryMinT: 0,
 | 
						|
			queryMaxT: 400,
 | 
						|
			inputChunkIntervals: []chunkInterval{
 | 
						|
				{0, 100, 200},
 | 
						|
				{1, 150, 300},
 | 
						|
				{2, 250, 350},
 | 
						|
				{3, 450, 550},
 | 
						|
			},
 | 
						|
			// ts                    0       100       150       200       250       300       350       400       450       500       550       600       650       700
 | 
						|
			// Query Interval        [--------------------------------------------------------------------]
 | 
						|
			// Chunk 0:                        [------------------]
 | 
						|
			// Chunk 1:                                 [-----------------------------]
 | 
						|
			// Chunk 2:                                                     [------------------]
 | 
						|
			// Chunk 3:                                                                                             [------------------]
 | 
						|
			// Output Graphically              [-----------------------------------------------]
 | 
						|
			expChunks: []chunkInterval{
 | 
						|
				{0, 100, 350},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "Query interval partially overlaps some chunks",
 | 
						|
			queryMinT: 100,
 | 
						|
			queryMaxT: 400,
 | 
						|
			inputChunkIntervals: []chunkInterval{
 | 
						|
				{0, 250, 500},
 | 
						|
				{1, 0, 200},
 | 
						|
				{2, 150, 300},
 | 
						|
			},
 | 
						|
			// ts                    0       100       150       200       250       300       350       400       450       500       550       600       650       700
 | 
						|
			// Query Interval                [------------------------------------------------------------]
 | 
						|
			// Chunk 0:                                                     [-------------------------------------------------]
 | 
						|
			// Chunk 1:             [-----------------------------]
 | 
						|
			// Chunk 2:                                [------------------------------]
 | 
						|
			// Output Graphically   [-----------------------------------------------------------------------------------------]
 | 
						|
			expChunks: []chunkInterval{
 | 
						|
				{1, 0, 500},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "A full overlap pair and disjointed triplet",
 | 
						|
			queryMinT: 0,
 | 
						|
			queryMaxT: 900,
 | 
						|
			inputChunkIntervals: []chunkInterval{
 | 
						|
				{0, 100, 300},
 | 
						|
				{1, 770, 850},
 | 
						|
				{2, 150, 250},
 | 
						|
				{3, 650, 750},
 | 
						|
				{4, 600, 800},
 | 
						|
			},
 | 
						|
			// ts                    0       100       150       200       250       300       350       400       450       500       550       600       650       700       750       800       850
 | 
						|
			// Query Interval        [---------------------------------------------------------------------------------------------------------------------------------------------------------------]
 | 
						|
			// Chunk 0:                        [---------------------------------------]
 | 
						|
			// Chunk 1:                                                                                                                                                               [--------------]
 | 
						|
			// Chunk 2:                                  [-------------------]
 | 
						|
			// Chunk 3:                                                                                                                                      [-------------------]
 | 
						|
			// Chunk 4:                                                                                                                             [---------------------------------------]
 | 
						|
			// Output Graphically              [---------------------------------------]                                                            [------------------------------------------------]
 | 
						|
			expChunks: []chunkInterval{
 | 
						|
				{0, 100, 300},
 | 
						|
				{4, 600, 850},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:      "Query range covers 3 disjoint chunks",
 | 
						|
			queryMinT: 0,
 | 
						|
			queryMaxT: 650,
 | 
						|
			inputChunkIntervals: []chunkInterval{
 | 
						|
				{0, 100, 150},
 | 
						|
				{1, 300, 350},
 | 
						|
				{2, 200, 250},
 | 
						|
			},
 | 
						|
			// ts                    0       100       150       200       250       300       350       400       450       500       550       600       650       700       750       800       850
 | 
						|
			// Query Interval        [----------------------------------------------------------------------------------------------------------------------]
 | 
						|
			// Chunk 0:                        [-------]
 | 
						|
			// Chunk 1:                                                              [----------]
 | 
						|
			// Chunk 2:                                           [--------]
 | 
						|
			// Output Graphically              [-------]          [--------]         [----------]
 | 
						|
			expChunks: []chunkInterval{
 | 
						|
				{0, 100, 150},
 | 
						|
				{1, 300, 350},
 | 
						|
				{2, 200, 250},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	s1Lset := labels.FromStrings("foo", "bar")
 | 
						|
	s1ID := uint64(1)
 | 
						|
 | 
						|
	for _, tc := range tests {
 | 
						|
		var permutations [][]chunkInterval
 | 
						|
		if len(tc.inputChunkIntervals) == 0 {
 | 
						|
			// handle special case
 | 
						|
			permutations = [][]chunkInterval{
 | 
						|
				nil,
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			permutations = permutateChunkIntervals(tc.inputChunkIntervals, nil, 0, len(tc.inputChunkIntervals)-1)
 | 
						|
		}
 | 
						|
		for perm, intervals := range permutations {
 | 
						|
			for _, headChunk := range []bool{false, true} {
 | 
						|
				t.Run(fmt.Sprintf("name=%s, permutation=%d, headChunk=%t", tc.name, perm, headChunk), func(t *testing.T) {
 | 
						|
					h, _ := newTestHead(t, 1000, false, true)
 | 
						|
					defer func() {
 | 
						|
						require.NoError(t, h.Close())
 | 
						|
					}()
 | 
						|
					require.NoError(t, h.Init(0))
 | 
						|
 | 
						|
					s1, _, _ := h.getOrCreate(s1ID, s1Lset)
 | 
						|
 | 
						|
					var lastChunk chunkInterval
 | 
						|
					var lastChunkPos int
 | 
						|
 | 
						|
					// the marker should be set based on whichever is the last chunk/interval that overlaps with the query range
 | 
						|
					for i, interv := range intervals {
 | 
						|
						if overlapsClosedInterval(interv.mint, interv.maxt, tc.queryMinT, tc.queryMaxT) {
 | 
						|
							lastChunk = interv
 | 
						|
							lastChunkPos = i
 | 
						|
						}
 | 
						|
					}
 | 
						|
					lastChunkRef := chunks.ChunkRef(chunks.NewHeadChunkRef(1, chunks.HeadChunkID(uint64(lastChunkPos))))
 | 
						|
 | 
						|
					// define our expected chunks, by looking at the expected ChunkIntervals and setting...
 | 
						|
					var expChunks []chunks.Meta
 | 
						|
					for _, e := range tc.expChunks {
 | 
						|
						meta := chunks.Meta{
 | 
						|
							Chunk:   chunkenc.Chunk(nil),
 | 
						|
							MinTime: e.mint,
 | 
						|
							MaxTime: e.maxt,
 | 
						|
							// markers based on the last chunk we found above
 | 
						|
							OOOLastMinTime: lastChunk.mint,
 | 
						|
							OOOLastMaxTime: lastChunk.maxt,
 | 
						|
							OOOLastRef:     lastChunkRef,
 | 
						|
						}
 | 
						|
 | 
						|
						// Ref to whatever Ref the chunk has, that we refer to by ID
 | 
						|
						for ref, c := range intervals {
 | 
						|
							if c.ID == e.ID {
 | 
						|
								meta.Ref = chunks.ChunkRef(chunks.NewHeadChunkRef(chunks.HeadSeriesRef(s1ID), chunks.HeadChunkID(ref)))
 | 
						|
								break
 | 
						|
							}
 | 
						|
						}
 | 
						|
						expChunks = append(expChunks, meta)
 | 
						|
					}
 | 
						|
					sort.Sort(metaByMinTimeAndMinRef(expChunks)) // we always want the chunks to come back sorted by minTime asc
 | 
						|
 | 
						|
					if headChunk && len(intervals) > 0 {
 | 
						|
						// Put the last interval in the head chunk
 | 
						|
						s1.oooHeadChunk = &oooHeadChunk{
 | 
						|
							minTime: intervals[len(intervals)-1].mint,
 | 
						|
							maxTime: intervals[len(intervals)-1].maxt,
 | 
						|
						}
 | 
						|
						intervals = intervals[:len(intervals)-1]
 | 
						|
					}
 | 
						|
 | 
						|
					for _, ic := range intervals {
 | 
						|
						s1.oooMmappedChunks = append(s1.oooMmappedChunks, &mmappedChunk{
 | 
						|
							minTime: ic.mint,
 | 
						|
							maxTime: ic.maxt,
 | 
						|
						})
 | 
						|
					}
 | 
						|
 | 
						|
					ir := NewOOOHeadIndexReader(h, tc.queryMinT, tc.queryMaxT)
 | 
						|
 | 
						|
					var chks []chunks.Meta
 | 
						|
					var b labels.ScratchBuilder
 | 
						|
					err := ir.Series(storage.SeriesRef(s1ID), &b, &chks)
 | 
						|
					require.NoError(t, err)
 | 
						|
					require.Equal(t, s1Lset, b.Labels())
 | 
						|
					require.Equal(t, expChunks, chks)
 | 
						|
 | 
						|
					err = ir.Series(storage.SeriesRef(s1ID+1), &b, &chks)
 | 
						|
					require.Equal(t, storage.ErrNotFound, err)
 | 
						|
				})
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func TestOOOHeadChunkReader_LabelValues(t *testing.T) {
 | 
						|
	chunkRange := int64(2000)
 | 
						|
	head, _ := newTestHead(t, chunkRange, false, true)
 | 
						|
	t.Cleanup(func() { require.NoError(t, head.Close()) })
 | 
						|
 | 
						|
	app := head.Appender(context.Background())
 | 
						|
 | 
						|
	// Add in-order samples
 | 
						|
	_, err := app.Append(0, labels.FromStrings("foo", "bar1"), 100, 1)
 | 
						|
	require.NoError(t, err)
 | 
						|
	_, err = app.Append(0, labels.FromStrings("foo", "bar2"), 100, 2)
 | 
						|
	require.NoError(t, err)
 | 
						|
 | 
						|
	// Add ooo samples for those series
 | 
						|
	_, err = app.Append(0, labels.FromStrings("foo", "bar1"), 90, 1)
 | 
						|
	require.NoError(t, err)
 | 
						|
	_, err = app.Append(0, labels.FromStrings("foo", "bar2"), 90, 2)
 | 
						|
	require.NoError(t, err)
 | 
						|
 | 
						|
	require.NoError(t, app.Commit())
 | 
						|
 | 
						|
	cases := []struct {
 | 
						|
		name       string
 | 
						|
		queryMinT  int64
 | 
						|
		queryMaxT  int64
 | 
						|
		expValues1 []string
 | 
						|
		expValues2 []string
 | 
						|
		expValues3 []string
 | 
						|
		expValues4 []string
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:       "LabelValues calls when ooo head has max query range",
 | 
						|
			queryMinT:  math.MinInt64,
 | 
						|
			queryMaxT:  math.MaxInt64,
 | 
						|
			expValues1: []string{"bar1"},
 | 
						|
			expValues2: []string{},
 | 
						|
			expValues3: []string{"bar1", "bar2"},
 | 
						|
			expValues4: []string{"bar1", "bar2"},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:       "LabelValues calls with ooo head query range not overlapping in-order data",
 | 
						|
			queryMinT:  90,
 | 
						|
			queryMaxT:  90,
 | 
						|
			expValues1: []string{"bar1"},
 | 
						|
			expValues2: []string{},
 | 
						|
			expValues3: []string{"bar1", "bar2"},
 | 
						|
			expValues4: []string{"bar1", "bar2"},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:       "LabelValues calls with ooo head query range not overlapping out-of-order data",
 | 
						|
			queryMinT:  100,
 | 
						|
			queryMaxT:  100,
 | 
						|
			expValues1: []string{},
 | 
						|
			expValues2: []string{},
 | 
						|
			expValues3: []string{},
 | 
						|
			expValues4: []string{},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range cases {
 | 
						|
		t.Run(tc.name, func(t *testing.T) {
 | 
						|
			// We first want to test using a head index reader that covers the biggest query interval
 | 
						|
			oh := NewOOOHeadIndexReader(head, tc.queryMinT, tc.queryMaxT)
 | 
						|
			matchers := []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "foo", "bar1")}
 | 
						|
			values, err := oh.LabelValues("foo", matchers...)
 | 
						|
			sort.Strings(values)
 | 
						|
			require.NoError(t, err)
 | 
						|
			require.Equal(t, tc.expValues1, values)
 | 
						|
 | 
						|
			matchers = []*labels.Matcher{labels.MustNewMatcher(labels.MatchNotRegexp, "foo", "^bar.")}
 | 
						|
			values, err = oh.LabelValues("foo", matchers...)
 | 
						|
			sort.Strings(values)
 | 
						|
			require.NoError(t, err)
 | 
						|
			require.Equal(t, tc.expValues2, values)
 | 
						|
 | 
						|
			matchers = []*labels.Matcher{labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.")}
 | 
						|
			values, err = oh.LabelValues("foo", matchers...)
 | 
						|
			sort.Strings(values)
 | 
						|
			require.NoError(t, err)
 | 
						|
			require.Equal(t, tc.expValues3, values)
 | 
						|
 | 
						|
			values, err = oh.LabelValues("foo")
 | 
						|
			sort.Strings(values)
 | 
						|
			require.NoError(t, err)
 | 
						|
			require.Equal(t, tc.expValues4, values)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// TestOOOHeadChunkReader_Chunk tests that the Chunk method works as expected.
 | 
						|
// It does so by appending out of order samples to the db and then initializing
 | 
						|
// an OOOHeadChunkReader to read chunks from it.
 | 
						|
func TestOOOHeadChunkReader_Chunk(t *testing.T) {
 | 
						|
	opts := DefaultOptions()
 | 
						|
	opts.OutOfOrderCapMax = 5
 | 
						|
	opts.OutOfOrderTimeWindow = 120 * time.Minute.Milliseconds()
 | 
						|
 | 
						|
	s1 := labels.FromStrings("l", "v1")
 | 
						|
	minutes := func(m int64) int64 { return m * time.Minute.Milliseconds() }
 | 
						|
 | 
						|
	appendSample := func(app storage.Appender, l labels.Labels, timestamp int64, value float64) storage.SeriesRef {
 | 
						|
		ref, err := app.Append(0, l, timestamp, value)
 | 
						|
		require.NoError(t, err)
 | 
						|
		return ref
 | 
						|
	}
 | 
						|
 | 
						|
	t.Run("Getting a non existing chunk fails with not found error", func(t *testing.T) {
 | 
						|
		db := newTestDBWithOpts(t, opts)
 | 
						|
 | 
						|
		cr := NewOOOHeadChunkReader(db.head, 0, 1000)
 | 
						|
		c, err := cr.Chunk(chunks.Meta{
 | 
						|
			Ref: 0x1000000, Chunk: chunkenc.Chunk(nil), MinTime: 100, MaxTime: 300,
 | 
						|
		})
 | 
						|
		require.Equal(t, err, fmt.Errorf("not found"))
 | 
						|
		require.Equal(t, c, nil)
 | 
						|
	})
 | 
						|
 | 
						|
	tests := []struct {
 | 
						|
		name                 string
 | 
						|
		queryMinT            int64
 | 
						|
		queryMaxT            int64
 | 
						|
		firstInOrderSampleAt int64
 | 
						|
		inputSamples         tsdbutil.SampleSlice
 | 
						|
		expChunkError        bool
 | 
						|
		expChunksSamples     []tsdbutil.SampleSlice
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:                 "Getting the head when there are no overlapping chunks returns just the samples in the head",
 | 
						|
			queryMinT:            minutes(0),
 | 
						|
			queryMaxT:            minutes(100),
 | 
						|
			firstInOrderSampleAt: minutes(120),
 | 
						|
			inputSamples: tsdbutil.SampleSlice{
 | 
						|
				sample{t: minutes(30), v: float64(0)},
 | 
						|
				sample{t: minutes(40), v: float64(0)},
 | 
						|
			},
 | 
						|
			expChunkError: false,
 | 
						|
			// ts (in minutes)         0       10       20       30       40       50       60       70       80       90       100
 | 
						|
			// Query Interval          [------------------------------------------------------------------------------------------]
 | 
						|
			// Chunk 0: Current Head                              [--------] (With 2 samples)
 | 
						|
			// Output Graphically                                 [--------] (With 2 samples)
 | 
						|
			expChunksSamples: []tsdbutil.SampleSlice{
 | 
						|
				{
 | 
						|
					sample{t: minutes(30), v: float64(0)},
 | 
						|
					sample{t: minutes(40), v: float64(0)},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:                 "Getting the head chunk when there are overlapping chunks returns all combined",
 | 
						|
			queryMinT:            minutes(0),
 | 
						|
			queryMaxT:            minutes(100),
 | 
						|
			firstInOrderSampleAt: minutes(120),
 | 
						|
			inputSamples: tsdbutil.SampleSlice{
 | 
						|
				// opts.OOOCapMax is 5 so these will be mmapped to the first mmapped chunk
 | 
						|
				sample{t: minutes(41), v: float64(0)},
 | 
						|
				sample{t: minutes(42), v: float64(0)},
 | 
						|
				sample{t: minutes(43), v: float64(0)},
 | 
						|
				sample{t: minutes(44), v: float64(0)},
 | 
						|
				sample{t: minutes(45), v: float64(0)},
 | 
						|
				// The following samples will go to the head chunk, and we want it
 | 
						|
				// to overlap with the previous chunk
 | 
						|
				sample{t: minutes(30), v: float64(1)},
 | 
						|
				sample{t: minutes(50), v: float64(1)},
 | 
						|
			},
 | 
						|
			expChunkError: false,
 | 
						|
			// ts (in minutes)         0       10       20       30       40       50       60       70       80       90       100
 | 
						|
			// Query Interval          [------------------------------------------------------------------------------------------]
 | 
						|
			// Chunk 0                                                     [---] (With 5 samples)
 | 
						|
			// Chunk 1: Current Head                              [-----------------] (With 2 samples)
 | 
						|
			// Output Graphically                                 [-----------------] (With 7 samples)
 | 
						|
			expChunksSamples: []tsdbutil.SampleSlice{
 | 
						|
				{
 | 
						|
					sample{t: minutes(30), v: float64(1)},
 | 
						|
					sample{t: minutes(41), v: float64(0)},
 | 
						|
					sample{t: minutes(42), v: float64(0)},
 | 
						|
					sample{t: minutes(43), v: float64(0)},
 | 
						|
					sample{t: minutes(44), v: float64(0)},
 | 
						|
					sample{t: minutes(45), v: float64(0)},
 | 
						|
					sample{t: minutes(50), v: float64(1)},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:                 "Two windows of overlapping chunks get properly converged",
 | 
						|
			queryMinT:            minutes(0),
 | 
						|
			queryMaxT:            minutes(100),
 | 
						|
			firstInOrderSampleAt: minutes(120),
 | 
						|
			inputSamples: tsdbutil.SampleSlice{
 | 
						|
				// Chunk 0
 | 
						|
				sample{t: minutes(10), v: float64(0)},
 | 
						|
				sample{t: minutes(12), v: float64(0)},
 | 
						|
				sample{t: minutes(14), v: float64(0)},
 | 
						|
				sample{t: minutes(16), v: float64(0)},
 | 
						|
				sample{t: minutes(20), v: float64(0)},
 | 
						|
				// Chunk 1
 | 
						|
				sample{t: minutes(20), v: float64(1)},
 | 
						|
				sample{t: minutes(22), v: float64(1)},
 | 
						|
				sample{t: minutes(24), v: float64(1)},
 | 
						|
				sample{t: minutes(26), v: float64(1)},
 | 
						|
				sample{t: minutes(29), v: float64(1)},
 | 
						|
				// Chunk 2
 | 
						|
				sample{t: minutes(30), v: float64(2)},
 | 
						|
				sample{t: minutes(32), v: float64(2)},
 | 
						|
				sample{t: minutes(34), v: float64(2)},
 | 
						|
				sample{t: minutes(36), v: float64(2)},
 | 
						|
				sample{t: minutes(40), v: float64(2)},
 | 
						|
				// Head
 | 
						|
				sample{t: minutes(40), v: float64(3)},
 | 
						|
				sample{t: minutes(50), v: float64(3)},
 | 
						|
			},
 | 
						|
			expChunkError: false,
 | 
						|
			// ts (in minutes)         0       10       20       30       40       50       60       70       80       90       100
 | 
						|
			// Query Interval          [------------------------------------------------------------------------------------------]
 | 
						|
			// Chunk 0                          [--------]
 | 
						|
			// Chunk 1                                   [-------]
 | 
						|
			// Chunk 2                                            [--------]
 | 
						|
			// Chunk 3: Current Head                                       [--------]
 | 
						|
			// Output Graphically               [----------------][-----------------]
 | 
						|
			expChunksSamples: []tsdbutil.SampleSlice{
 | 
						|
				{
 | 
						|
					sample{t: minutes(10), v: float64(0)},
 | 
						|
					sample{t: minutes(12), v: float64(0)},
 | 
						|
					sample{t: minutes(14), v: float64(0)},
 | 
						|
					sample{t: minutes(16), v: float64(0)},
 | 
						|
					sample{t: minutes(20), v: float64(1)},
 | 
						|
					sample{t: minutes(22), v: float64(1)},
 | 
						|
					sample{t: minutes(24), v: float64(1)},
 | 
						|
					sample{t: minutes(26), v: float64(1)},
 | 
						|
					sample{t: minutes(29), v: float64(1)},
 | 
						|
				},
 | 
						|
				{
 | 
						|
					sample{t: minutes(30), v: float64(2)},
 | 
						|
					sample{t: minutes(32), v: float64(2)},
 | 
						|
					sample{t: minutes(34), v: float64(2)},
 | 
						|
					sample{t: minutes(36), v: float64(2)},
 | 
						|
					sample{t: minutes(40), v: float64(3)},
 | 
						|
					sample{t: minutes(50), v: float64(3)},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:                 "Two windows of overlapping chunks in descending order get properly converged",
 | 
						|
			queryMinT:            minutes(0),
 | 
						|
			queryMaxT:            minutes(100),
 | 
						|
			firstInOrderSampleAt: minutes(120),
 | 
						|
			inputSamples: tsdbutil.SampleSlice{
 | 
						|
				// Chunk 0
 | 
						|
				sample{t: minutes(40), v: float64(0)},
 | 
						|
				sample{t: minutes(42), v: float64(0)},
 | 
						|
				sample{t: minutes(44), v: float64(0)},
 | 
						|
				sample{t: minutes(46), v: float64(0)},
 | 
						|
				sample{t: minutes(50), v: float64(0)},
 | 
						|
				// Chunk 1
 | 
						|
				sample{t: minutes(30), v: float64(1)},
 | 
						|
				sample{t: minutes(32), v: float64(1)},
 | 
						|
				sample{t: minutes(34), v: float64(1)},
 | 
						|
				sample{t: minutes(36), v: float64(1)},
 | 
						|
				sample{t: minutes(40), v: float64(1)},
 | 
						|
				// Chunk 2
 | 
						|
				sample{t: minutes(20), v: float64(2)},
 | 
						|
				sample{t: minutes(22), v: float64(2)},
 | 
						|
				sample{t: minutes(24), v: float64(2)},
 | 
						|
				sample{t: minutes(26), v: float64(2)},
 | 
						|
				sample{t: minutes(29), v: float64(2)},
 | 
						|
				// Head
 | 
						|
				sample{t: minutes(10), v: float64(3)},
 | 
						|
				sample{t: minutes(20), v: float64(3)},
 | 
						|
			},
 | 
						|
			expChunkError: false,
 | 
						|
			// ts (in minutes)         0       10       20       30       40       50       60       70       80       90       100
 | 
						|
			// Query Interval          [------------------------------------------------------------------------------------------]
 | 
						|
			// Chunk 0                                                     [--------]
 | 
						|
			// Chunk 1                                            [--------]
 | 
						|
			// Chunk 2                                   [-------]
 | 
						|
			// Chunk 3: Current Head            [--------]
 | 
						|
			// Output Graphically               [----------------][-----------------]
 | 
						|
			expChunksSamples: []tsdbutil.SampleSlice{
 | 
						|
				{
 | 
						|
					sample{t: minutes(10), v: float64(3)},
 | 
						|
					sample{t: minutes(20), v: float64(2)},
 | 
						|
					sample{t: minutes(22), v: float64(2)},
 | 
						|
					sample{t: minutes(24), v: float64(2)},
 | 
						|
					sample{t: minutes(26), v: float64(2)},
 | 
						|
					sample{t: minutes(29), v: float64(2)},
 | 
						|
				},
 | 
						|
				{
 | 
						|
					sample{t: minutes(30), v: float64(1)},
 | 
						|
					sample{t: minutes(32), v: float64(1)},
 | 
						|
					sample{t: minutes(34), v: float64(1)},
 | 
						|
					sample{t: minutes(36), v: float64(1)},
 | 
						|
					sample{t: minutes(40), v: float64(0)},
 | 
						|
					sample{t: minutes(42), v: float64(0)},
 | 
						|
					sample{t: minutes(44), v: float64(0)},
 | 
						|
					sample{t: minutes(46), v: float64(0)},
 | 
						|
					sample{t: minutes(50), v: float64(0)},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:                 "If chunks are not overlapped they are not converged",
 | 
						|
			queryMinT:            minutes(0),
 | 
						|
			queryMaxT:            minutes(100),
 | 
						|
			firstInOrderSampleAt: minutes(120),
 | 
						|
			inputSamples: tsdbutil.SampleSlice{
 | 
						|
				// Chunk 0
 | 
						|
				sample{t: minutes(10), v: float64(0)},
 | 
						|
				sample{t: minutes(12), v: float64(0)},
 | 
						|
				sample{t: minutes(14), v: float64(0)},
 | 
						|
				sample{t: minutes(16), v: float64(0)},
 | 
						|
				sample{t: minutes(18), v: float64(0)},
 | 
						|
				// Chunk 1
 | 
						|
				sample{t: minutes(20), v: float64(1)},
 | 
						|
				sample{t: minutes(22), v: float64(1)},
 | 
						|
				sample{t: minutes(24), v: float64(1)},
 | 
						|
				sample{t: minutes(26), v: float64(1)},
 | 
						|
				sample{t: minutes(28), v: float64(1)},
 | 
						|
				// Chunk 2
 | 
						|
				sample{t: minutes(30), v: float64(2)},
 | 
						|
				sample{t: minutes(32), v: float64(2)},
 | 
						|
				sample{t: minutes(34), v: float64(2)},
 | 
						|
				sample{t: minutes(36), v: float64(2)},
 | 
						|
				sample{t: minutes(38), v: float64(2)},
 | 
						|
				// Head
 | 
						|
				sample{t: minutes(40), v: float64(3)},
 | 
						|
				sample{t: minutes(42), v: float64(3)},
 | 
						|
			},
 | 
						|
			expChunkError: false,
 | 
						|
			// ts (in minutes)         0       10       20       30       40       50       60       70       80       90       100
 | 
						|
			// Query Interval          [------------------------------------------------------------------------------------------]
 | 
						|
			// Chunk 0                          [-------]
 | 
						|
			// Chunk 1                                   [-------]
 | 
						|
			// Chunk 2                                            [-------]
 | 
						|
			// Chunk 3: Current Head                                       [-------]
 | 
						|
			// Output Graphically               [-------][-------][-------][--------]
 | 
						|
			expChunksSamples: []tsdbutil.SampleSlice{
 | 
						|
				{
 | 
						|
					sample{t: minutes(10), v: float64(0)},
 | 
						|
					sample{t: minutes(12), v: float64(0)},
 | 
						|
					sample{t: minutes(14), v: float64(0)},
 | 
						|
					sample{t: minutes(16), v: float64(0)},
 | 
						|
					sample{t: minutes(18), v: float64(0)},
 | 
						|
				},
 | 
						|
				{
 | 
						|
					sample{t: minutes(20), v: float64(1)},
 | 
						|
					sample{t: minutes(22), v: float64(1)},
 | 
						|
					sample{t: minutes(24), v: float64(1)},
 | 
						|
					sample{t: minutes(26), v: float64(1)},
 | 
						|
					sample{t: minutes(28), v: float64(1)},
 | 
						|
				},
 | 
						|
				{
 | 
						|
					sample{t: minutes(30), v: float64(2)},
 | 
						|
					sample{t: minutes(32), v: float64(2)},
 | 
						|
					sample{t: minutes(34), v: float64(2)},
 | 
						|
					sample{t: minutes(36), v: float64(2)},
 | 
						|
					sample{t: minutes(38), v: float64(2)},
 | 
						|
				},
 | 
						|
				{
 | 
						|
					sample{t: minutes(40), v: float64(3)},
 | 
						|
					sample{t: minutes(42), v: float64(3)},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:                 "Triplet of chunks overlapping returns a single merged chunk",
 | 
						|
			queryMinT:            minutes(0),
 | 
						|
			queryMaxT:            minutes(100),
 | 
						|
			firstInOrderSampleAt: minutes(120),
 | 
						|
			inputSamples: tsdbutil.SampleSlice{
 | 
						|
				// Chunk 0
 | 
						|
				sample{t: minutes(10), v: float64(0)},
 | 
						|
				sample{t: minutes(15), v: float64(0)},
 | 
						|
				sample{t: minutes(20), v: float64(0)},
 | 
						|
				sample{t: minutes(25), v: float64(0)},
 | 
						|
				sample{t: minutes(30), v: float64(0)},
 | 
						|
				// Chunk 1
 | 
						|
				sample{t: minutes(20), v: float64(1)},
 | 
						|
				sample{t: minutes(25), v: float64(1)},
 | 
						|
				sample{t: minutes(30), v: float64(1)},
 | 
						|
				sample{t: minutes(35), v: float64(1)},
 | 
						|
				sample{t: minutes(42), v: float64(1)},
 | 
						|
				// Chunk 2 Head
 | 
						|
				sample{t: minutes(32), v: float64(2)},
 | 
						|
				sample{t: minutes(50), v: float64(2)},
 | 
						|
			},
 | 
						|
			expChunkError: false,
 | 
						|
			// ts (in minutes)         0       10       20       30       40       50       60       70       80       90       100
 | 
						|
			// Query Interval          [------------------------------------------------------------------------------------------]
 | 
						|
			// Chunk 0                          [-----------------]
 | 
						|
			// Chunk 1                                   [--------------------]
 | 
						|
			// Chunk 2 Current Head                                  [--------------]
 | 
						|
			// Output Graphically               [-----------------------------------]
 | 
						|
			expChunksSamples: []tsdbutil.SampleSlice{
 | 
						|
				{
 | 
						|
					sample{t: minutes(10), v: float64(0)},
 | 
						|
					sample{t: minutes(15), v: float64(0)},
 | 
						|
					sample{t: minutes(20), v: float64(1)},
 | 
						|
					sample{t: minutes(25), v: float64(1)},
 | 
						|
					sample{t: minutes(30), v: float64(1)},
 | 
						|
					sample{t: minutes(32), v: float64(2)},
 | 
						|
					sample{t: minutes(35), v: float64(1)},
 | 
						|
					sample{t: minutes(42), v: float64(1)},
 | 
						|
					sample{t: minutes(50), v: float64(2)},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:                 "Query interval partially overlaps with a triplet of chunks but still returns a single merged chunk",
 | 
						|
			queryMinT:            minutes(12),
 | 
						|
			queryMaxT:            minutes(33),
 | 
						|
			firstInOrderSampleAt: minutes(120),
 | 
						|
			inputSamples: tsdbutil.SampleSlice{
 | 
						|
				// Chunk 0
 | 
						|
				sample{t: minutes(10), v: float64(0)},
 | 
						|
				sample{t: minutes(15), v: float64(0)},
 | 
						|
				sample{t: minutes(20), v: float64(0)},
 | 
						|
				sample{t: minutes(25), v: float64(0)},
 | 
						|
				sample{t: minutes(30), v: float64(0)},
 | 
						|
				// Chunk 1
 | 
						|
				sample{t: minutes(20), v: float64(1)},
 | 
						|
				sample{t: minutes(25), v: float64(1)},
 | 
						|
				sample{t: minutes(30), v: float64(1)},
 | 
						|
				sample{t: minutes(35), v: float64(1)},
 | 
						|
				sample{t: minutes(42), v: float64(1)},
 | 
						|
				// Chunk 2 Head
 | 
						|
				sample{t: minutes(32), v: float64(2)},
 | 
						|
				sample{t: minutes(50), v: float64(2)},
 | 
						|
			},
 | 
						|
			expChunkError: false,
 | 
						|
			// ts (in minutes)         0       10       20       30       40       50       60       70       80       90       100
 | 
						|
			// Query Interval                     [------------------]
 | 
						|
			// Chunk 0                          [-----------------]
 | 
						|
			// Chunk 1                                   [--------------------]
 | 
						|
			// Chunk 2 Current Head                                  [--------------]
 | 
						|
			// Output Graphically               [-----------------------------------]
 | 
						|
			expChunksSamples: []tsdbutil.SampleSlice{
 | 
						|
				{
 | 
						|
					sample{t: minutes(10), v: float64(0)},
 | 
						|
					sample{t: minutes(15), v: float64(0)},
 | 
						|
					sample{t: minutes(20), v: float64(1)},
 | 
						|
					sample{t: minutes(25), v: float64(1)},
 | 
						|
					sample{t: minutes(30), v: float64(1)},
 | 
						|
					sample{t: minutes(32), v: float64(2)},
 | 
						|
					sample{t: minutes(35), v: float64(1)},
 | 
						|
					sample{t: minutes(42), v: float64(1)},
 | 
						|
					sample{t: minutes(50), v: float64(2)},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range tests {
 | 
						|
		t.Run(fmt.Sprintf("name=%s", tc.name), func(t *testing.T) {
 | 
						|
			db := newTestDBWithOpts(t, opts)
 | 
						|
 | 
						|
			app := db.Appender(context.Background())
 | 
						|
			s1Ref := appendSample(app, s1, tc.firstInOrderSampleAt, float64(tc.firstInOrderSampleAt/1*time.Minute.Milliseconds()))
 | 
						|
			require.NoError(t, app.Commit())
 | 
						|
 | 
						|
			// OOO few samples for s1.
 | 
						|
			app = db.Appender(context.Background())
 | 
						|
			for _, s := range tc.inputSamples {
 | 
						|
				appendSample(app, s1, s.T(), s.V())
 | 
						|
			}
 | 
						|
			require.NoError(t, app.Commit())
 | 
						|
 | 
						|
			// The Series method is the one that populates the chunk meta OOO
 | 
						|
			// markers like OOOLastRef. These are then used by the ChunkReader.
 | 
						|
			ir := NewOOOHeadIndexReader(db.head, tc.queryMinT, tc.queryMaxT)
 | 
						|
			var chks []chunks.Meta
 | 
						|
			var b labels.ScratchBuilder
 | 
						|
			err := ir.Series(s1Ref, &b, &chks)
 | 
						|
			require.NoError(t, err)
 | 
						|
			require.Equal(t, len(tc.expChunksSamples), len(chks))
 | 
						|
 | 
						|
			cr := NewOOOHeadChunkReader(db.head, tc.queryMinT, tc.queryMaxT)
 | 
						|
			for i := 0; i < len(chks); i++ {
 | 
						|
				c, err := cr.Chunk(chks[i])
 | 
						|
				require.NoError(t, err)
 | 
						|
 | 
						|
				var resultSamples tsdbutil.SampleSlice
 | 
						|
				it := c.Iterator(nil)
 | 
						|
				for it.Next() == chunkenc.ValFloat {
 | 
						|
					t, v := it.At()
 | 
						|
					resultSamples = append(resultSamples, sample{t: t, v: v})
 | 
						|
				}
 | 
						|
				require.Equal(t, tc.expChunksSamples[i], resultSamples)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding tests
 | 
						|
// that if a query comes and performs a Series() call followed by a Chunks() call
 | 
						|
// the response is consistent with the data seen by Series() even if the OOO
 | 
						|
// head receives more samples before Chunks() is called.
 | 
						|
// An example:
 | 
						|
// - Response A comes from: Series() then Chunk()
 | 
						|
// - Response B comes from : Series(), in parallel new samples added to the head, then Chunk()
 | 
						|
// - A == B
 | 
						|
func TestOOOHeadChunkReader_Chunk_ConsistentQueryResponseDespiteOfHeadExpanding(t *testing.T) {
 | 
						|
	opts := DefaultOptions()
 | 
						|
	opts.OutOfOrderCapMax = 5
 | 
						|
	opts.OutOfOrderTimeWindow = 120 * time.Minute.Milliseconds()
 | 
						|
 | 
						|
	s1 := labels.FromStrings("l", "v1")
 | 
						|
	minutes := func(m int64) int64 { return m * time.Minute.Milliseconds() }
 | 
						|
 | 
						|
	appendSample := func(app storage.Appender, l labels.Labels, timestamp int64, value float64) storage.SeriesRef {
 | 
						|
		ref, err := app.Append(0, l, timestamp, value)
 | 
						|
		require.NoError(t, err)
 | 
						|
		return ref
 | 
						|
	}
 | 
						|
 | 
						|
	tests := []struct {
 | 
						|
		name                   string
 | 
						|
		queryMinT              int64
 | 
						|
		queryMaxT              int64
 | 
						|
		firstInOrderSampleAt   int64
 | 
						|
		initialSamples         tsdbutil.SampleSlice
 | 
						|
		samplesAfterSeriesCall tsdbutil.SampleSlice
 | 
						|
		expChunkError          bool
 | 
						|
		expChunksSamples       []tsdbutil.SampleSlice
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name:                 "Current head gets old, new and in between sample after Series call, they all should be omitted from the result",
 | 
						|
			queryMinT:            minutes(0),
 | 
						|
			queryMaxT:            minutes(100),
 | 
						|
			firstInOrderSampleAt: minutes(120),
 | 
						|
			initialSamples: tsdbutil.SampleSlice{
 | 
						|
				// Chunk 0
 | 
						|
				sample{t: minutes(20), v: float64(0)},
 | 
						|
				sample{t: minutes(22), v: float64(0)},
 | 
						|
				sample{t: minutes(24), v: float64(0)},
 | 
						|
				sample{t: minutes(26), v: float64(0)},
 | 
						|
				sample{t: minutes(30), v: float64(0)},
 | 
						|
				// Chunk 1 Head
 | 
						|
				sample{t: minutes(25), v: float64(1)},
 | 
						|
				sample{t: minutes(35), v: float64(1)},
 | 
						|
			},
 | 
						|
			samplesAfterSeriesCall: tsdbutil.SampleSlice{
 | 
						|
				sample{t: minutes(10), v: float64(1)},
 | 
						|
				sample{t: minutes(32), v: float64(1)},
 | 
						|
				sample{t: minutes(50), v: float64(1)},
 | 
						|
			},
 | 
						|
			expChunkError: false,
 | 
						|
			// ts (in minutes)         0       10       20       30       40       50       60       70       80       90       100
 | 
						|
			// Query Interval          [-----------------------------------]
 | 
						|
			// Chunk 0:                                  [--------] (5 samples)
 | 
						|
			// Chunk 1: Current Head                          [-------] (2 samples)
 | 
						|
			// New samples added after Series()
 | 
						|
			// Chunk 1: Current Head            [-----------------------------------] (5 samples)
 | 
						|
			// Output Graphically                        [------------] (With 8 samples, samples newer than lastmint or older than lastmaxt are omitted but the ones in between are kept)
 | 
						|
			expChunksSamples: []tsdbutil.SampleSlice{
 | 
						|
				{
 | 
						|
					sample{t: minutes(20), v: float64(0)},
 | 
						|
					sample{t: minutes(22), v: float64(0)},
 | 
						|
					sample{t: minutes(24), v: float64(0)},
 | 
						|
					sample{t: minutes(25), v: float64(1)},
 | 
						|
					sample{t: minutes(26), v: float64(0)},
 | 
						|
					sample{t: minutes(30), v: float64(0)},
 | 
						|
					sample{t: minutes(32), v: float64(1)}, // This sample was added after Series() but before Chunk() and its in between the lastmint and maxt so it should be kept
 | 
						|
					sample{t: minutes(35), v: float64(1)},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name:                 "After Series() previous head gets mmapped after getting samples, new head gets new samples also overlapping, none of these should appear in the response.",
 | 
						|
			queryMinT:            minutes(0),
 | 
						|
			queryMaxT:            minutes(100),
 | 
						|
			firstInOrderSampleAt: minutes(120),
 | 
						|
			initialSamples: tsdbutil.SampleSlice{
 | 
						|
				// Chunk 0
 | 
						|
				sample{t: minutes(20), v: float64(0)},
 | 
						|
				sample{t: minutes(22), v: float64(0)},
 | 
						|
				sample{t: minutes(24), v: float64(0)},
 | 
						|
				sample{t: minutes(26), v: float64(0)},
 | 
						|
				sample{t: minutes(30), v: float64(0)},
 | 
						|
				// Chunk 1 Head
 | 
						|
				sample{t: minutes(25), v: float64(1)},
 | 
						|
				sample{t: minutes(35), v: float64(1)},
 | 
						|
			},
 | 
						|
			samplesAfterSeriesCall: tsdbutil.SampleSlice{
 | 
						|
				sample{t: minutes(10), v: float64(1)},
 | 
						|
				sample{t: minutes(32), v: float64(1)},
 | 
						|
				sample{t: minutes(50), v: float64(1)},
 | 
						|
				// Chunk 1 gets mmapped and Chunk 2, the new head is born
 | 
						|
				sample{t: minutes(25), v: float64(2)},
 | 
						|
				sample{t: minutes(31), v: float64(2)},
 | 
						|
			},
 | 
						|
			expChunkError: false,
 | 
						|
			// ts (in minutes)         0       10       20       30       40       50       60       70       80       90       100
 | 
						|
			// Query Interval          [-----------------------------------]
 | 
						|
			// Chunk 0:                                  [--------] (5 samples)
 | 
						|
			// Chunk 1: Current Head                          [-------] (2 samples)
 | 
						|
			// New samples added after Series()
 | 
						|
			// Chunk 1 (mmapped)                     [-------------------------] (5 samples)
 | 
						|
			// Chunk 2: Current Head                    [-----------] (2 samples)
 | 
						|
			// Output Graphically                        [------------]  (8 samples) It has 5 from Chunk 0 and 3 from Chunk 1
 | 
						|
			expChunksSamples: []tsdbutil.SampleSlice{
 | 
						|
				{
 | 
						|
					sample{t: minutes(20), v: float64(0)},
 | 
						|
					sample{t: minutes(22), v: float64(0)},
 | 
						|
					sample{t: minutes(24), v: float64(0)},
 | 
						|
					sample{t: minutes(25), v: float64(1)},
 | 
						|
					sample{t: minutes(26), v: float64(0)},
 | 
						|
					sample{t: minutes(30), v: float64(0)},
 | 
						|
					sample{t: minutes(32), v: float64(1)}, // This sample was added after Series() but before Chunk() and its in between the lastmint and maxt so it should be kept
 | 
						|
					sample{t: minutes(35), v: float64(1)},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range tests {
 | 
						|
		t.Run(fmt.Sprintf("name=%s", tc.name), func(t *testing.T) {
 | 
						|
			db := newTestDBWithOpts(t, opts)
 | 
						|
 | 
						|
			app := db.Appender(context.Background())
 | 
						|
			s1Ref := appendSample(app, s1, tc.firstInOrderSampleAt, float64(tc.firstInOrderSampleAt/1*time.Minute.Milliseconds()))
 | 
						|
			require.NoError(t, app.Commit())
 | 
						|
 | 
						|
			// OOO few samples for s1.
 | 
						|
			app = db.Appender(context.Background())
 | 
						|
			for _, s := range tc.initialSamples {
 | 
						|
				appendSample(app, s1, s.T(), s.V())
 | 
						|
			}
 | 
						|
			require.NoError(t, app.Commit())
 | 
						|
 | 
						|
			// The Series method is the one that populates the chunk meta OOO
 | 
						|
			// markers like OOOLastRef. These are then used by the ChunkReader.
 | 
						|
			ir := NewOOOHeadIndexReader(db.head, tc.queryMinT, tc.queryMaxT)
 | 
						|
			var chks []chunks.Meta
 | 
						|
			var b labels.ScratchBuilder
 | 
						|
			err := ir.Series(s1Ref, &b, &chks)
 | 
						|
			require.NoError(t, err)
 | 
						|
			require.Equal(t, len(tc.expChunksSamples), len(chks))
 | 
						|
 | 
						|
			// Now we keep receiving ooo samples
 | 
						|
			// OOO few samples for s1.
 | 
						|
			app = db.Appender(context.Background())
 | 
						|
			for _, s := range tc.samplesAfterSeriesCall {
 | 
						|
				appendSample(app, s1, s.T(), s.V())
 | 
						|
			}
 | 
						|
			require.NoError(t, app.Commit())
 | 
						|
 | 
						|
			cr := NewOOOHeadChunkReader(db.head, tc.queryMinT, tc.queryMaxT)
 | 
						|
			for i := 0; i < len(chks); i++ {
 | 
						|
				c, err := cr.Chunk(chks[i])
 | 
						|
				require.NoError(t, err)
 | 
						|
 | 
						|
				var resultSamples tsdbutil.SampleSlice
 | 
						|
				it := c.Iterator(nil)
 | 
						|
				for it.Next() == chunkenc.ValFloat {
 | 
						|
					ts, v := it.At()
 | 
						|
					resultSamples = append(resultSamples, sample{t: ts, v: v})
 | 
						|
				}
 | 
						|
				require.Equal(t, tc.expChunksSamples[i], resultSamples)
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// TestSortByMinTimeAndMinRef tests that the sort function for chunk metas does sort
 | 
						|
// by chunk meta MinTime and in case of same references by the lower reference.
 | 
						|
func TestSortByMinTimeAndMinRef(t *testing.T) {
 | 
						|
	tests := []struct {
 | 
						|
		name  string
 | 
						|
		input []chunkMetaAndChunkDiskMapperRef
 | 
						|
		exp   []chunkMetaAndChunkDiskMapperRef
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name: "chunks are ordered by min time",
 | 
						|
			input: []chunkMetaAndChunkDiskMapperRef{
 | 
						|
				{
 | 
						|
					meta: chunks.Meta{
 | 
						|
						Ref:     0,
 | 
						|
						MinTime: 0,
 | 
						|
					},
 | 
						|
					ref: chunks.ChunkDiskMapperRef(0),
 | 
						|
				},
 | 
						|
				{
 | 
						|
					meta: chunks.Meta{
 | 
						|
						Ref:     1,
 | 
						|
						MinTime: 1,
 | 
						|
					},
 | 
						|
					ref: chunks.ChunkDiskMapperRef(1),
 | 
						|
				},
 | 
						|
			},
 | 
						|
			exp: []chunkMetaAndChunkDiskMapperRef{
 | 
						|
				{
 | 
						|
					meta: chunks.Meta{
 | 
						|
						Ref:     0,
 | 
						|
						MinTime: 0,
 | 
						|
					},
 | 
						|
					ref: chunks.ChunkDiskMapperRef(0),
 | 
						|
				},
 | 
						|
				{
 | 
						|
					meta: chunks.Meta{
 | 
						|
						Ref:     1,
 | 
						|
						MinTime: 1,
 | 
						|
					},
 | 
						|
					ref: chunks.ChunkDiskMapperRef(1),
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "if same mintime, lower reference goes first",
 | 
						|
			input: []chunkMetaAndChunkDiskMapperRef{
 | 
						|
				{
 | 
						|
					meta: chunks.Meta{
 | 
						|
						Ref:     10,
 | 
						|
						MinTime: 0,
 | 
						|
					},
 | 
						|
					ref: chunks.ChunkDiskMapperRef(0),
 | 
						|
				},
 | 
						|
				{
 | 
						|
					meta: chunks.Meta{
 | 
						|
						Ref:     5,
 | 
						|
						MinTime: 0,
 | 
						|
					},
 | 
						|
					ref: chunks.ChunkDiskMapperRef(1),
 | 
						|
				},
 | 
						|
			},
 | 
						|
			exp: []chunkMetaAndChunkDiskMapperRef{
 | 
						|
				{
 | 
						|
					meta: chunks.Meta{
 | 
						|
						Ref:     5,
 | 
						|
						MinTime: 0,
 | 
						|
					},
 | 
						|
					ref: chunks.ChunkDiskMapperRef(1),
 | 
						|
				},
 | 
						|
				{
 | 
						|
					meta: chunks.Meta{
 | 
						|
						Ref:     10,
 | 
						|
						MinTime: 0,
 | 
						|
					},
 | 
						|
					ref: chunks.ChunkDiskMapperRef(0),
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range tests {
 | 
						|
		t.Run(fmt.Sprintf("name=%s", tc.name), func(t *testing.T) {
 | 
						|
			sort.Sort(byMinTimeAndMinRef(tc.input))
 | 
						|
			require.Equal(t, tc.exp, tc.input)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// TestSortMetaByMinTimeAndMinRef tests that the sort function for chunk metas does sort
 | 
						|
// by chunk meta MinTime and in case of same references by the lower reference.
 | 
						|
func TestSortMetaByMinTimeAndMinRef(t *testing.T) {
 | 
						|
	tests := []struct {
 | 
						|
		name       string
 | 
						|
		inputMetas []chunks.Meta
 | 
						|
		expMetas   []chunks.Meta
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name: "chunks are ordered by min time",
 | 
						|
			inputMetas: []chunks.Meta{
 | 
						|
				{
 | 
						|
					Ref:     0,
 | 
						|
					MinTime: 0,
 | 
						|
				},
 | 
						|
				{
 | 
						|
					Ref:     1,
 | 
						|
					MinTime: 1,
 | 
						|
				},
 | 
						|
			},
 | 
						|
			expMetas: []chunks.Meta{
 | 
						|
				{
 | 
						|
					Ref:     0,
 | 
						|
					MinTime: 0,
 | 
						|
				},
 | 
						|
				{
 | 
						|
					Ref:     1,
 | 
						|
					MinTime: 1,
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "if same mintime, lower reference goes first",
 | 
						|
			inputMetas: []chunks.Meta{
 | 
						|
				{
 | 
						|
					Ref:     10,
 | 
						|
					MinTime: 0,
 | 
						|
				},
 | 
						|
				{
 | 
						|
					Ref:     5,
 | 
						|
					MinTime: 0,
 | 
						|
				},
 | 
						|
			},
 | 
						|
			expMetas: []chunks.Meta{
 | 
						|
				{
 | 
						|
					Ref:     5,
 | 
						|
					MinTime: 0,
 | 
						|
				},
 | 
						|
				{
 | 
						|
					Ref:     10,
 | 
						|
					MinTime: 0,
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tc := range tests {
 | 
						|
		t.Run(fmt.Sprintf("name=%s", tc.name), func(t *testing.T) {
 | 
						|
			sort.Sort(metaByMinTimeAndMinRef(tc.inputMetas))
 | 
						|
			require.Equal(t, tc.expMetas, tc.inputMetas)
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func newTestDBWithOpts(t *testing.T, opts *Options) *DB {
 | 
						|
	dir := t.TempDir()
 | 
						|
 | 
						|
	db, err := Open(dir, nil, nil, opts, nil)
 | 
						|
	require.NoError(t, err)
 | 
						|
 | 
						|
	t.Cleanup(func() {
 | 
						|
		require.NoError(t, db.Close())
 | 
						|
	})
 | 
						|
 | 
						|
	return db
 | 
						|
}
 |