mirror of https://github.com/prometheus/prometheus
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2154 lines
65 KiB
2154 lines
65 KiB
// Copyright 2017 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" |
|
"errors" |
|
"fmt" |
|
"math" |
|
"math/rand" |
|
"os" |
|
"path" |
|
"path/filepath" |
|
"runtime" |
|
"strconv" |
|
"sync" |
|
"testing" |
|
"time" |
|
|
|
"github.com/go-kit/log" |
|
"github.com/oklog/ulid" |
|
prom_testutil "github.com/prometheus/client_golang/prometheus/testutil" |
|
"github.com/stretchr/testify/require" |
|
|
|
"github.com/prometheus/prometheus/model/histogram" |
|
"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/fileutil" |
|
"github.com/prometheus/prometheus/tsdb/index" |
|
"github.com/prometheus/prometheus/tsdb/tombstones" |
|
"github.com/prometheus/prometheus/tsdb/tsdbutil" |
|
"github.com/prometheus/prometheus/tsdb/wlog" |
|
) |
|
|
|
func TestSplitByRange(t *testing.T) { |
|
cases := []struct { |
|
trange int64 |
|
ranges [][2]int64 |
|
output [][][2]int64 |
|
}{ |
|
{ |
|
trange: 60, |
|
ranges: [][2]int64{{0, 10}}, |
|
output: [][][2]int64{ |
|
{{0, 10}}, |
|
}, |
|
}, |
|
{ |
|
trange: 60, |
|
ranges: [][2]int64{{0, 60}}, |
|
output: [][][2]int64{ |
|
{{0, 60}}, |
|
}, |
|
}, |
|
{ |
|
trange: 60, |
|
ranges: [][2]int64{{0, 10}, {9, 15}, {30, 60}}, |
|
output: [][][2]int64{ |
|
{{0, 10}, {9, 15}, {30, 60}}, |
|
}, |
|
}, |
|
{ |
|
trange: 60, |
|
ranges: [][2]int64{{70, 90}, {125, 130}, {130, 180}, {1000, 1001}}, |
|
output: [][][2]int64{ |
|
{{70, 90}}, |
|
{{125, 130}, {130, 180}}, |
|
{{1000, 1001}}, |
|
}, |
|
}, |
|
// Mis-aligned or too-large blocks are ignored. |
|
{ |
|
trange: 60, |
|
ranges: [][2]int64{{50, 70}, {70, 80}}, |
|
output: [][][2]int64{ |
|
{{70, 80}}, |
|
}, |
|
}, |
|
{ |
|
trange: 72, |
|
ranges: [][2]int64{{0, 144}, {144, 216}, {216, 288}}, |
|
output: [][][2]int64{ |
|
{{144, 216}}, |
|
{{216, 288}}, |
|
}, |
|
}, |
|
// Various awkward edge cases easy to hit with negative numbers. |
|
{ |
|
trange: 60, |
|
ranges: [][2]int64{{-10, -5}}, |
|
output: [][][2]int64{ |
|
{{-10, -5}}, |
|
}, |
|
}, |
|
{ |
|
trange: 60, |
|
ranges: [][2]int64{{-60, -50}, {-10, -5}}, |
|
output: [][][2]int64{ |
|
{{-60, -50}, {-10, -5}}, |
|
}, |
|
}, |
|
{ |
|
trange: 60, |
|
ranges: [][2]int64{{-60, -50}, {-10, -5}, {0, 15}}, |
|
output: [][][2]int64{ |
|
{{-60, -50}, {-10, -5}}, |
|
{{0, 15}}, |
|
}, |
|
}, |
|
} |
|
|
|
for _, c := range cases { |
|
// Transform input range tuples into dirMetas. |
|
blocks := make([]dirMeta, 0, len(c.ranges)) |
|
for _, r := range c.ranges { |
|
blocks = append(blocks, dirMeta{ |
|
meta: &BlockMeta{ |
|
MinTime: r[0], |
|
MaxTime: r[1], |
|
}, |
|
}) |
|
} |
|
|
|
// Transform output range tuples into dirMetas. |
|
exp := make([][]dirMeta, len(c.output)) |
|
for i, group := range c.output { |
|
for _, r := range group { |
|
exp[i] = append(exp[i], dirMeta{ |
|
meta: &BlockMeta{MinTime: r[0], MaxTime: r[1]}, |
|
}) |
|
} |
|
} |
|
|
|
require.Equal(t, exp, splitByRange(blocks, c.trange)) |
|
} |
|
} |
|
|
|
// See https://github.com/prometheus/prometheus/issues/3064 |
|
func TestNoPanicFor0Tombstones(t *testing.T) { |
|
metas := []dirMeta{ |
|
{ |
|
dir: "1", |
|
meta: &BlockMeta{ |
|
MinTime: 0, |
|
MaxTime: 100, |
|
}, |
|
}, |
|
{ |
|
dir: "2", |
|
meta: &BlockMeta{ |
|
MinTime: 101, |
|
MaxTime: 200, |
|
}, |
|
}, |
|
} |
|
|
|
c, err := NewLeveledCompactor(context.Background(), nil, nil, []int64{50}, nil, nil) |
|
require.NoError(t, err) |
|
|
|
c.plan(metas) |
|
} |
|
|
|
func TestLeveledCompactor_plan(t *testing.T) { |
|
// This mimics our default ExponentialBlockRanges with min block size equals to 20. |
|
compactor, err := NewLeveledCompactor(context.Background(), nil, nil, []int64{ |
|
20, |
|
60, |
|
180, |
|
540, |
|
1620, |
|
}, nil, nil) |
|
require.NoError(t, err) |
|
|
|
cases := map[string]struct { |
|
metas []dirMeta |
|
expected []string |
|
}{ |
|
"Outside Range": { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 20, nil), |
|
}, |
|
expected: nil, |
|
}, |
|
"We should wait for four blocks of size 20 to appear before compacting.": { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 20, nil), |
|
metaRange("2", 20, 40, nil), |
|
}, |
|
expected: nil, |
|
}, |
|
`We should wait for a next block of size 20 to appear before compacting |
|
the existing ones. We have three, but we ignore the fresh one from WAl`: { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 20, nil), |
|
metaRange("2", 20, 40, nil), |
|
metaRange("3", 40, 60, nil), |
|
}, |
|
expected: nil, |
|
}, |
|
"Block to fill the entire parent range appeared – should be compacted": { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 20, nil), |
|
metaRange("2", 20, 40, nil), |
|
metaRange("3", 40, 60, nil), |
|
metaRange("4", 60, 80, nil), |
|
}, |
|
expected: []string{"1", "2", "3"}, |
|
}, |
|
`Block for the next parent range appeared with gap with size 20. Nothing will happen in the first one |
|
anymore but we ignore fresh one still, so no compaction`: { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 20, nil), |
|
metaRange("2", 20, 40, nil), |
|
metaRange("3", 60, 80, nil), |
|
}, |
|
expected: nil, |
|
}, |
|
`Block for the next parent range appeared, and we have a gap with size 20 between second and third block. |
|
We will not get this missed gap anymore and we should compact just these two.`: { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 20, nil), |
|
metaRange("2", 20, 40, nil), |
|
metaRange("3", 60, 80, nil), |
|
metaRange("4", 80, 100, nil), |
|
}, |
|
expected: []string{"1", "2"}, |
|
}, |
|
"We have 20, 20, 20, 60, 60 range blocks. '5' is marked as fresh one": { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 20, nil), |
|
metaRange("2", 20, 40, nil), |
|
metaRange("3", 40, 60, nil), |
|
metaRange("4", 60, 120, nil), |
|
metaRange("5", 120, 180, nil), |
|
}, |
|
expected: []string{"1", "2", "3"}, |
|
}, |
|
"We have 20, 60, 20, 60, 240 range blocks. We can compact 20 + 60 + 60": { |
|
metas: []dirMeta{ |
|
metaRange("2", 20, 40, nil), |
|
metaRange("4", 60, 120, nil), |
|
metaRange("5", 960, 980, nil), // Fresh one. |
|
metaRange("6", 120, 180, nil), |
|
metaRange("7", 720, 960, nil), |
|
}, |
|
expected: []string{"2", "4", "6"}, |
|
}, |
|
"Do not select large blocks that have many tombstones when there is no fresh block": { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 540, &BlockStats{ |
|
NumSeries: 10, |
|
NumTombstones: 3, |
|
}), |
|
}, |
|
expected: nil, |
|
}, |
|
"Select large blocks that have many tombstones when fresh appears": { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 540, &BlockStats{ |
|
NumSeries: 10, |
|
NumTombstones: 3, |
|
}), |
|
metaRange("2", 540, 560, nil), |
|
}, |
|
expected: []string{"1"}, |
|
}, |
|
"For small blocks, do not compact tombstones, even when fresh appears.": { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 60, &BlockStats{ |
|
NumSeries: 10, |
|
NumTombstones: 3, |
|
}), |
|
metaRange("2", 60, 80, nil), |
|
}, |
|
expected: nil, |
|
}, |
|
`Regression test: we were stuck in a compact loop where we always recompacted |
|
the same block when tombstones and series counts were zero`: { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 540, &BlockStats{ |
|
NumSeries: 0, |
|
NumTombstones: 0, |
|
}), |
|
metaRange("2", 540, 560, nil), |
|
}, |
|
expected: nil, |
|
}, |
|
`Regression test: we were wrongly assuming that new block is fresh from WAL when its ULID is newest. |
|
We need to actually look on max time instead. |
|
|
|
With previous, wrong approach "8" block was ignored, so we were wrongly compacting 5 and 7 and introducing |
|
block overlaps`: { |
|
metas: []dirMeta{ |
|
metaRange("5", 0, 360, nil), |
|
metaRange("6", 540, 560, nil), // Fresh one. |
|
metaRange("7", 360, 420, nil), |
|
metaRange("8", 420, 540, nil), |
|
}, |
|
expected: []string{"7", "8"}, |
|
}, |
|
// |--------------| |
|
// |----------------| |
|
// |--------------| |
|
"Overlapping blocks 1": { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 20, nil), |
|
metaRange("2", 19, 40, nil), |
|
metaRange("3", 40, 60, nil), |
|
}, |
|
expected: []string{"1", "2"}, |
|
}, |
|
// |--------------| |
|
// |--------------| |
|
// |--------------| |
|
"Overlapping blocks 2": { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 20, nil), |
|
metaRange("2", 20, 40, nil), |
|
metaRange("3", 30, 50, nil), |
|
}, |
|
expected: []string{"2", "3"}, |
|
}, |
|
// |--------------| |
|
// |---------------------| |
|
// |--------------| |
|
"Overlapping blocks 3": { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 20, nil), |
|
metaRange("2", 10, 40, nil), |
|
metaRange("3", 30, 50, nil), |
|
}, |
|
expected: []string{"1", "2", "3"}, |
|
}, |
|
// |--------------| |
|
// |--------------------------------| |
|
// |--------------| |
|
// |--------------| |
|
"Overlapping blocks 4": { |
|
metas: []dirMeta{ |
|
metaRange("5", 0, 360, nil), |
|
metaRange("6", 340, 560, nil), |
|
metaRange("7", 360, 420, nil), |
|
metaRange("8", 420, 540, nil), |
|
}, |
|
expected: []string{"5", "6", "7", "8"}, |
|
}, |
|
// |--------------| |
|
// |--------------| |
|
// |--------------| |
|
// |--------------| |
|
"Overlapping blocks 5": { |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 10, nil), |
|
metaRange("2", 9, 20, nil), |
|
metaRange("3", 30, 40, nil), |
|
metaRange("4", 39, 50, nil), |
|
}, |
|
expected: []string{"1", "2"}, |
|
}, |
|
} |
|
|
|
for title, c := range cases { |
|
if !t.Run(title, func(t *testing.T) { |
|
res, err := compactor.plan(c.metas) |
|
require.NoError(t, err) |
|
require.Equal(t, c.expected, res) |
|
}) { |
|
return |
|
} |
|
} |
|
} |
|
|
|
func TestRangeWithFailedCompactionWontGetSelected(t *testing.T) { |
|
compactor, err := NewLeveledCompactor(context.Background(), nil, nil, []int64{ |
|
20, |
|
60, |
|
240, |
|
720, |
|
2160, |
|
}, nil, nil) |
|
require.NoError(t, err) |
|
|
|
cases := []struct { |
|
metas []dirMeta |
|
}{ |
|
{ |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 20, nil), |
|
metaRange("2", 20, 40, nil), |
|
metaRange("3", 40, 60, nil), |
|
metaRange("4", 60, 80, nil), |
|
}, |
|
}, |
|
{ |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 20, nil), |
|
metaRange("2", 20, 40, nil), |
|
metaRange("3", 60, 80, nil), |
|
metaRange("4", 80, 100, nil), |
|
}, |
|
}, |
|
{ |
|
metas: []dirMeta{ |
|
metaRange("1", 0, 20, nil), |
|
metaRange("2", 20, 40, nil), |
|
metaRange("3", 40, 60, nil), |
|
metaRange("4", 60, 120, nil), |
|
metaRange("5", 120, 180, nil), |
|
metaRange("6", 180, 200, nil), |
|
}, |
|
}, |
|
} |
|
|
|
for _, c := range cases { |
|
c.metas[1].meta.Compaction.Failed = true |
|
res, err := compactor.plan(c.metas) |
|
require.NoError(t, err) |
|
|
|
require.Equal(t, []string(nil), res) |
|
} |
|
} |
|
|
|
func TestCompactionFailWillCleanUpTempDir(t *testing.T) { |
|
compactor, err := NewLeveledCompactor(context.Background(), nil, log.NewNopLogger(), []int64{ |
|
20, |
|
60, |
|
240, |
|
720, |
|
2160, |
|
}, nil, nil) |
|
require.NoError(t, err) |
|
|
|
tmpdir := t.TempDir() |
|
|
|
require.Error(t, compactor.write(tmpdir, &BlockMeta{}, DefaultBlockPopulator{}, erringBReader{})) |
|
_, err = os.Stat(filepath.Join(tmpdir, BlockMeta{}.ULID.String()) + tmpForCreationBlockDirSuffix) |
|
require.True(t, os.IsNotExist(err), "directory is not cleaned up") |
|
} |
|
|
|
func metaRange(name string, mint, maxt int64, stats *BlockStats) dirMeta { |
|
meta := &BlockMeta{MinTime: mint, MaxTime: maxt} |
|
if stats != nil { |
|
meta.Stats = *stats |
|
} |
|
return dirMeta{ |
|
dir: name, |
|
meta: meta, |
|
} |
|
} |
|
|
|
type erringBReader struct{} |
|
|
|
func (erringBReader) Index() (IndexReader, error) { return nil, errors.New("index") } |
|
func (erringBReader) Chunks() (ChunkReader, error) { return nil, errors.New("chunks") } |
|
func (erringBReader) Tombstones() (tombstones.Reader, error) { return nil, errors.New("tombstones") } |
|
func (erringBReader) Meta() BlockMeta { return BlockMeta{} } |
|
func (erringBReader) Size() int64 { return 0 } |
|
|
|
type nopChunkWriter struct{} |
|
|
|
func (nopChunkWriter) WriteChunks(...chunks.Meta) error { return nil } |
|
func (nopChunkWriter) Close() error { return nil } |
|
|
|
func samplesForRange(minTime, maxTime int64, maxSamplesPerChunk int) (ret [][]sample) { |
|
var curr []sample |
|
for i := minTime; i <= maxTime; i++ { |
|
curr = append(curr, sample{t: i}) |
|
if len(curr) >= maxSamplesPerChunk { |
|
ret = append(ret, curr) |
|
curr = []sample{} |
|
} |
|
} |
|
if len(curr) > 0 { |
|
ret = append(ret, curr) |
|
} |
|
return ret |
|
} |
|
|
|
func TestCompaction_populateBlock(t *testing.T) { |
|
for _, tc := range []struct { |
|
title string |
|
inputSeriesSamples [][]seriesSamples |
|
compactMinTime int64 |
|
compactMaxTime int64 // When not defined the test runner sets a default of math.MaxInt64. |
|
irPostingsFunc IndexReaderPostingsFunc |
|
expSeriesSamples []seriesSamples |
|
expErr error |
|
}{ |
|
{ |
|
title: "Populate block from empty input should return error.", |
|
inputSeriesSamples: [][]seriesSamples{}, |
|
expErr: errors.New("cannot populate block from no readers"), |
|
}, |
|
{ |
|
// Populate from single block without chunks. We expect these kind of series being ignored. |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{{lset: map[string]string{"a": "b"}}}, |
|
}, |
|
}, |
|
{ |
|
title: "Populate from single block. We expect the same samples at the output.", |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}}, {{t: 11}, {t: 20}}}, |
|
}, |
|
}, |
|
}, |
|
expSeriesSamples: []seriesSamples{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}}, {{t: 11}, {t: 20}}}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
title: "Populate from two blocks.", |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}}, {{t: 11}, {t: 20}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "c"}, |
|
chunks: [][]sample{{{t: 1}, {t: 9}}, {{t: 10}, {t: 19}}}, |
|
}, |
|
{ |
|
// no-chunk series should be dropped. |
|
lset: map[string]string{"a": "empty"}, |
|
}, |
|
}, |
|
{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 21}, {t: 30}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "c"}, |
|
chunks: [][]sample{{{t: 40}, {t: 45}}}, |
|
}, |
|
}, |
|
}, |
|
expSeriesSamples: []seriesSamples{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}}, {{t: 11}, {t: 20}}, {{t: 21}, {t: 30}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "c"}, |
|
chunks: [][]sample{{{t: 1}, {t: 9}}, {{t: 10}, {t: 19}}, {{t: 40}, {t: 45}}}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
title: "Populate from two blocks; chunks with negative time.", |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}}, {{t: 11}, {t: 20}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "c"}, |
|
chunks: [][]sample{{{t: -11}, {t: -9}}, {{t: 10}, {t: 19}}}, |
|
}, |
|
{ |
|
// no-chunk series should be dropped. |
|
lset: map[string]string{"a": "empty"}, |
|
}, |
|
}, |
|
{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 21}, {t: 30}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "c"}, |
|
chunks: [][]sample{{{t: 40}, {t: 45}}}, |
|
}, |
|
}, |
|
}, |
|
compactMinTime: -11, |
|
expSeriesSamples: []seriesSamples{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}}, {{t: 11}, {t: 20}}, {{t: 21}, {t: 30}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "c"}, |
|
chunks: [][]sample{{{t: -11}, {t: -9}}, {{t: 10}, {t: 19}}, {{t: 40}, {t: 45}}}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
title: "Populate from two blocks showing that order is maintained.", |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}}, {{t: 11}, {t: 20}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "c"}, |
|
chunks: [][]sample{{{t: 1}, {t: 9}}, {{t: 10}, {t: 19}}}, |
|
}, |
|
}, |
|
{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 21}, {t: 30}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "c"}, |
|
chunks: [][]sample{{{t: 40}, {t: 45}}}, |
|
}, |
|
}, |
|
}, |
|
expSeriesSamples: []seriesSamples{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}}, {{t: 11}, {t: 20}}, {{t: 21}, {t: 30}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "c"}, |
|
chunks: [][]sample{{{t: 1}, {t: 9}}, {{t: 10}, {t: 19}}, {{t: 40}, {t: 45}}}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
title: "Populate from two blocks showing that order of series is sorted.", |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "4"}, |
|
chunks: [][]sample{{{t: 5}, {t: 7}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "3"}, |
|
chunks: [][]sample{{{t: 5}, {t: 6}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "same"}, |
|
chunks: [][]sample{{{t: 1}, {t: 4}}}, |
|
}, |
|
}, |
|
{ |
|
{ |
|
lset: map[string]string{"a": "2"}, |
|
chunks: [][]sample{{{t: 1}, {t: 3}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "1"}, |
|
chunks: [][]sample{{{t: 1}, {t: 2}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "same"}, |
|
chunks: [][]sample{{{t: 5}, {t: 8}}}, |
|
}, |
|
}, |
|
}, |
|
expSeriesSamples: []seriesSamples{ |
|
{ |
|
lset: map[string]string{"a": "1"}, |
|
chunks: [][]sample{{{t: 1}, {t: 2}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "2"}, |
|
chunks: [][]sample{{{t: 1}, {t: 3}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "3"}, |
|
chunks: [][]sample{{{t: 5}, {t: 6}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "4"}, |
|
chunks: [][]sample{{{t: 5}, {t: 7}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "same"}, |
|
chunks: [][]sample{{{t: 1}, {t: 4}}, {{t: 5}, {t: 8}}}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
title: "Populate from two blocks 1:1 duplicated chunks; with negative timestamps.", |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "1"}, |
|
chunks: [][]sample{{{t: 1}, {t: 2}}, {{t: 3}, {t: 4}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "2"}, |
|
chunks: [][]sample{{{t: -3}, {t: -2}}, {{t: 1}, {t: 3}, {t: 4}}, {{t: 5}, {t: 6}}}, |
|
}, |
|
}, |
|
{ |
|
{ |
|
lset: map[string]string{"a": "1"}, |
|
chunks: [][]sample{{{t: 3}, {t: 4}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "2"}, |
|
chunks: [][]sample{{{t: 1}, {t: 3}, {t: 4}}, {{t: 7}, {t: 8}}}, |
|
}, |
|
}, |
|
}, |
|
compactMinTime: -3, |
|
expSeriesSamples: []seriesSamples{ |
|
{ |
|
lset: map[string]string{"a": "1"}, |
|
chunks: [][]sample{{{t: 1}, {t: 2}}, {{t: 3}, {t: 4}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "2"}, |
|
chunks: [][]sample{{{t: -3}, {t: -2}}, {{t: 1}, {t: 3}, {t: 4}}, {{t: 5}, {t: 6}}, {{t: 7}, {t: 8}}}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
// This should not happened because head block is making sure the chunks are not crossing block boundaries. |
|
// We used to return error, but now chunk is trimmed. |
|
title: "Populate from single block containing chunk outside of compact meta time range.", |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 1}, {t: 2}}, {{t: 10}, {t: 30}}}, |
|
}, |
|
}, |
|
}, |
|
compactMinTime: 0, |
|
compactMaxTime: 20, |
|
expSeriesSamples: []seriesSamples{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 1}, {t: 2}}, {{t: 10}}}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
// Introduced by https://github.com/prometheus/tsdb/issues/347. We used to return error, but now chunk is trimmed. |
|
title: "Populate from single block containing extra chunk", |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "issue347"}, |
|
chunks: [][]sample{{{t: 1}, {t: 2}}, {{t: 10}, {t: 20}}}, |
|
}, |
|
}, |
|
}, |
|
compactMinTime: 0, |
|
compactMaxTime: 10, |
|
expSeriesSamples: []seriesSamples{ |
|
{ |
|
lset: map[string]string{"a": "issue347"}, |
|
chunks: [][]sample{{{t: 1}, {t: 2}}}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
// Deduplication expected. |
|
// Introduced by pull/370 and pull/539. |
|
title: "Populate from two blocks containing duplicated chunk.", |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 1}, {t: 2}}, {{t: 10}, {t: 20}}}, |
|
}, |
|
}, |
|
{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 10}, {t: 20}}}, |
|
}, |
|
}, |
|
}, |
|
expSeriesSamples: []seriesSamples{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 1}, {t: 2}}, {{t: 10}, {t: 20}}}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
// Introduced by https://github.com/prometheus/tsdb/pull/539. |
|
title: "Populate from three overlapping blocks.", |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "overlap-all"}, |
|
chunks: [][]sample{{{t: 19}, {t: 30}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "overlap-beginning"}, |
|
chunks: [][]sample{{{t: 0}, {t: 5}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "overlap-ending"}, |
|
chunks: [][]sample{{{t: 21}, {t: 30}}}, |
|
}, |
|
}, |
|
{ |
|
{ |
|
lset: map[string]string{"a": "overlap-all"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}, {t: 11}, {t: 20}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "overlap-beginning"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}, {t: 12}, {t: 20}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "overlap-ending"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}, {t: 13}, {t: 20}}}, |
|
}, |
|
}, |
|
{ |
|
{ |
|
lset: map[string]string{"a": "overlap-all"}, |
|
chunks: [][]sample{{{t: 27}, {t: 35}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "overlap-ending"}, |
|
chunks: [][]sample{{{t: 27}, {t: 35}}}, |
|
}, |
|
}, |
|
}, |
|
expSeriesSamples: []seriesSamples{ |
|
{ |
|
lset: map[string]string{"a": "overlap-all"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}, {t: 11}, {t: 19}, {t: 20}, {t: 27}, {t: 30}, {t: 35}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "overlap-beginning"}, |
|
chunks: [][]sample{{{t: 0}, {t: 5}, {t: 10}, {t: 12}, {t: 20}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "overlap-ending"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}, {t: 13}, {t: 20}}, {{t: 21}, {t: 27}, {t: 30}, {t: 35}}}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
title: "Populate from three partially overlapping blocks with few full chunks.", |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "1", "b": "1"}, |
|
chunks: samplesForRange(0, 659, 120), // 5 chunks and half. |
|
}, |
|
{ |
|
lset: map[string]string{"a": "1", "b": "2"}, |
|
chunks: samplesForRange(0, 659, 120), |
|
}, |
|
}, |
|
{ |
|
{ |
|
lset: map[string]string{"a": "1", "b": "2"}, |
|
chunks: samplesForRange(480, 1199, 120), // two chunks overlapping with previous, two non overlapping and two overlapping with next block. |
|
}, |
|
{ |
|
lset: map[string]string{"a": "1", "b": "3"}, |
|
chunks: samplesForRange(480, 1199, 120), |
|
}, |
|
}, |
|
{ |
|
{ |
|
lset: map[string]string{"a": "1", "b": "2"}, |
|
chunks: samplesForRange(960, 1499, 120), // 5 chunks and half. |
|
}, |
|
{ |
|
lset: map[string]string{"a": "1", "b": "4"}, |
|
chunks: samplesForRange(960, 1499, 120), |
|
}, |
|
}, |
|
}, |
|
expSeriesSamples: []seriesSamples{ |
|
{ |
|
lset: map[string]string{"a": "1", "b": "1"}, |
|
chunks: samplesForRange(0, 659, 120), |
|
}, |
|
{ |
|
lset: map[string]string{"a": "1", "b": "2"}, |
|
chunks: samplesForRange(0, 1499, 120), |
|
}, |
|
{ |
|
lset: map[string]string{"a": "1", "b": "3"}, |
|
chunks: samplesForRange(480, 1199, 120), |
|
}, |
|
{ |
|
lset: map[string]string{"a": "1", "b": "4"}, |
|
chunks: samplesForRange(960, 1499, 120), |
|
}, |
|
}, |
|
}, |
|
{ |
|
title: "Populate from three partially overlapping blocks with chunks that are expected to merge into single big chunks.", |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "1", "b": "2"}, |
|
chunks: [][]sample{{{t: 0}, {t: 6902464}}, {{t: 6961968}, {t: 7080976}}}, |
|
}, |
|
}, |
|
{ |
|
{ |
|
lset: map[string]string{"a": "1", "b": "2"}, |
|
chunks: [][]sample{{{t: 3600000}, {t: 13953696}}, {{t: 14042952}, {t: 14221464}}}, |
|
}, |
|
}, |
|
{ |
|
{ |
|
lset: map[string]string{"a": "1", "b": "2"}, |
|
chunks: [][]sample{{{t: 10800000}, {t: 14251232}}, {{t: 14280984}, {t: 14340488}}}, |
|
}, |
|
}, |
|
}, |
|
expSeriesSamples: []seriesSamples{ |
|
{ |
|
lset: map[string]string{"a": "1", "b": "2"}, |
|
chunks: [][]sample{{{t: 0}, {t: 3600000}, {t: 6902464}, {t: 6961968}, {t: 7080976}, {t: 10800000}, {t: 13953696}, {t: 14042952}, {t: 14221464}, {t: 14251232}}, {{t: 14280984}, {t: 14340488}}}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
// Regression test for populateWithDelChunkSeriesIterator failing to set minTime on chunks. |
|
title: "Populate from mixed type series and expect sample inside the interval only.", |
|
compactMinTime: 1, |
|
compactMaxTime: 11, |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "1"}, |
|
chunks: [][]sample{ |
|
{{t: 0, h: tsdbutil.GenerateTestHistogram(0)}, {t: 1, h: tsdbutil.GenerateTestHistogram(1)}}, |
|
{{t: 10, f: 1}, {t: 11, f: 2}}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
expSeriesSamples: []seriesSamples{ |
|
{ |
|
lset: map[string]string{"a": "1"}, |
|
chunks: [][]sample{ |
|
{{t: 1, h: tsdbutil.GenerateTestHistogram(1)}}, |
|
{{t: 10, f: 1}}, |
|
}, |
|
}, |
|
}, |
|
}, |
|
{ |
|
title: "Populate from single block with index reader postings function selecting different series. Expect empty block.", |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}}, {{t: 11}, {t: 20}}}, |
|
}, |
|
}, |
|
}, |
|
irPostingsFunc: func(ctx context.Context, reader IndexReader) index.Postings { |
|
p, err := reader.Postings(ctx, "a", "c") |
|
if err != nil { |
|
return index.EmptyPostings() |
|
} |
|
return reader.SortedPostings(p) |
|
}, |
|
}, |
|
{ |
|
title: "Populate from single block with index reader postings function selecting one series. Expect partial block.", |
|
inputSeriesSamples: [][]seriesSamples{ |
|
{ |
|
{ |
|
lset: map[string]string{"a": "b"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}}, {{t: 11}, {t: 20}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "c"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}}, {{t: 11}, {t: 20}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "d"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}}, {{t: 11}, {t: 20}}}, |
|
}, |
|
}, |
|
}, |
|
irPostingsFunc: func(ctx context.Context, reader IndexReader) index.Postings { |
|
p, err := reader.Postings(ctx, "a", "c", "d") |
|
if err != nil { |
|
return index.EmptyPostings() |
|
} |
|
return reader.SortedPostings(p) |
|
}, |
|
expSeriesSamples: []seriesSamples{ |
|
{ |
|
lset: map[string]string{"a": "c"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}}, {{t: 11}, {t: 20}}}, |
|
}, |
|
{ |
|
lset: map[string]string{"a": "d"}, |
|
chunks: [][]sample{{{t: 0}, {t: 10}}, {{t: 11}, {t: 20}}}, |
|
}, |
|
}, |
|
}, |
|
} { |
|
t.Run(tc.title, func(t *testing.T) { |
|
blocks := make([]BlockReader, 0, len(tc.inputSeriesSamples)) |
|
for _, b := range tc.inputSeriesSamples { |
|
ir, cr, mint, maxt := createIdxChkReaders(t, b) |
|
blocks = append(blocks, &mockBReader{ir: ir, cr: cr, mint: mint, maxt: maxt}) |
|
} |
|
|
|
c, err := NewLeveledCompactor(context.Background(), nil, nil, []int64{0}, nil, nil) |
|
require.NoError(t, err) |
|
|
|
meta := &BlockMeta{ |
|
MinTime: tc.compactMinTime, |
|
MaxTime: tc.compactMaxTime, |
|
} |
|
if meta.MaxTime == 0 { |
|
meta.MaxTime = math.MaxInt64 |
|
} |
|
|
|
iw := &mockIndexWriter{} |
|
blockPopulator := DefaultBlockPopulator{} |
|
irPostingsFunc := AllSortedPostings |
|
if tc.irPostingsFunc != nil { |
|
irPostingsFunc = tc.irPostingsFunc |
|
} |
|
err = blockPopulator.PopulateBlock(c.ctx, c.metrics, c.logger, c.chunkPool, c.mergeFunc, blocks, meta, iw, nopChunkWriter{}, irPostingsFunc) |
|
if tc.expErr != nil { |
|
require.Error(t, err) |
|
require.Equal(t, tc.expErr.Error(), err.Error()) |
|
return |
|
} |
|
require.NoError(t, err) |
|
|
|
// Check if response is expected and chunk is valid. |
|
var raw []seriesSamples |
|
for _, s := range iw.seriesChunks { |
|
ss := seriesSamples{lset: s.l.Map()} |
|
var iter chunkenc.Iterator |
|
for _, chk := range s.chunks { |
|
var ( |
|
samples = make([]sample, 0, chk.Chunk.NumSamples()) |
|
iter = chk.Chunk.Iterator(iter) |
|
firstTs int64 = math.MaxInt64 |
|
s sample |
|
) |
|
for vt := iter.Next(); vt != chunkenc.ValNone; vt = iter.Next() { |
|
switch vt { |
|
case chunkenc.ValFloat: |
|
s.t, s.f = iter.At() |
|
samples = append(samples, s) |
|
case chunkenc.ValHistogram: |
|
s.t, s.h = iter.AtHistogram(nil) |
|
samples = append(samples, s) |
|
case chunkenc.ValFloatHistogram: |
|
s.t, s.fh = iter.AtFloatHistogram(nil) |
|
samples = append(samples, s) |
|
default: |
|
require.Fail(t, "unexpected value type") |
|
} |
|
if firstTs == math.MaxInt64 { |
|
firstTs = s.t |
|
} |
|
} |
|
|
|
// Check if chunk has correct min, max times. |
|
require.Equal(t, firstTs, chk.MinTime, "chunk Meta %v does not match the first encoded sample timestamp: %v", chk, firstTs) |
|
require.Equal(t, s.t, chk.MaxTime, "chunk Meta %v does not match the last encoded sample timestamp %v", chk, s.t) |
|
|
|
require.NoError(t, iter.Err()) |
|
ss.chunks = append(ss.chunks, samples) |
|
} |
|
raw = append(raw, ss) |
|
} |
|
require.Equal(t, tc.expSeriesSamples, raw) |
|
|
|
// Check if stats are calculated properly. |
|
s := BlockStats{NumSeries: uint64(len(tc.expSeriesSamples))} |
|
for _, series := range tc.expSeriesSamples { |
|
s.NumChunks += uint64(len(series.chunks)) |
|
for _, chk := range series.chunks { |
|
s.NumSamples += uint64(len(chk)) |
|
} |
|
} |
|
require.Equal(t, s, meta.Stats) |
|
}) |
|
} |
|
} |
|
|
|
func BenchmarkCompaction(b *testing.B) { |
|
cases := []struct { |
|
ranges [][2]int64 |
|
compactionType string |
|
}{ |
|
{ |
|
ranges: [][2]int64{{0, 100}, {200, 300}, {400, 500}, {600, 700}}, |
|
compactionType: "normal", |
|
}, |
|
{ |
|
ranges: [][2]int64{{0, 1000}, {2000, 3000}, {4000, 5000}, {6000, 7000}}, |
|
compactionType: "normal", |
|
}, |
|
{ |
|
ranges: [][2]int64{{0, 2000}, {3000, 5000}, {6000, 8000}, {9000, 11000}}, |
|
compactionType: "normal", |
|
}, |
|
{ |
|
ranges: [][2]int64{{0, 5000}, {6000, 11000}, {12000, 17000}, {18000, 23000}}, |
|
compactionType: "normal", |
|
}, |
|
// 40% overlaps. |
|
{ |
|
ranges: [][2]int64{{0, 100}, {60, 160}, {120, 220}, {180, 280}}, |
|
compactionType: "vertical", |
|
}, |
|
{ |
|
ranges: [][2]int64{{0, 1000}, {600, 1600}, {1200, 2200}, {1800, 2800}}, |
|
compactionType: "vertical", |
|
}, |
|
{ |
|
ranges: [][2]int64{{0, 2000}, {1200, 3200}, {2400, 4400}, {3600, 5600}}, |
|
compactionType: "vertical", |
|
}, |
|
{ |
|
ranges: [][2]int64{{0, 5000}, {3000, 8000}, {6000, 11000}, {9000, 14000}}, |
|
compactionType: "vertical", |
|
}, |
|
} |
|
|
|
nSeries := 10000 |
|
for _, c := range cases { |
|
nBlocks := len(c.ranges) |
|
b.Run(fmt.Sprintf("type=%s,blocks=%d,series=%d,samplesPerSeriesPerBlock=%d", c.compactionType, nBlocks, nSeries, c.ranges[0][1]-c.ranges[0][0]+1), func(b *testing.B) { |
|
dir := b.TempDir() |
|
blockDirs := make([]string, 0, len(c.ranges)) |
|
var blocks []*Block |
|
for _, r := range c.ranges { |
|
block, err := OpenBlock(nil, createBlock(b, dir, genSeries(nSeries, 10, r[0], r[1])), nil) |
|
require.NoError(b, err) |
|
blocks = append(blocks, block) |
|
defer func() { |
|
require.NoError(b, block.Close()) |
|
}() |
|
blockDirs = append(blockDirs, block.Dir()) |
|
} |
|
|
|
c, err := NewLeveledCompactor(context.Background(), nil, log.NewNopLogger(), []int64{0}, nil, nil) |
|
require.NoError(b, err) |
|
|
|
b.ResetTimer() |
|
b.ReportAllocs() |
|
for i := 0; i < b.N; i++ { |
|
_, err = c.Compact(dir, blockDirs, blocks) |
|
require.NoError(b, err) |
|
} |
|
}) |
|
} |
|
} |
|
|
|
func BenchmarkCompactionFromHead(b *testing.B) { |
|
dir := b.TempDir() |
|
totalSeries := 100000 |
|
for labelNames := 1; labelNames < totalSeries; labelNames *= 10 { |
|
labelValues := totalSeries / labelNames |
|
b.Run(fmt.Sprintf("labelnames=%d,labelvalues=%d", labelNames, labelValues), func(b *testing.B) { |
|
chunkDir := b.TempDir() |
|
opts := DefaultHeadOptions() |
|
opts.ChunkRange = 1000 |
|
opts.ChunkDirRoot = chunkDir |
|
h, err := NewHead(nil, nil, nil, nil, opts, nil) |
|
require.NoError(b, err) |
|
for ln := 0; ln < labelNames; ln++ { |
|
app := h.Appender(context.Background()) |
|
for lv := 0; lv < labelValues; lv++ { |
|
app.Append(0, labels.FromStrings(strconv.Itoa(ln), fmt.Sprintf("%d%s%d", lv, postingsBenchSuffix, ln)), 0, 0) |
|
} |
|
require.NoError(b, app.Commit()) |
|
} |
|
|
|
b.ResetTimer() |
|
b.ReportAllocs() |
|
for i := 0; i < b.N; i++ { |
|
createBlockFromHead(b, filepath.Join(dir, fmt.Sprintf("%d-%d", i, labelNames)), h) |
|
} |
|
h.Close() |
|
}) |
|
} |
|
} |
|
|
|
func BenchmarkCompactionFromOOOHead(b *testing.B) { |
|
dir := b.TempDir() |
|
totalSeries := 100000 |
|
totalSamples := 100 |
|
for labelNames := 1; labelNames < totalSeries; labelNames *= 10 { |
|
labelValues := totalSeries / labelNames |
|
b.Run(fmt.Sprintf("labelnames=%d,labelvalues=%d", labelNames, labelValues), func(b *testing.B) { |
|
chunkDir := b.TempDir() |
|
opts := DefaultHeadOptions() |
|
opts.ChunkRange = 1000 |
|
opts.ChunkDirRoot = chunkDir |
|
opts.OutOfOrderTimeWindow.Store(int64(totalSamples)) |
|
h, err := NewHead(nil, nil, nil, nil, opts, nil) |
|
require.NoError(b, err) |
|
for ln := 0; ln < labelNames; ln++ { |
|
app := h.Appender(context.Background()) |
|
for lv := 0; lv < labelValues; lv++ { |
|
lbls := labels.FromStrings(strconv.Itoa(ln), fmt.Sprintf("%d%s%d", lv, postingsBenchSuffix, ln)) |
|
_, err = app.Append(0, lbls, int64(totalSamples), 0) |
|
require.NoError(b, err) |
|
for ts := 0; ts < totalSamples; ts++ { |
|
_, err = app.Append(0, lbls, int64(ts), float64(ts)) |
|
require.NoError(b, err) |
|
} |
|
} |
|
require.NoError(b, app.Commit()) |
|
} |
|
|
|
b.ResetTimer() |
|
b.ReportAllocs() |
|
for i := 0; i < b.N; i++ { |
|
oooHead, err := NewOOOCompactionHead(context.TODO(), h) |
|
require.NoError(b, err) |
|
createBlockFromOOOHead(b, filepath.Join(dir, fmt.Sprintf("%d-%d", i, labelNames)), oooHead) |
|
} |
|
h.Close() |
|
}) |
|
} |
|
} |
|
|
|
// TestDisableAutoCompactions checks that we can |
|
// disable and enable the auto compaction. |
|
// This is needed for unit tests that rely on |
|
// checking state before and after a compaction. |
|
func TestDisableAutoCompactions(t *testing.T) { |
|
db := openTestDB(t, nil, nil) |
|
defer func() { |
|
require.NoError(t, db.Close()) |
|
}() |
|
|
|
blockRange := db.compactor.(*LeveledCompactor).ranges[0] |
|
label := labels.FromStrings("foo", "bar") |
|
|
|
// Trigger a compaction to check that it was skipped and |
|
// no new blocks were created when compaction is disabled. |
|
db.DisableCompactions() |
|
app := db.Appender(context.Background()) |
|
for i := int64(0); i < 3; i++ { |
|
_, err := app.Append(0, label, i*blockRange, 0) |
|
require.NoError(t, err) |
|
_, err = app.Append(0, label, i*blockRange+1000, 0) |
|
require.NoError(t, err) |
|
} |
|
require.NoError(t, app.Commit()) |
|
|
|
select { |
|
case db.compactc <- struct{}{}: |
|
default: |
|
} |
|
|
|
for x := 0; x < 10; x++ { |
|
if prom_testutil.ToFloat64(db.metrics.compactionsSkipped) > 0.0 { |
|
break |
|
} |
|
time.Sleep(10 * time.Millisecond) |
|
} |
|
|
|
require.Greater(t, prom_testutil.ToFloat64(db.metrics.compactionsSkipped), 0.0, "No compaction was skipped after the set timeout.") |
|
require.Empty(t, db.blocks) |
|
|
|
// Enable the compaction, trigger it and check that the block is persisted. |
|
db.EnableCompactions() |
|
select { |
|
case db.compactc <- struct{}{}: |
|
default: |
|
} |
|
for x := 0; x < 100; x++ { |
|
if len(db.Blocks()) > 0 { |
|
break |
|
} |
|
time.Sleep(100 * time.Millisecond) |
|
} |
|
require.NotEmpty(t, db.Blocks(), "No block was persisted after the set timeout.") |
|
} |
|
|
|
// TestCancelCompactions ensures that when the db is closed |
|
// any running compaction is cancelled to unblock closing the db. |
|
func TestCancelCompactions(t *testing.T) { |
|
tmpdir := t.TempDir() |
|
|
|
// Create some blocks to fall within the compaction range. |
|
createBlock(t, tmpdir, genSeries(1, 10000, 0, 1000)) |
|
createBlock(t, tmpdir, genSeries(1, 10000, 1000, 2000)) |
|
createBlock(t, tmpdir, genSeries(1, 1, 2000, 2001)) // The most recent block is ignored so can be e small one. |
|
|
|
// Copy the db so we have an exact copy to compare compaction times. |
|
tmpdirCopy := t.TempDir() |
|
err := fileutil.CopyDirs(tmpdir, tmpdirCopy) |
|
require.NoError(t, err) |
|
|
|
// Measure the compaction time without interrupting it. |
|
var timeCompactionUninterrupted time.Duration |
|
{ |
|
db, err := open(tmpdir, log.NewNopLogger(), nil, DefaultOptions(), []int64{1, 2000}, nil) |
|
require.NoError(t, err) |
|
require.Len(t, db.Blocks(), 3, "initial block count mismatch") |
|
require.Equal(t, 0.0, prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.Ran), "initial compaction counter mismatch") |
|
db.compactc <- struct{}{} // Trigger a compaction. |
|
for prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.PopulatingBlocks) <= 0 { |
|
time.Sleep(3 * time.Millisecond) |
|
} |
|
|
|
start := time.Now() |
|
for prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.Ran) != 1 { |
|
time.Sleep(3 * time.Millisecond) |
|
} |
|
timeCompactionUninterrupted = time.Since(start) |
|
|
|
require.NoError(t, db.Close()) |
|
} |
|
// Measure the compaction time when closing the db in the middle of compaction. |
|
{ |
|
db, err := open(tmpdirCopy, log.NewNopLogger(), nil, DefaultOptions(), []int64{1, 2000}, nil) |
|
require.NoError(t, err) |
|
require.Len(t, db.Blocks(), 3, "initial block count mismatch") |
|
require.Equal(t, 0.0, prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.Ran), "initial compaction counter mismatch") |
|
db.compactc <- struct{}{} // Trigger a compaction. |
|
|
|
for prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.PopulatingBlocks) <= 0 { |
|
time.Sleep(3 * time.Millisecond) |
|
} |
|
|
|
start := time.Now() |
|
require.NoError(t, db.Close()) |
|
actT := time.Since(start) |
|
|
|
expT := timeCompactionUninterrupted / 2 // Closing the db in the middle of compaction should less than half the time. |
|
require.Less(t, actT, expT, "closing the db took more than expected. exp: <%v, act: %v", expT, actT) |
|
|
|
// Make sure that no blocks were marked as compaction failed. |
|
// This checks that the `context.Canceled` error is properly checked at all levels: |
|
// - tsdb_errors.NewMulti() should have the Is() method implemented for correct checks. |
|
// - callers should check with errors.Is() instead of ==. |
|
readOnlyDB, err := OpenDBReadOnly(tmpdirCopy, "", log.NewNopLogger()) |
|
require.NoError(t, err) |
|
blocks, err := readOnlyDB.Blocks() |
|
require.NoError(t, err) |
|
for i, b := range blocks { |
|
require.Falsef(t, b.Meta().Compaction.Failed, "block %d (%s) should not be marked as compaction failed", i, b.Meta().ULID) |
|
} |
|
require.NoError(t, readOnlyDB.Close()) |
|
} |
|
} |
|
|
|
// TestDeleteCompactionBlockAfterFailedReload ensures that a failed reloadBlocks immediately after a compaction |
|
// deletes the resulting block to avoid creatings blocks with the same time range. |
|
func TestDeleteCompactionBlockAfterFailedReload(t *testing.T) { |
|
tests := map[string]func(*DB) int{ |
|
"Test Head Compaction": func(db *DB) int { |
|
rangeToTriggerCompaction := db.compactor.(*LeveledCompactor).ranges[0]/2*3 - 1 |
|
defaultLabel := labels.FromStrings("foo", "bar") |
|
|
|
// Add some data to the head that is enough to trigger a compaction. |
|
app := db.Appender(context.Background()) |
|
_, err := app.Append(0, defaultLabel, 1, 0) |
|
require.NoError(t, err) |
|
_, err = app.Append(0, defaultLabel, 2, 0) |
|
require.NoError(t, err) |
|
_, err = app.Append(0, defaultLabel, 3+rangeToTriggerCompaction, 0) |
|
require.NoError(t, err) |
|
require.NoError(t, app.Commit()) |
|
|
|
return 0 |
|
}, |
|
"Test Block Compaction": func(db *DB) int { |
|
blocks := []*BlockMeta{ |
|
{MinTime: 0, MaxTime: 100}, |
|
{MinTime: 100, MaxTime: 150}, |
|
{MinTime: 150, MaxTime: 200}, |
|
} |
|
for _, m := range blocks { |
|
createBlock(t, db.Dir(), genSeries(1, 1, m.MinTime, m.MaxTime)) |
|
} |
|
require.NoError(t, db.reload()) |
|
require.Equal(t, len(blocks), len(db.Blocks()), "unexpected block count after a reloadBlocks") |
|
|
|
return len(blocks) |
|
}, |
|
} |
|
|
|
for title, bootStrap := range tests { |
|
t.Run(title, func(t *testing.T) { |
|
ctx := context.Background() |
|
|
|
db := openTestDB(t, nil, []int64{1, 100}) |
|
defer func() { |
|
require.NoError(t, db.Close()) |
|
}() |
|
db.DisableCompactions() |
|
|
|
expBlocks := bootStrap(db) |
|
|
|
// Create a block that will trigger the reloadBlocks to fail. |
|
blockPath := createBlock(t, db.Dir(), genSeries(1, 1, 200, 300)) |
|
lastBlockIndex := path.Join(blockPath, indexFilename) |
|
actBlocks, err := blockDirs(db.Dir()) |
|
require.NoError(t, err) |
|
require.Equal(t, expBlocks, len(actBlocks)-1) // -1 to exclude the corrupted block. |
|
require.NoError(t, os.RemoveAll(lastBlockIndex)) // Corrupt the block by removing the index file. |
|
|
|
require.Equal(t, 0.0, prom_testutil.ToFloat64(db.metrics.reloadsFailed), "initial 'failed db reloadBlocks' count metrics mismatch") |
|
require.Equal(t, 0.0, prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.Ran), "initial `compactions` count metric mismatch") |
|
require.Equal(t, 0.0, prom_testutil.ToFloat64(db.metrics.compactionsFailed), "initial `compactions failed` count metric mismatch") |
|
|
|
// Do the compaction and check the metrics. |
|
// Compaction should succeed, but the reloadBlocks should fail and |
|
// the new block created from the compaction should be deleted. |
|
require.Error(t, db.Compact(ctx)) |
|
require.Equal(t, 1.0, prom_testutil.ToFloat64(db.metrics.reloadsFailed), "'failed db reloadBlocks' count metrics mismatch") |
|
require.Equal(t, 1.0, prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.Ran), "`compaction` count metric mismatch") |
|
require.Equal(t, 1.0, prom_testutil.ToFloat64(db.metrics.compactionsFailed), "`compactions failed` count metric mismatch") |
|
|
|
actBlocks, err = blockDirs(db.Dir()) |
|
require.NoError(t, err) |
|
require.Equal(t, expBlocks, len(actBlocks)-1, "block count should be the same as before the compaction") // -1 to exclude the corrupted block. |
|
}) |
|
} |
|
} |
|
|
|
func TestHeadCompactionWithHistograms(t *testing.T) { |
|
for _, floatTest := range []bool{true, false} { |
|
t.Run(fmt.Sprintf("float=%t", floatTest), func(t *testing.T) { |
|
head, _ := newTestHead(t, DefaultBlockDuration, wlog.CompressionNone, false) |
|
require.NoError(t, head.Init(0)) |
|
t.Cleanup(func() { |
|
require.NoError(t, head.Close()) |
|
}) |
|
|
|
minute := func(m int) int64 { return int64(m) * time.Minute.Milliseconds() } |
|
ctx := context.Background() |
|
appendHistogram := func( |
|
lbls labels.Labels, from, to int, h *histogram.Histogram, exp *[]chunks.Sample, |
|
) { |
|
t.Helper() |
|
app := head.Appender(ctx) |
|
for tsMinute := from; tsMinute <= to; tsMinute++ { |
|
var err error |
|
if floatTest { |
|
_, err = app.AppendHistogram(0, lbls, minute(tsMinute), nil, h.ToFloat(nil)) |
|
efh := h.ToFloat(nil) |
|
if tsMinute == from { |
|
efh.CounterResetHint = histogram.UnknownCounterReset |
|
} else { |
|
efh.CounterResetHint = histogram.NotCounterReset |
|
} |
|
*exp = append(*exp, sample{t: minute(tsMinute), fh: efh}) |
|
} else { |
|
_, err = app.AppendHistogram(0, lbls, minute(tsMinute), h, nil) |
|
eh := h.Copy() |
|
if tsMinute == from { |
|
eh.CounterResetHint = histogram.UnknownCounterReset |
|
} else { |
|
eh.CounterResetHint = histogram.NotCounterReset |
|
} |
|
*exp = append(*exp, sample{t: minute(tsMinute), h: eh}) |
|
} |
|
require.NoError(t, err) |
|
} |
|
require.NoError(t, app.Commit()) |
|
} |
|
appendFloat := func(lbls labels.Labels, from, to int, exp *[]chunks.Sample) { |
|
t.Helper() |
|
app := head.Appender(ctx) |
|
for tsMinute := from; tsMinute <= to; tsMinute++ { |
|
_, err := app.Append(0, lbls, minute(tsMinute), float64(tsMinute)) |
|
require.NoError(t, err) |
|
*exp = append(*exp, sample{t: minute(tsMinute), f: float64(tsMinute)}) |
|
} |
|
require.NoError(t, app.Commit()) |
|
} |
|
|
|
var ( |
|
series1 = labels.FromStrings("foo", "bar1") |
|
series2 = labels.FromStrings("foo", "bar2") |
|
series3 = labels.FromStrings("foo", "bar3") |
|
series4 = labels.FromStrings("foo", "bar4") |
|
exp1, exp2, exp3, exp4 []chunks.Sample |
|
) |
|
h := &histogram.Histogram{ |
|
Count: 15, |
|
ZeroCount: 4, |
|
ZeroThreshold: 0.001, |
|
Sum: 35.5, |
|
Schema: 1, |
|
PositiveSpans: []histogram.Span{ |
|
{Offset: 0, Length: 2}, |
|
{Offset: 2, Length: 2}, |
|
}, |
|
PositiveBuckets: []int64{1, 1, -1, 0}, |
|
NegativeSpans: []histogram.Span{ |
|
{Offset: 0, Length: 1}, |
|
{Offset: 1, Length: 2}, |
|
}, |
|
NegativeBuckets: []int64{1, 2, -1}, |
|
} |
|
|
|
// Series with only histograms. |
|
appendHistogram(series1, 100, 105, h, &exp1) |
|
|
|
// Series starting with float and then getting histograms. |
|
appendFloat(series2, 100, 102, &exp2) |
|
appendHistogram(series2, 103, 105, h.Copy(), &exp2) |
|
appendFloat(series2, 106, 107, &exp2) |
|
appendHistogram(series2, 108, 109, h.Copy(), &exp2) |
|
|
|
// Series starting with histogram and then getting float. |
|
appendHistogram(series3, 101, 103, h.Copy(), &exp3) |
|
appendFloat(series3, 104, 106, &exp3) |
|
appendHistogram(series3, 107, 108, h.Copy(), &exp3) |
|
appendFloat(series3, 109, 110, &exp3) |
|
|
|
// A float only series. |
|
appendFloat(series4, 100, 102, &exp4) |
|
|
|
// Compaction. |
|
mint := head.MinTime() |
|
maxt := head.MaxTime() + 1 // Block intervals are half-open: [b.MinTime, b.MaxTime). |
|
compactor, err := NewLeveledCompactor(context.Background(), nil, nil, []int64{DefaultBlockDuration}, chunkenc.NewPool(), nil) |
|
require.NoError(t, err) |
|
ids, err := compactor.Write(head.opts.ChunkDirRoot, head, mint, maxt, nil) |
|
require.NoError(t, err) |
|
require.Len(t, ids, 1) |
|
|
|
// Open the block and query it and check the histograms. |
|
block, err := OpenBlock(nil, path.Join(head.opts.ChunkDirRoot, ids[0].String()), nil) |
|
require.NoError(t, err) |
|
t.Cleanup(func() { |
|
require.NoError(t, block.Close()) |
|
}) |
|
|
|
q, err := NewBlockQuerier(block, block.MinTime(), block.MaxTime()) |
|
require.NoError(t, err) |
|
|
|
actHists := query(t, q, labels.MustNewMatcher(labels.MatchRegexp, "foo", "bar.*")) |
|
require.Equal(t, map[string][]chunks.Sample{ |
|
series1.String(): exp1, |
|
series2.String(): exp2, |
|
series3.String(): exp3, |
|
series4.String(): exp4, |
|
}, actHists) |
|
}) |
|
} |
|
} |
|
|
|
// Depending on numSeriesPerSchema, it can take few gigs of memory; |
|
// the test adds all samples to appender before committing instead of |
|
// buffering the writes to make it run faster. |
|
func TestSparseHistogramSpaceSavings(t *testing.T) { |
|
t.Skip() |
|
|
|
cases := []struct { |
|
numSeriesPerSchema int |
|
numBuckets int |
|
numSpans int |
|
gapBetweenSpans int |
|
}{ |
|
{1, 15, 1, 0}, |
|
{1, 50, 1, 0}, |
|
{1, 100, 1, 0}, |
|
{1, 15, 3, 5}, |
|
{1, 50, 3, 3}, |
|
{1, 100, 3, 2}, |
|
{100, 15, 1, 0}, |
|
{100, 50, 1, 0}, |
|
{100, 100, 1, 0}, |
|
{100, 15, 3, 5}, |
|
{100, 50, 3, 3}, |
|
{100, 100, 3, 2}, |
|
} |
|
|
|
type testSummary struct { |
|
oldBlockTotalSeries int |
|
oldBlockIndexSize int64 |
|
oldBlockChunksSize int64 |
|
oldBlockTotalSize int64 |
|
|
|
sparseBlockTotalSeries int |
|
sparseBlockIndexSize int64 |
|
sparseBlockChunksSize int64 |
|
sparseBlockTotalSize int64 |
|
|
|
numBuckets int |
|
numSpans int |
|
gapBetweenSpans int |
|
} |
|
|
|
var summaries []testSummary |
|
|
|
allSchemas := []int{-4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8} |
|
schemaDescription := []string{"minus_4", "minus_3", "minus_2", "minus_1", "0", "1", "2", "3", "4", "5", "6", "7", "8"} |
|
numHistograms := 120 * 4 // 15s scrape interval. |
|
timeStep := DefaultBlockDuration / int64(numHistograms) |
|
for _, c := range cases { |
|
t.Run( |
|
fmt.Sprintf("series=%d,span=%d,gap=%d,buckets=%d", |
|
len(allSchemas)*c.numSeriesPerSchema, |
|
c.numSpans, |
|
c.gapBetweenSpans, |
|
c.numBuckets, |
|
), |
|
func(t *testing.T) { |
|
oldHead, _ := newTestHead(t, DefaultBlockDuration, wlog.CompressionNone, false) |
|
t.Cleanup(func() { |
|
require.NoError(t, oldHead.Close()) |
|
}) |
|
sparseHead, _ := newTestHead(t, DefaultBlockDuration, wlog.CompressionNone, false) |
|
t.Cleanup(func() { |
|
require.NoError(t, sparseHead.Close()) |
|
}) |
|
|
|
var allSparseSeries []struct { |
|
baseLabels labels.Labels |
|
hists []*histogram.Histogram |
|
} |
|
|
|
for sid, schema := range allSchemas { |
|
for i := 0; i < c.numSeriesPerSchema; i++ { |
|
lbls := labels.FromStrings( |
|
"__name__", fmt.Sprintf("rpc_durations_%d_histogram_seconds", i), |
|
"instance", "localhost:8080", |
|
"job", fmt.Sprintf("sparse_histogram_schema_%s", schemaDescription[sid]), |
|
) |
|
allSparseSeries = append(allSparseSeries, struct { |
|
baseLabels labels.Labels |
|
hists []*histogram.Histogram |
|
}{baseLabels: lbls, hists: generateCustomHistograms(numHistograms, c.numBuckets, c.numSpans, c.gapBetweenSpans, schema)}) |
|
} |
|
} |
|
|
|
oldApp := oldHead.Appender(context.Background()) |
|
sparseApp := sparseHead.Appender(context.Background()) |
|
numOldSeriesPerHistogram := 0 |
|
|
|
var oldULIDs []ulid.ULID |
|
var sparseULIDs []ulid.ULID |
|
|
|
var wg sync.WaitGroup |
|
|
|
wg.Add(1) |
|
go func() { |
|
defer wg.Done() |
|
|
|
// Ingest sparse histograms. |
|
for _, ah := range allSparseSeries { |
|
var ( |
|
ref storage.SeriesRef |
|
err error |
|
) |
|
for i := 0; i < numHistograms; i++ { |
|
ts := int64(i) * timeStep |
|
ref, err = sparseApp.AppendHistogram(ref, ah.baseLabels, ts, ah.hists[i], nil) |
|
require.NoError(t, err) |
|
} |
|
} |
|
require.NoError(t, sparseApp.Commit()) |
|
|
|
// Sparse head compaction. |
|
mint := sparseHead.MinTime() |
|
maxt := sparseHead.MaxTime() + 1 // Block intervals are half-open: [b.MinTime, b.MaxTime). |
|
compactor, err := NewLeveledCompactor(context.Background(), nil, nil, []int64{DefaultBlockDuration}, chunkenc.NewPool(), nil) |
|
require.NoError(t, err) |
|
sparseULIDs, err = compactor.Write(sparseHead.opts.ChunkDirRoot, sparseHead, mint, maxt, nil) |
|
require.NoError(t, err) |
|
require.Len(t, sparseULIDs, 1) |
|
}() |
|
|
|
wg.Add(1) |
|
go func() { |
|
defer wg.Done() |
|
|
|
// Ingest histograms the old way. |
|
for _, ah := range allSparseSeries { |
|
refs := make([]storage.SeriesRef, c.numBuckets+((c.numSpans-1)*c.gapBetweenSpans)) |
|
for i := 0; i < numHistograms; i++ { |
|
ts := int64(i) * timeStep |
|
|
|
h := ah.hists[i] |
|
|
|
numOldSeriesPerHistogram = 0 |
|
it := h.CumulativeBucketIterator() |
|
itIdx := 0 |
|
var err error |
|
for it.Next() { |
|
numOldSeriesPerHistogram++ |
|
b := it.At() |
|
lbls := labels.NewBuilder(ah.baseLabels).Set("le", fmt.Sprintf("%.16f", b.Upper)).Labels() |
|
refs[itIdx], err = oldApp.Append(refs[itIdx], lbls, ts, float64(b.Count)) |
|
require.NoError(t, err) |
|
itIdx++ |
|
} |
|
baseName := ah.baseLabels.Get(labels.MetricName) |
|
// _count metric. |
|
countLbls := labels.NewBuilder(ah.baseLabels).Set(labels.MetricName, baseName+"_count").Labels() |
|
_, err = oldApp.Append(0, countLbls, ts, float64(h.Count)) |
|
require.NoError(t, err) |
|
numOldSeriesPerHistogram++ |
|
|
|
// _sum metric. |
|
sumLbls := labels.NewBuilder(ah.baseLabels).Set(labels.MetricName, baseName+"_sum").Labels() |
|
_, err = oldApp.Append(0, sumLbls, ts, h.Sum) |
|
require.NoError(t, err) |
|
numOldSeriesPerHistogram++ |
|
} |
|
} |
|
|
|
require.NoError(t, oldApp.Commit()) |
|
|
|
// Old head compaction. |
|
mint := oldHead.MinTime() |
|
maxt := oldHead.MaxTime() + 1 // Block intervals are half-open: [b.MinTime, b.MaxTime). |
|
compactor, err := NewLeveledCompactor(context.Background(), nil, nil, []int64{DefaultBlockDuration}, chunkenc.NewPool(), nil) |
|
require.NoError(t, err) |
|
oldULIDs, err = compactor.Write(oldHead.opts.ChunkDirRoot, oldHead, mint, maxt, nil) |
|
require.NoError(t, err) |
|
require.Len(t, oldULIDs, 1) |
|
}() |
|
|
|
wg.Wait() |
|
|
|
oldBlockDir := filepath.Join(oldHead.opts.ChunkDirRoot, oldULIDs[0].String()) |
|
sparseBlockDir := filepath.Join(sparseHead.opts.ChunkDirRoot, sparseULIDs[0].String()) |
|
|
|
oldSize, err := fileutil.DirSize(oldBlockDir) |
|
require.NoError(t, err) |
|
oldIndexSize, err := fileutil.DirSize(filepath.Join(oldBlockDir, "index")) |
|
require.NoError(t, err) |
|
oldChunksSize, err := fileutil.DirSize(filepath.Join(oldBlockDir, "chunks")) |
|
require.NoError(t, err) |
|
|
|
sparseSize, err := fileutil.DirSize(sparseBlockDir) |
|
require.NoError(t, err) |
|
sparseIndexSize, err := fileutil.DirSize(filepath.Join(sparseBlockDir, "index")) |
|
require.NoError(t, err) |
|
sparseChunksSize, err := fileutil.DirSize(filepath.Join(sparseBlockDir, "chunks")) |
|
require.NoError(t, err) |
|
|
|
summaries = append(summaries, testSummary{ |
|
oldBlockTotalSeries: len(allSchemas) * c.numSeriesPerSchema * numOldSeriesPerHistogram, |
|
oldBlockIndexSize: oldIndexSize, |
|
oldBlockChunksSize: oldChunksSize, |
|
oldBlockTotalSize: oldSize, |
|
sparseBlockTotalSeries: len(allSchemas) * c.numSeriesPerSchema, |
|
sparseBlockIndexSize: sparseIndexSize, |
|
sparseBlockChunksSize: sparseChunksSize, |
|
sparseBlockTotalSize: sparseSize, |
|
numBuckets: c.numBuckets, |
|
numSpans: c.numSpans, |
|
gapBetweenSpans: c.gapBetweenSpans, |
|
}) |
|
}) |
|
} |
|
|
|
for _, s := range summaries { |
|
fmt.Printf(` |
|
Meta: NumBuckets=%d, NumSpans=%d, GapBetweenSpans=%d |
|
Old Block: NumSeries=%d, IndexSize=%d, ChunksSize=%d, TotalSize=%d |
|
Sparse Block: NumSeries=%d, IndexSize=%d, ChunksSize=%d, TotalSize=%d |
|
Savings: Index=%.2f%%, Chunks=%.2f%%, Total=%.2f%% |
|
`, |
|
s.numBuckets, s.numSpans, s.gapBetweenSpans, |
|
s.oldBlockTotalSeries, s.oldBlockIndexSize, s.oldBlockChunksSize, s.oldBlockTotalSize, |
|
s.sparseBlockTotalSeries, s.sparseBlockIndexSize, s.sparseBlockChunksSize, s.sparseBlockTotalSize, |
|
100*(1-float64(s.sparseBlockIndexSize)/float64(s.oldBlockIndexSize)), |
|
100*(1-float64(s.sparseBlockChunksSize)/float64(s.oldBlockChunksSize)), |
|
100*(1-float64(s.sparseBlockTotalSize)/float64(s.oldBlockTotalSize)), |
|
) |
|
} |
|
} |
|
|
|
func generateCustomHistograms(numHists, numBuckets, numSpans, gapBetweenSpans, schema int) (r []*histogram.Histogram) { |
|
// First histogram with all the settings. |
|
h := &histogram.Histogram{ |
|
Sum: 1000 * rand.Float64(), |
|
Schema: int32(schema), |
|
} |
|
|
|
// Generate spans. |
|
h.PositiveSpans = []histogram.Span{ |
|
{Offset: int32(rand.Intn(10)), Length: uint32(numBuckets)}, |
|
} |
|
if numSpans > 1 { |
|
spanWidth := numBuckets / numSpans |
|
// First span gets those additional buckets. |
|
h.PositiveSpans[0].Length = uint32(spanWidth + (numBuckets - spanWidth*numSpans)) |
|
for i := 0; i < numSpans-1; i++ { |
|
h.PositiveSpans = append(h.PositiveSpans, histogram.Span{Offset: int32(rand.Intn(gapBetweenSpans) + 1), Length: uint32(spanWidth)}) |
|
} |
|
} |
|
|
|
// Generate buckets. |
|
v := int64(rand.Intn(30) + 1) |
|
h.PositiveBuckets = []int64{v} |
|
count := v |
|
firstHistValues := []int64{v} |
|
for i := 0; i < numBuckets-1; i++ { |
|
delta := int64(rand.Intn(20)) |
|
if rand.Int()%2 == 0 && firstHistValues[len(firstHistValues)-1] > delta { |
|
// Randomly making delta negative such that curr value will be >0. |
|
delta = -delta |
|
} |
|
|
|
currVal := firstHistValues[len(firstHistValues)-1] + delta |
|
count += currVal |
|
firstHistValues = append(firstHistValues, currVal) |
|
|
|
h.PositiveBuckets = append(h.PositiveBuckets, delta) |
|
} |
|
|
|
h.Count = uint64(count) |
|
|
|
r = append(r, h) |
|
|
|
// Remaining histograms with same spans but changed bucket values. |
|
for j := 0; j < numHists-1; j++ { |
|
newH := h.Copy() |
|
newH.Sum = float64(j+1) * 1000 * rand.Float64() |
|
|
|
// Generate buckets. |
|
count := int64(0) |
|
currVal := int64(0) |
|
for i := range newH.PositiveBuckets { |
|
delta := int64(rand.Intn(10)) |
|
if i == 0 { |
|
newH.PositiveBuckets[i] += delta |
|
currVal = newH.PositiveBuckets[i] |
|
continue |
|
} |
|
currVal += newH.PositiveBuckets[i] |
|
if rand.Int()%2 == 0 && (currVal-delta) > firstHistValues[i] { |
|
// Randomly making delta negative such that curr value will be >0 |
|
// and above the previous count since we are not doing resets here. |
|
delta = -delta |
|
} |
|
newH.PositiveBuckets[i] += delta |
|
currVal += delta |
|
count += currVal |
|
} |
|
|
|
newH.Count = uint64(count) |
|
|
|
r = append(r, newH) |
|
h = newH |
|
} |
|
|
|
return r |
|
} |
|
|
|
func TestCompactBlockMetas(t *testing.T) { |
|
parent1 := ulid.MustNew(100, nil) |
|
parent2 := ulid.MustNew(200, nil) |
|
parent3 := ulid.MustNew(300, nil) |
|
parent4 := ulid.MustNew(400, nil) |
|
|
|
input := []*BlockMeta{ |
|
{ULID: parent1, MinTime: 1000, MaxTime: 2000, Compaction: BlockMetaCompaction{Level: 2, Sources: []ulid.ULID{ulid.MustNew(1, nil), ulid.MustNew(10, nil)}}}, |
|
{ULID: parent2, MinTime: 200, MaxTime: 500, Compaction: BlockMetaCompaction{Level: 1}}, |
|
{ULID: parent3, MinTime: 500, MaxTime: 2500, Compaction: BlockMetaCompaction{Level: 3, Sources: []ulid.ULID{ulid.MustNew(5, nil), ulid.MustNew(6, nil)}}}, |
|
{ULID: parent4, MinTime: 100, MaxTime: 900, Compaction: BlockMetaCompaction{Level: 1}}, |
|
} |
|
|
|
outUlid := ulid.MustNew(1000, nil) |
|
output := CompactBlockMetas(outUlid, input...) |
|
|
|
expected := &BlockMeta{ |
|
ULID: outUlid, |
|
MinTime: 100, |
|
MaxTime: 2500, |
|
Stats: BlockStats{}, |
|
Compaction: BlockMetaCompaction{ |
|
Level: 4, |
|
Sources: []ulid.ULID{ulid.MustNew(1, nil), ulid.MustNew(5, nil), ulid.MustNew(6, nil), ulid.MustNew(10, nil)}, |
|
Parents: []BlockDesc{ |
|
{ULID: parent1, MinTime: 1000, MaxTime: 2000}, |
|
{ULID: parent2, MinTime: 200, MaxTime: 500}, |
|
{ULID: parent3, MinTime: 500, MaxTime: 2500}, |
|
{ULID: parent4, MinTime: 100, MaxTime: 900}, |
|
}, |
|
}, |
|
} |
|
require.Equal(t, expected, output) |
|
} |
|
|
|
func TestCompactEmptyResultBlockWithTombstone(t *testing.T) { |
|
ctx := context.Background() |
|
tmpdir := t.TempDir() |
|
blockDir := createBlock(t, tmpdir, genSeries(1, 1, 0, 10)) |
|
block, err := OpenBlock(nil, blockDir, nil) |
|
require.NoError(t, err) |
|
// Write tombstone covering the whole block. |
|
err = block.Delete(ctx, 0, 10, labels.MustNewMatcher(labels.MatchEqual, defaultLabelName, "0")) |
|
require.NoError(t, err) |
|
|
|
c, err := NewLeveledCompactor(ctx, nil, log.NewNopLogger(), []int64{0}, nil, nil) |
|
require.NoError(t, err) |
|
|
|
ulids, err := c.Compact(tmpdir, []string{blockDir}, []*Block{block}) |
|
require.NoError(t, err) |
|
require.Nil(t, ulids) |
|
require.NoError(t, block.Close()) |
|
} |
|
|
|
func TestDelayedCompaction(t *testing.T) { |
|
// The delay is chosen in such a way as to not slow down the tests, but also to make |
|
// the effective compaction duration negligible compared to it, so that the duration comparisons make sense. |
|
delay := 1000 * time.Millisecond |
|
|
|
waitUntilCompactedAndCheck := func(db *DB) { |
|
t.Helper() |
|
start := time.Now() |
|
for db.head.compactable() { |
|
// This simulates what happens at the end of commits, for less busy DB, a compaction |
|
// is triggered every minute. This is to speed up the test. |
|
select { |
|
case db.compactc <- struct{}{}: |
|
default: |
|
} |
|
time.Sleep(time.Millisecond) |
|
} |
|
duration := time.Since(start) |
|
// Only waited for one offset: offset<=delay<<<2*offset |
|
require.Greater(t, duration, db.opts.CompactionDelay) |
|
require.Less(t, duration, 2*db.opts.CompactionDelay) |
|
} |
|
|
|
compactAndCheck := func(db *DB) { |
|
t.Helper() |
|
start := time.Now() |
|
db.Compact(context.Background()) |
|
for db.head.compactable() { |
|
time.Sleep(time.Millisecond) |
|
} |
|
if runtime.GOOS == "windows" { |
|
// TODO: enable on windows once ms resolution timers are better supported. |
|
return |
|
} |
|
duration := time.Since(start) |
|
require.Less(t, duration, delay) |
|
} |
|
|
|
cases := []struct { |
|
name string |
|
// The delays are chosen in such a way as to not slow down the tests, but also in a way to make the |
|
// effective compaction duration negligible compared to them, so that the duration comparisons make sense. |
|
compactionDelay time.Duration |
|
}{ |
|
{ |
|
"delayed compaction not enabled", |
|
0, |
|
}, |
|
{ |
|
"delayed compaction enabled", |
|
delay, |
|
}, |
|
} |
|
|
|
for _, c := range cases { |
|
c := c |
|
t.Run(c.name, func(t *testing.T) { |
|
t.Parallel() |
|
|
|
var options *Options |
|
if c.compactionDelay > 0 { |
|
options = &Options{CompactionDelay: c.compactionDelay} |
|
} |
|
db := openTestDB(t, options, []int64{10}) |
|
defer func() { |
|
require.NoError(t, db.Close()) |
|
}() |
|
|
|
label := labels.FromStrings("foo", "bar") |
|
|
|
// The first compaction is expected to result in 1 block. |
|
db.DisableCompactions() |
|
app := db.Appender(context.Background()) |
|
_, err := app.Append(0, label, 0, 0) |
|
require.NoError(t, err) |
|
_, err = app.Append(0, label, 11, 0) |
|
require.NoError(t, err) |
|
_, err = app.Append(0, label, 21, 0) |
|
require.NoError(t, err) |
|
require.NoError(t, app.Commit()) |
|
|
|
if c.compactionDelay == 0 { |
|
// When delay is not enabled, compaction should run on the first trigger. |
|
compactAndCheck(db) |
|
} else { |
|
db.EnableCompactions() |
|
waitUntilCompactedAndCheck(db) |
|
// The db.compactc signals have been processed multiple times since a compaction is triggered every 1ms by waitUntilCompacted. |
|
// This implies that the compaction delay doesn't block or wait on the initial trigger. |
|
// 3 is an arbitrary value because it's difficult to determine the precise value. |
|
require.GreaterOrEqual(t, prom_testutil.ToFloat64(db.metrics.compactionsTriggered)-prom_testutil.ToFloat64(db.metrics.compactionsSkipped), 3.0) |
|
// The delay doesn't change the head blocks alignment. |
|
require.Eventually(t, func() bool { |
|
return db.head.MinTime() == db.compactor.(*LeveledCompactor).ranges[0]+1 |
|
}, 500*time.Millisecond, 10*time.Millisecond) |
|
// One compaction was run and one block was produced. |
|
require.Equal(t, 1.0, prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.Ran)) |
|
} |
|
|
|
// The second compaction is expected to result in 2 blocks. |
|
// This ensures that the logic for compaction delay doesn't only work for the first compaction, but also takes into account the future compactions. |
|
// This also ensures that no delay happens between consecutive compactions. |
|
db.DisableCompactions() |
|
app = db.Appender(context.Background()) |
|
_, err = app.Append(0, label, 31, 0) |
|
require.NoError(t, err) |
|
_, err = app.Append(0, label, 41, 0) |
|
require.NoError(t, err) |
|
require.NoError(t, app.Commit()) |
|
|
|
if c.compactionDelay == 0 { |
|
// Compaction should still run on the first trigger. |
|
compactAndCheck(db) |
|
} else { |
|
db.EnableCompactions() |
|
waitUntilCompactedAndCheck(db) |
|
} |
|
|
|
// Two other compactions were run. |
|
require.Eventually(t, func() bool { |
|
return prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.Ran) == 3.0 |
|
}, 500*time.Millisecond, 10*time.Millisecond) |
|
|
|
if c.compactionDelay == 0 { |
|
return |
|
} |
|
|
|
// This test covers a special case. If auto compaction is in a delay period and a manual compaction is triggered, |
|
// auto compaction should stop waiting for the delay if the head is no longer compactable. |
|
// Of course, if the head is still compactable after the manual compaction, auto compaction will continue waiting for the same delay. |
|
getTimeWhenCompactionDelayStarted := func() time.Time { |
|
t.Helper() |
|
db.cmtx.Lock() |
|
defer db.cmtx.Unlock() |
|
return db.timeWhenCompactionDelayStarted |
|
} |
|
|
|
db.DisableCompactions() |
|
app = db.Appender(context.Background()) |
|
_, err = app.Append(0, label, 51, 0) |
|
require.NoError(t, err) |
|
require.NoError(t, app.Commit()) |
|
|
|
require.True(t, db.head.compactable()) |
|
db.EnableCompactions() |
|
// Trigger an auto compaction. |
|
db.compactc <- struct{}{} |
|
// That made auto compaction start waiting for the delay. |
|
require.Eventually(t, func() bool { |
|
return !getTimeWhenCompactionDelayStarted().IsZero() |
|
}, 100*time.Millisecond, 10*time.Millisecond) |
|
// Trigger a manual compaction. |
|
require.NoError(t, db.CompactHead(NewRangeHead(db.Head(), 0, 50.0))) |
|
require.Equal(t, 4.0, prom_testutil.ToFloat64(db.compactor.(*LeveledCompactor).metrics.Ran)) |
|
// Re-trigger an auto compaction. |
|
db.compactc <- struct{}{} |
|
// That made auto compaction stop waiting for the delay. |
|
require.Eventually(t, func() bool { |
|
return getTimeWhenCompactionDelayStarted().IsZero() |
|
}, 100*time.Millisecond, 10*time.Millisecond) |
|
}) |
|
} |
|
} |
|
|
|
// TestDelayedCompactionDoesNotBlockUnrelatedOps makes sure that when delayed compaction is enabled, |
|
// operations that don't directly derive from the Head compaction are not delayed, here we consider disk blocks compaction. |
|
func TestDelayedCompactionDoesNotBlockUnrelatedOps(t *testing.T) { |
|
cases := []struct { |
|
name string |
|
whenCompactable bool |
|
}{ |
|
{ |
|
"Head is compactable", |
|
true, |
|
}, |
|
{ |
|
"Head is not compactable", |
|
false, |
|
}, |
|
} |
|
|
|
for _, c := range cases { |
|
c := c |
|
t.Run(c.name, func(t *testing.T) { |
|
t.Parallel() |
|
|
|
tmpdir := t.TempDir() |
|
// Some blocks that need compation are present. |
|
createBlock(t, tmpdir, genSeries(1, 1, 0, 100)) |
|
createBlock(t, tmpdir, genSeries(1, 1, 100, 200)) |
|
createBlock(t, tmpdir, genSeries(1, 1, 200, 300)) |
|
|
|
options := DefaultOptions() |
|
// This will make the test timeout if compaction really waits for it. |
|
options.CompactionDelay = time.Hour |
|
db, err := open(tmpdir, log.NewNopLogger(), nil, options, []int64{10, 200}, nil) |
|
require.NoError(t, err) |
|
defer func() { |
|
require.NoError(t, db.Close()) |
|
}() |
|
|
|
db.DisableCompactions() |
|
require.Len(t, db.Blocks(), 3) |
|
|
|
if c.whenCompactable { |
|
label := labels.FromStrings("foo", "bar") |
|
app := db.Appender(context.Background()) |
|
_, err := app.Append(0, label, 301, 0) |
|
require.NoError(t, err) |
|
_, err = app.Append(0, label, 317, 0) |
|
require.NoError(t, err) |
|
require.NoError(t, app.Commit()) |
|
// The Head is compactable and will still be at the end. |
|
require.True(t, db.head.compactable()) |
|
defer func() { |
|
require.True(t, db.head.compactable()) |
|
}() |
|
} |
|
|
|
// The blocks were compacted. |
|
db.Compact(context.Background()) |
|
require.Len(t, db.Blocks(), 2) |
|
}) |
|
} |
|
}
|
|
|