Files
ingot/internal/head/head_test.go
T
2026-07-04 18:51:08 -04:00

913 lines
25 KiB
Go

package head
import (
"math"
"os"
"path/filepath"
"reflect"
"sync"
"testing"
"git.dvdt.dev/david/ingot/internal/block"
"git.dvdt.dev/david/ingot/internal/wal"
"git.dvdt.dev/david/ingot/labels"
)
// sample is a convenience type for expected results.
type sample struct {
t int64
vBits uint64
}
func s(t int64, v float64) sample { return sample{t, math.Float64bits(v)} }
// collectSamples reads all samples from a head series iterator.
func collectSamples(t *testing.T, h *Head, ref uint64, mint, maxt int64) []sample {
t.Helper()
it := h.SeriesIterator(ref, mint, maxt)
var out []sample
for it.Next() {
ts, v := it.At()
out = append(out, sample{ts, math.Float64bits(v)})
}
if err := it.Err(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
return out
}
// Action kinds for the appender lifecycle table.
const (
actNew = iota // create a new Appender
actAppend // call Append
actCommit // call Commit
actRollback // call Rollback
)
type action struct {
kind int
// actAppend fields.
ref uint64
labels []labels.Label
t int64
v float64
// Expected results (actAppend: ref+err, actCommit/actRollback: err).
wantRef uint64
wantErr error
}
func openHead(t *testing.T) *Head {
t.Helper()
dir := filepath.Join(t.TempDir(), "wal")
h, err := Open(dir, wal.Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
t.Cleanup(func() { h.Close() })
return h
}
func TestHead(t *testing.T) {
tests := []struct {
name string
actions []action
wantSamples map[uint64][]sample // ref -> expected samples after all actions
}{
{
name: "single_series_single_sample",
actions: []action{
{kind: actNew},
{kind: actAppend, labels: []labels.Label{{Name: "__name__", Value: "temp"}}, t: 1000, v: 71.3, wantRef: 1, wantErr: nil},
{kind: actCommit, wantErr: nil},
},
wantSamples: map[uint64][]sample{
1: {s(1000, 71.3)},
},
},
{
name: "single_series_multiple_samples",
actions: []action{
{kind: actNew},
{kind: actAppend, labels: []labels.Label{{Name: "__name__", Value: "temp"}}, t: 1000, v: 1.0, wantRef: 1, wantErr: nil},
{kind: actAppend, ref: 1, t: 1015, v: 2.0, wantRef: 1, wantErr: nil},
{kind: actAppend, ref: 1, t: 1030, v: 3.0, wantRef: 1, wantErr: nil},
{kind: actCommit, wantErr: nil},
},
wantSamples: map[uint64][]sample{
1: {s(1000, 1.0), s(1015, 2.0), s(1030, 3.0)},
},
},
{
name: "multiple_series",
actions: []action{
{kind: actNew},
{kind: actAppend, labels: []labels.Label{{Name: "__name__", Value: "temp"}}, t: 1000, v: 71.3, wantRef: 1, wantErr: nil},
{kind: actAppend, labels: []labels.Label{{Name: "__name__", Value: "humidity"}}, t: 1000, v: 55.0, wantRef: 2, wantErr: nil},
{kind: actAppend, ref: 1, t: 1015, v: 71.4, wantRef: 1, wantErr: nil},
{kind: actAppend, ref: 2, t: 1015, v: 54.0, wantRef: 2, wantErr: nil},
{kind: actCommit, wantErr: nil},
},
wantSamples: map[uint64][]sample{
1: {s(1000, 71.3), s(1015, 71.4)},
2: {s(1000, 55.0), s(1015, 54.0)},
},
},
{
name: "out_of_order_rejected",
actions: []action{
{kind: actNew},
{kind: actAppend, labels: []labels.Label{{Name: "__name__", Value: "temp"}}, t: 1000, v: 1.0, wantRef: 1, wantErr: nil},
{kind: actAppend, ref: 1, t: 999, v: 2.0, wantRef: 0, wantErr: ErrOutOfOrder},
{kind: actCommit, wantErr: nil},
},
wantSamples: map[uint64][]sample{
1: {s(1000, 1.0)},
},
},
{
name: "duplicate_timestamp_rejected",
actions: []action{
{kind: actNew},
{kind: actAppend, labels: []labels.Label{{Name: "__name__", Value: "temp"}}, t: 1000, v: 1.0, wantRef: 1, wantErr: nil},
{kind: actAppend, ref: 1, t: 1000, v: 2.0, wantRef: 0, wantErr: ErrOutOfOrder},
{kind: actCommit, wantErr: nil},
},
wantSamples: map[uint64][]sample{
1: {s(1000, 1.0)},
},
},
{
name: "out_of_order_across_commits",
actions: []action{
{kind: actNew},
{kind: actAppend, labels: []labels.Label{{Name: "__name__", Value: "temp"}}, t: 1000, v: 1.0, wantRef: 1, wantErr: nil},
{kind: actCommit, wantErr: nil},
{kind: actNew},
{kind: actAppend, ref: 1, t: 500, v: 2.0, wantRef: 0, wantErr: ErrOutOfOrder},
{kind: actCommit, wantErr: nil},
},
wantSamples: map[uint64][]sample{
1: {s(1000, 1.0)},
},
},
{
name: "unknown_ref_rejected",
actions: []action{
{kind: actNew},
{kind: actAppend, ref: 999, t: 1000, v: 1.0, wantRef: 0, wantErr: ErrSeriesNotFound},
{kind: actCommit, wantErr: nil},
},
wantSamples: map[uint64][]sample{},
},
{
name: "empty_label_name_rejected",
actions: []action{
{kind: actNew},
{kind: actAppend, labels: []labels.Label{{Name: "", Value: "temp"}}, t: 1000, v: 1.0, wantRef: 0, wantErr: labels.ErrEmptyName},
{kind: actCommit, wantErr: nil},
},
wantSamples: map[uint64][]sample{},
},
{
name: "unsorted_labels_normalized",
actions: []action{
{kind: actNew},
{kind: actAppend, labels: []labels.Label{{Name: "room", Value: "office"}, {Name: "__name__", Value: "temp"}}, t: 1000, v: 71.3, wantRef: 1, wantErr: nil},
{kind: actCommit, wantErr: nil},
},
wantSamples: map[uint64][]sample{
1: {s(1000, 71.3)},
},
},
{
name: "chunk_sealing",
actions: func() []action {
acts := []action{{kind: actNew}}
acts = append(acts, action{kind: actAppend, labels: []labels.Label{{Name: "__name__", Value: "temp"}}, t: 0, v: 0, wantRef: 1})
for i := 1; i < 250; i++ {
acts = append(acts, action{kind: actAppend, ref: 1, t: int64(i * 15000), v: float64(i), wantRef: 1})
}
acts = append(acts, action{kind: actCommit})
return acts
}(),
wantSamples: map[uint64][]sample{
1: func() []sample {
out := make([]sample, 250)
for i := range out {
out[i] = s(int64(i*15000), float64(i))
}
return out
}(),
},
},
{
name: "query_out_of_range_returns_empty",
actions: []action{
{kind: actNew},
{kind: actAppend, labels: []labels.Label{{Name: "__name__", Value: "temp"}}, t: 1000, v: 1.0, wantRef: 1, wantErr: nil},
{kind: actCommit, wantErr: nil},
},
// wantSamples checks ref=1 over all time; a separate query assertion below.
wantSamples: map[uint64][]sample{
1: {s(1000, 1.0)},
},
},
// --- Appender lifecycle error paths ---
{
name: "rollback_discards_samples",
actions: []action{
{kind: actNew},
{kind: actAppend, labels: []labels.Label{{Name: "__name__", Value: "temp"}}, t: 1000, v: 1.0, wantRef: 1, wantErr: nil},
{kind: actCommit, wantErr: nil},
{kind: actNew},
{kind: actAppend, ref: 1, t: 2000, v: 2.0, wantRef: 1, wantErr: nil},
{kind: actRollback, wantErr: nil},
},
wantSamples: map[uint64][]sample{
1: {s(1000, 1.0)},
},
},
{
name: "rollback_removes_new_series",
actions: []action{
{kind: actNew},
{kind: actAppend, labels: []labels.Label{{Name: "__name__", Value: "temp"}}, t: 1000, v: 1.0, wantRef: 1, wantErr: nil},
{kind: actRollback, wantErr: nil},
},
wantSamples: map[uint64][]sample{},
},
{
name: "commit_after_commit",
actions: []action{
{kind: actNew},
{kind: actCommit, wantErr: nil},
{kind: actCommit, wantErr: ErrAppenderClosed},
},
wantSamples: map[uint64][]sample{},
},
{
name: "rollback_after_commit",
actions: []action{
{kind: actNew},
{kind: actCommit, wantErr: nil},
{kind: actRollback, wantErr: ErrAppenderClosed},
},
wantSamples: map[uint64][]sample{},
},
{
name: "commit_after_rollback",
actions: []action{
{kind: actNew},
{kind: actRollback, wantErr: nil},
{kind: actCommit, wantErr: ErrAppenderClosed},
},
wantSamples: map[uint64][]sample{},
},
{
name: "rollback_after_rollback",
actions: []action{
{kind: actNew},
{kind: actRollback, wantErr: nil},
{kind: actRollback, wantErr: ErrAppenderClosed},
},
wantSamples: map[uint64][]sample{},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h := openHead(t)
var app *Appender
for i, act := range tc.actions {
switch act.kind {
case actNew:
app = h.Appender()
case actAppend:
ref, err := app.Append(act.ref, act.labels, act.t, act.v)
if ref != act.wantRef {
t.Errorf("action %d ref: got %v, want %v", i, ref, act.wantRef)
}
if err != act.wantErr {
t.Errorf("action %d error: got %v, want %v", i, err, act.wantErr)
}
case actCommit:
err := app.Commit()
if err != act.wantErr {
t.Errorf("action %d commit error: got %v, want %v", i, err, act.wantErr)
}
case actRollback:
err := app.Rollback()
if err != act.wantErr {
t.Errorf("action %d rollback error: got %v, want %v", i, err, act.wantErr)
}
}
}
for ref, wantSamples := range tc.wantSamples {
got := collectSamples(t, h, ref, math.MinInt64, math.MaxInt64)
if len(got) != len(wantSamples) {
t.Fatalf("ref %d sample count: got %v, want %v", ref, len(got), len(wantSamples))
}
for i, want := range wantSamples {
if !reflect.DeepEqual(got[i], want) {
t.Errorf("ref %d sample %d: got %v, want %v", ref, i, got[i], want)
}
}
}
})
}
}
func TestWALReplay(t *testing.T) {
tests := []struct {
name string
setup func(t *testing.T, dir string)
wantSeries map[uint64][]sample
}{
{
name: "basic_recovery",
setup: func(t *testing.T, dir string) {
h, err := Open(dir, wal.Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
app := h.Appender()
ref, err := app.Append(0, []labels.Label{{Name: "__name__", Value: "temp"}}, 1000, 71.3)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, err = app.Append(ref, nil, 1015, 71.4)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := app.Commit(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := h.Close(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
},
wantSeries: map[uint64][]sample{
1: {s(1000, 71.3), s(1015, 71.4)},
},
},
{
name: "multi_series_recovery",
setup: func(t *testing.T, dir string) {
h, err := Open(dir, wal.Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
app := h.Appender()
_, err = app.Append(0, []labels.Label{{Name: "__name__", Value: "temp"}}, 1000, 71.3)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, err = app.Append(0, []labels.Label{{Name: "__name__", Value: "humidity"}}, 1000, 55.0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := app.Commit(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := h.Close(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
},
wantSeries: map[uint64][]sample{
1: {s(1000, 71.3)},
2: {s(1000, 55.0)},
},
},
{
name: "chunk_sealing_recovery",
setup: func(t *testing.T, dir string) {
h, err := Open(dir, wal.Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
app := h.Appender()
ref, err := app.Append(0, []labels.Label{{Name: "__name__", Value: "temp"}}, 0, 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
for i := 1; i < 250; i++ {
_, err = app.Append(ref, nil, int64(i*15000), float64(i))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
if err := app.Commit(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := h.Close(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
},
wantSeries: map[uint64][]sample{
1: func() []sample {
out := make([]sample, 250)
for i := range out {
out[i] = s(int64(i*15000), float64(i))
}
return out
}(),
},
},
{
name: "multiple_commits_recovery",
setup: func(t *testing.T, dir string) {
h, err := Open(dir, wal.Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
app := h.Appender()
_, err = app.Append(0, []labels.Label{{Name: "__name__", Value: "temp"}}, 1000, 1.0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := app.Commit(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
app = h.Appender()
_, err = app.Append(1, nil, 2000, 2.0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := app.Commit(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := h.Close(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
},
wantSeries: map[uint64][]sample{
1: {s(1000, 1.0), s(2000, 2.0)},
},
},
{
name: "rollback_not_recovered",
setup: func(t *testing.T, dir string) {
h, err := Open(dir, wal.Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
app := h.Appender()
_, err = app.Append(0, []labels.Label{{Name: "__name__", Value: "temp"}}, 1000, 1.0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := app.Commit(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Second batch is rolled back — should not survive restart.
app = h.Appender()
_, err = app.Append(1, nil, 2000, 2.0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := app.Rollback(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := h.Close(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
},
wantSeries: map[uint64][]sample{
1: {s(1000, 1.0)},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
dir := filepath.Join(t.TempDir(), "wal")
tc.setup(t, dir)
h, err := Open(dir, wal.Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer h.Close()
for ref, wantSamples := range tc.wantSeries {
got := collectSamples(t, h, ref, math.MinInt64, math.MaxInt64)
if len(got) != len(wantSamples) {
t.Fatalf("ref %d sample count: got %v, want %v", ref, len(got), len(wantSamples))
}
for i, want := range wantSamples {
if !reflect.DeepEqual(got[i], want) {
t.Errorf("ref %d sample %d: got %v, want %v", ref, i, got[i], want)
}
}
}
})
}
}
func TestFlushOlderThan(t *testing.T) {
tests := []struct {
name string
numSamples int // samples per series (each at 15s intervals)
flushMaxT int64
postFlushAppend int // additional samples to append after flush (0 = none)
wantULID bool // whether flush produces a block
wantBlockSeries int // number of series in the block (0 if no block)
wantBlockCount int // samples in the block (0 if no block)
wantHeadCount int // minimum samples remaining in head after flush
}{
{
name: "flush_sealed_chunks",
numSamples: 250,
flushMaxT: math.MaxInt64,
postFlushAppend: 0,
wantULID: true,
wantBlockSeries: 1,
wantBlockCount: 240,
wantHeadCount: 10,
},
{
name: "nothing_to_flush",
numSamples: 50,
flushMaxT: math.MaxInt64,
postFlushAppend: 0,
wantULID: false,
wantBlockSeries: 0,
wantBlockCount: 0,
wantHeadCount: 50,
},
{
name: "partial_flush_by_time",
numSamples: 250,
flushMaxT: 120 * 15000,
postFlushAppend: 0,
wantULID: true,
wantBlockSeries: 1,
wantBlockCount: 120,
wantHeadCount: 10,
},
{
name: "flush_then_continue_appending",
numSamples: 250,
flushMaxT: math.MaxInt64,
postFlushAppend: 10,
wantULID: true,
wantBlockSeries: 1,
wantBlockCount: 240,
wantHeadCount: 10,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h := openHead(t)
app := h.Appender()
ref, err := app.Append(0, []labels.Label{{Name: "__name__", Value: "temp"}}, 0, 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
for i := 1; i < tc.numSamples; i++ {
_, err = app.Append(ref, nil, int64(i*15000), float64(i))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
if err := app.Commit(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
ulid, err := h.FlushOlderThan(tc.flushMaxT)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Post-flush appends.
if tc.postFlushAppend > 0 {
app = h.Appender()
for i := tc.numSamples; i < tc.numSamples+tc.postFlushAppend; i++ {
_, err = app.Append(ref, nil, int64(i*15000), float64(i))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
if err := app.Commit(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
// Assert ULID presence.
gotULID := ulid != ""
if gotULID != tc.wantULID {
t.Errorf("block ULID presence: got %v, want %v", gotULID, tc.wantULID)
}
// Assert block contents.
blockCount := 0
if ulid != "" {
blockDir := filepath.Join(h.DataDir(), ulid)
br, err := block.Open(blockDir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer br.Close()
if br.Meta.Stats.NumSeries != tc.wantBlockSeries {
t.Errorf("block series count: got %v, want %v", br.Meta.Stats.NumSeries, tc.wantBlockSeries)
}
it, err := br.SeriesChunkIterator(ref, math.MinInt64, math.MaxInt64)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
for it.Next() {
blockCount++
}
if err := it.Err(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
if blockCount != tc.wantBlockCount {
t.Errorf("block sample count: got %v, want %v", blockCount, tc.wantBlockCount)
}
// Assert head still has data.
headSamples := collectSamples(t, h, ref, math.MinInt64, math.MaxInt64)
if len(headSamples) < tc.wantHeadCount {
t.Errorf("head sample count: got %v, want >= %v", len(headSamples), tc.wantHeadCount)
}
})
}
}
func TestFlushWALTruncation(t *testing.T) {
tests := []struct {
name string
numSamples int
}{
{name: "250_samples", numSamples: 250},
{name: "500_samples", numSamples: 500},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
walDir := filepath.Join(dir, "wal")
h, err := Open(walDir, wal.Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
app := h.Appender()
ref, err := app.Append(0, []labels.Label{{Name: "__name__", Value: "temp"}}, 0, 0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_ = ref
for i := 1; i < tc.numSamples; i++ {
_, err = app.Append(ref, nil, int64(i*15000), float64(i))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
if err := app.Commit(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
walEntries, err := os.ReadDir(walDir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
segsBefore := len(walEntries)
_, err = h.FlushOlderThan(math.MaxInt64)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
walEntries, err = os.ReadDir(walDir)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
segsAfter := len(walEntries)
if segsAfter > segsBefore {
t.Errorf("WAL should be truncated after flush: got %v segments after, had %v before", segsAfter, segsBefore)
}
if err := h.Close(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Re-open: head should be functional.
h2, err := Open(walDir, wal.Options{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
defer h2.Close()
app = h2.Appender()
_, err = app.Append(0, []labels.Label{{Name: "__name__", Value: "new_series"}}, 5000000, 42.0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := app.Commit(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestHeadPostings(t *testing.T) {
h := openHead(t)
// Add three series.
app := h.Appender()
_, err := app.Append(0, []labels.Label{{Name: "__name__", Value: "temp"}, {Name: "room", Value: "office"}}, 1000, 1.0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, err = app.Append(0, []labels.Label{{Name: "__name__", Value: "humidity"}, {Name: "room", Value: "office"}}, 1000, 2.0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, err = app.Append(0, []labels.Label{{Name: "__name__", Value: "temp"}, {Name: "room", Value: "kitchen"}}, 1000, 3.0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err := app.Commit(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
tests := []struct {
name string
label string
value string
wantRefs []uint64
}{
{name: "match_name_temp", label: "__name__", value: "temp", wantRefs: []uint64{1, 3}},
{name: "match_room_office", label: "room", value: "office", wantRefs: []uint64{1, 2}},
{name: "match_name_humidity", label: "__name__", value: "humidity", wantRefs: []uint64{2}},
{name: "no_match", label: "__name__", value: "pressure", wantRefs: nil},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := h.Postings(tc.label, tc.value)
if !reflect.DeepEqual(got, tc.wantRefs) {
t.Errorf("got %v, want %v", got, tc.wantRefs)
}
})
}
}
func TestHeadQueryMethods(t *testing.T) {
tests := []struct {
name string
series [][]labels.Label
wantLabelValues map[string][]string // label name -> expected values
wantLabels map[uint64][]labels.Label // ref -> expected labels
wantAllPostings []uint64
}{
{
name: "multiple_series_with_shared_labels",
series: [][]labels.Label{
{{Name: "__name__", Value: "temp"}, {Name: "room", Value: "office"}},
{{Name: "__name__", Value: "humidity"}, {Name: "room", Value: "office"}},
{{Name: "__name__", Value: "temp"}, {Name: "room", Value: "kitchen"}},
},
wantLabelValues: map[string][]string{
"__name__": {"humidity", "temp"},
"room": {"kitchen", "office"},
"nonexistent": {},
},
wantLabels: map[uint64][]labels.Label{
1: {{Name: "__name__", Value: "temp"}, {Name: "room", Value: "office"}},
2: {{Name: "__name__", Value: "humidity"}, {Name: "room", Value: "office"}},
3: {{Name: "__name__", Value: "temp"}, {Name: "room", Value: "kitchen"}},
},
wantAllPostings: []uint64{1, 2, 3},
},
{
name: "single_series",
series: [][]labels.Label{
{{Name: "__name__", Value: "temp"}},
},
wantLabelValues: map[string][]string{
"__name__": {"temp"},
"nonexistent": {},
},
wantLabels: map[uint64][]labels.Label{
1: {{Name: "__name__", Value: "temp"}},
},
wantAllPostings: []uint64{1},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h := openHead(t)
app := h.Appender()
for _, ls := range tc.series {
_, err := app.Append(0, ls, 1000, 1.0)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
if err := app.Commit(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Assert LabelValues.
for name, wantVals := range tc.wantLabelValues {
got := h.LabelValues(name)
if !reflect.DeepEqual(got, wantVals) {
t.Errorf("LabelValues(%q): got %v, want %v", name, got, wantVals)
}
}
// Assert Labels by ref.
for ref, wantLabels := range tc.wantLabels {
ls, ok := h.Labels(ref)
if !ok {
t.Errorf("Labels(%d) should exist", ref)
}
if !reflect.DeepEqual(ls, wantLabels) {
t.Errorf("Labels(%d): got %v, want %v", ref, ls, wantLabels)
}
}
// Unknown ref returns false.
_, ok := h.Labels(999)
if ok {
t.Errorf("Labels(999) should not exist")
}
// Assert AllPostings.
if !reflect.DeepEqual(h.AllPostings(), tc.wantAllPostings) {
t.Errorf("AllPostings: got %v, want %v", h.AllPostings(), tc.wantAllPostings)
}
})
}
}
func TestConcurrentAppend(t *testing.T) {
tests := []struct {
name string
numGoroutines int
samplesPerGoroutine int
}{
{name: "8_goroutines_100_samples", numGoroutines: 8, samplesPerGoroutine: 100},
{name: "1_goroutine_1000_samples", numGoroutines: 1, samplesPerGoroutine: 1000},
{name: "32_goroutines_10_samples", numGoroutines: 32, samplesPerGoroutine: 10},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
h := openHead(t)
refs := make([]uint64, tc.numGoroutines)
var wg sync.WaitGroup
for g := 0; g < tc.numGoroutines; g++ {
wg.Add(1)
go func(g int) {
defer wg.Done()
app := h.Appender()
ls := []labels.Label{
{Name: "__name__", Value: "metric"},
{Name: "goroutine", Value: string(rune('A' + g))},
}
ref, err := app.Append(0, ls, int64(g*1000000), 0)
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
refs[g] = ref
for i := 1; i < tc.samplesPerGoroutine; i++ {
_, err = app.Append(ref, nil, int64(g*1000000+i*1000), float64(i))
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
}
if err := app.Commit(); err != nil {
t.Errorf("unexpected error: %v", err)
}
}(g)
}
wg.Wait()
for g := 0; g < tc.numGoroutines; g++ {
got := collectSamples(t, h, refs[g], math.MinInt64, math.MaxInt64)
if len(got) != tc.samplesPerGoroutine {
t.Errorf("goroutine %d sample count: got %v, want %v", g, len(got), tc.samplesPerGoroutine)
}
}
})
}
}