2026-01-05 07:46:21 -05:00
// Copyright The Prometheus Authors
2021-03-16 05:47:45 -04:00
// 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 (
2021-07-20 00:52:57 -04:00
"context"
"fmt"
"math"
2021-03-16 05:47:45 -04:00
"reflect"
2026-01-07 07:25:50 -05:00
"sort"
2021-03-16 05:47:45 -04:00
"strconv"
2021-05-06 16:53:52 -04:00
"strings"
2024-10-29 05:40:46 -04:00
"sync"
2021-03-16 05:47:45 -04:00
"testing"
2021-10-22 04:19:38 -04:00
"github.com/prometheus/client_golang/prometheus"
2021-03-16 05:47:45 -04:00
"github.com/stretchr/testify/require"
2021-11-08 09:23:17 -05:00
"github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/labels"
2021-03-16 05:47:45 -04:00
"github.com/prometheus/prometheus/storage"
)
2021-07-20 00:52:57 -04:00
var eMetrics = NewExemplarMetrics ( prometheus . DefaultRegisterer )
2021-05-19 21:51:45 -04:00
// Tests the same exemplar cases as AddExemplar, but specifically the ValidateExemplar function so it can be relied on externally.
2021-05-06 16:53:52 -04:00
func TestValidateExemplar ( t * testing . T ) {
2026-01-07 07:25:50 -05:00
exs , err := NewCircularExemplarStorage ( 2 , eMetrics , 0 )
2021-03-16 05:47:45 -04:00
require . NoError ( t , err )
es := exs . ( * CircularExemplarStorage )
2022-03-09 17:17:29 -05:00
l := labels . FromStrings ( "service" , "asdf" )
2021-03-16 05:47:45 -04:00
e := exemplar . Exemplar {
2024-02-15 09:19:54 -05:00
Labels : labels . FromStrings ( "trace_id" , "qwerty" ) ,
2022-03-09 17:17:29 -05:00
Value : 0.1 ,
Ts : 1 ,
2021-03-16 05:47:45 -04:00
}
2021-05-06 16:53:52 -04:00
require . NoError ( t , es . ValidateExemplar ( l , e ) )
require . NoError ( t , es . AddExemplar ( l , e ) )
e2 := exemplar . Exemplar {
2024-02-15 09:19:54 -05:00
Labels : labels . FromStrings ( "trace_id" , "zxcvb" ) ,
2022-03-09 17:17:29 -05:00
Value : 0.1 ,
Ts : 2 ,
2021-05-06 16:53:52 -04:00
}
require . NoError ( t , es . ValidateExemplar ( l , e2 ) )
require . NoError ( t , es . AddExemplar ( l , e2 ) )
require . Equal ( t , es . ValidateExemplar ( l , e2 ) , storage . ErrDuplicateExemplar , "error is expected attempting to validate duplicate exemplar" )
e3 := e2
e3 . Ts = 3
require . Equal ( t , es . ValidateExemplar ( l , e3 ) , storage . ErrDuplicateExemplar , "error is expected when attempting to add duplicate exemplar, even with different timestamp" )
e3 . Ts = 1
e3 . Value = 0.3
require . Equal ( t , es . ValidateExemplar ( l , e3 ) , storage . ErrOutOfOrderExemplar )
e4 := exemplar . Exemplar {
2022-03-09 17:17:29 -05:00
Labels : labels . FromStrings ( "a" , strings . Repeat ( "b" , exemplar . ExemplarMaxLabelSetLength ) ) ,
Value : 0.1 ,
Ts : 2 ,
2021-05-06 16:53:52 -04:00
}
require . Equal ( t , storage . ErrExemplarLabelLength , es . ValidateExemplar ( l , e4 ) )
}
2026-01-07 07:25:50 -05:00
func TestCircularExemplarStorage_AddExemplar ( t * testing . T ) {
series1 := labels . FromStrings ( "trace_id" , "foo" )
series2 := labels . FromStrings ( "trace_id" , "bar" )
2021-05-06 16:53:52 -04:00
2026-01-07 07:25:50 -05:00
series1Matcher := [ ] * labels . Matcher { {
Type : labels . MatchEqual ,
Name : "trace_id" ,
Value : series1 . Get ( "trace_id" ) ,
} }
series2Matcher := [ ] * labels . Matcher { {
Type : labels . MatchEqual ,
Name : "trace_id" ,
Value : series2 . Get ( "trace_id" ) ,
} }
testCases := [ ] struct {
name string
size int64
exemplars [ ] exemplar . Exemplar
wantExemplars [ ] exemplar . Exemplar
matcher [ ] * labels . Matcher
wantError error
} {
{
name : "insert after newest" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
} ,
matcher : series1Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
} ,
} ,
{
name : "insert before oldest" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 2 } ,
{ Labels : series1 , Value : 0.2 , Ts : 1 } ,
} ,
matcher : series1Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.2 , Ts : 1 } ,
{ Labels : series1 , Value : 0.1 , Ts : 2 } ,
} ,
} ,
{
name : "insert in between" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 3 } ,
{ Labels : series1 , Value : 0.3 , Ts : 2 } ,
} ,
matcher : series1Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.3 , Ts : 2 } ,
{ Labels : series1 , Value : 0.2 , Ts : 3 } ,
} ,
} ,
{
name : "insert after newest with overflow" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.4 , Ts : 4 } ,
} ,
matcher : series1Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.4 , Ts : 4 } ,
} ,
} ,
{
name : "insert before oldest with overflow" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.4 , Ts : 0 } ,
} ,
matcher : series1Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.4 , Ts : 0 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
} ,
} ,
{
name : "insert between with overflow" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 3 } ,
{ Labels : series1 , Value : 0.3 , Ts : 4 } ,
{ Labels : series1 , Value : 0.4 , Ts : 2 } ,
} ,
matcher : series1Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.4 , Ts : 2 } ,
{ Labels : series1 , Value : 0.2 , Ts : 3 } ,
{ Labels : series1 , Value : 0.3 , Ts : 4 } ,
} ,
} ,
2026-01-15 02:49:37 -05:00
{
name : "out-of-order insert where evicted entry is insertion point" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.2 , Ts : 2 } , // pos 0, linked list middle
{ Labels : series1 , Value : 0.1 , Ts : 1 } , // pos 1, linked list oldest
{ Labels : series1 , Value : 0.5 , Ts : 5 } , // pos 2, linked list newest
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
} ,
matcher : series1Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.5 , Ts : 5 } ,
} ,
} ,
2026-01-07 07:25:50 -05:00
{
name : "insert out of the OOO window" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 200 } ,
{ Labels : series1 , Value : 0.2 , Ts : 1 } ,
} ,
wantError : storage . ErrOutOfOrderExemplar ,
} ,
{
name : "insert multiple series" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 3 } ,
{ Labels : series2 , Value : 0.3 , Ts : 4 } ,
} ,
matcher : series1Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 3 } ,
} ,
} ,
{
name : "insert multiple series with overflow" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series2 , Value : 0.1 , Ts : 1 } ,
{ Labels : series2 , Value : 0.2 , Ts : 2 } ,
{ Labels : series2 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.4 , Ts : 4 } ,
} ,
matcher : series2Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series2 , Value : 0.2 , Ts : 2 } ,
{ Labels : series2 , Value : 0.3 , Ts : 3 } ,
} ,
} ,
{
name : "series1 overflows series2 out-of-order" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series2 , Value : 0.1 , Ts : 3 } ,
{ Labels : series2 , Value : 0.2 , Ts : 2 } ,
{ Labels : series2 , Value : 0.3 , Ts : 4 } ,
{ Labels : series1 , Value : 0.4 , Ts : 4 } ,
{ Labels : series1 , Value : 0.5 , Ts : 1 } ,
} ,
matcher : series2Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series2 , Value : 0.3 , Ts : 4 } ,
} ,
} ,
{
name : "ignore duplicate exemplars" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 3 } ,
{ Labels : series1 , Value : 0.1 , Ts : 3 } ,
} ,
matcher : series1Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 3 } ,
} ,
} ,
{
name : "ignore duplicate exemplars when buffer is full" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 3 } ,
{ Labels : series1 , Value : 0.2 , Ts : 4 } ,
{ Labels : series1 , Value : 0.3 , Ts : 5 } ,
{ Labels : series1 , Value : 0.3 , Ts : 5 } ,
} ,
matcher : series1Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 3 } ,
{ Labels : series1 , Value : 0.2 , Ts : 4 } ,
{ Labels : series1 , Value : 0.3 , Ts : 5 } ,
} ,
} ,
{
name : "empty timestamps are valid" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 0 } ,
{ Labels : series1 , Value : 0.2 , Ts : 0 } ,
} ,
matcher : series1Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 0 } ,
{ Labels : series1 , Value : 0.2 , Ts : 0 } ,
} ,
} ,
{
name : "exemplar label length exceeds maximum" ,
size : 3 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : labels . FromStrings ( "a" , strings . Repeat ( "b" , exemplar . ExemplarMaxLabelSetLength ) ) , Value : 0.1 , Ts : 2 } ,
} ,
wantError : storage . ErrExemplarLabelLength ,
} ,
{
name : "native histograms" ,
size : 6 ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
} ,
matcher : series1Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
} ,
} ,
{
name : "evict only exemplar for series then re-add" ,
size : 2 ,
exemplars : [ ] exemplar . Exemplar {
// series1 at index 0, series2 at index 1, then series1 evicts its own only exemplar
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series2 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
} ,
matcher : series1Matcher ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
} ,
} ,
2021-05-06 16:53:52 -04:00
}
2026-01-07 07:25:50 -05:00
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
exs , err := NewCircularExemplarStorage ( tc . size , eMetrics , 100 )
require . NoError ( t , err )
es := exs . ( * CircularExemplarStorage )
2021-05-06 16:53:52 -04:00
2026-01-07 07:25:50 -05:00
// Add exemplars and compare tc.wantErr against the first exemplar failing.
var addError error
for i , ex := range tc . exemplars {
addError = es . AddExemplar ( ex . Labels , ex )
if addError != nil {
break
}
if testing . Verbose ( ) {
t . Logf ( "Buffer[%d]:\n%s" , i , debugCircularBuffer ( es ) )
}
}
if tc . wantError == nil {
require . NoError ( t , addError )
} else {
require . ErrorIs ( t , addError , tc . wantError )
}
if addError != nil {
return
}
2021-03-16 05:47:45 -04:00
2026-01-07 07:25:50 -05:00
// Ensure exemplars are returned correctly and in-order.
gotExemplars , err := es . Select ( 0 , 1000 , tc . matcher )
require . NoError ( t , err )
if len ( tc . wantExemplars ) == 0 {
require . Empty ( t , gotExemplars )
} else {
require . Len ( t , gotExemplars , 1 )
require . Equal ( t , tc . wantExemplars , gotExemplars [ 0 ] . Exemplars )
}
} )
2021-03-16 05:47:45 -04:00
}
2026-01-07 07:25:50 -05:00
}
2021-03-16 05:47:45 -04:00
2026-01-07 07:25:50 -05:00
func TestCircularExemplarStorage_Resize ( t * testing . T ) {
series1 := labels . FromStrings ( "trace_id" , "foo" )
series2 := labels . FromStrings ( "trace_id" , "bar" )
matcher1 := [ ] * labels . Matcher {
labels . MustNewMatcher ( labels . MatchRegexp , "trace_id" , "(foo|bar)" ) ,
}
2021-03-16 05:47:45 -04:00
2026-01-07 07:25:50 -05:00
testCases := [ ] struct {
name string
exemplars [ ] exemplar . Exemplar
resize int64
wantExemplars [ ] exemplar . Exemplar
wantNextIndex int
wantError error
} {
{
name : "in-order, grow" ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
} ,
resize : 10 ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
} ,
2026-01-14 10:44:50 -05:00
wantNextIndex : 3 ,
2026-01-07 07:25:50 -05:00
} ,
{
name : "in-order, shrink" ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
} ,
resize : 2 ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
} ,
wantNextIndex : 0 ,
} ,
{
name : "out-of-order, shrink" ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
} ,
resize : 2 ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
} ,
wantNextIndex : 0 ,
} ,
{
name : "out-of-order, grow" ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
} ,
resize : 5 ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
} ,
2026-01-14 10:44:50 -05:00
wantNextIndex : 3 ,
2026-01-07 07:25:50 -05:00
} ,
{
name : "duplicate timestamps" ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 1 } ,
{ Labels : series1 , Value : 0.3 , Ts : 2 } ,
} ,
resize : 3 ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 1 } ,
{ Labels : series1 , Value : 0.3 , Ts : 2 } ,
} ,
} ,
{
name : "empty input, grow" ,
exemplars : [ ] exemplar . Exemplar { } ,
resize : 10 ,
wantExemplars : [ ] exemplar . Exemplar { } ,
2026-01-14 10:44:50 -05:00
wantNextIndex : 3 ,
2026-01-07 07:25:50 -05:00
} ,
{
name : "empty input, shrink" ,
exemplars : [ ] exemplar . Exemplar { } ,
resize : 1 ,
wantExemplars : [ ] exemplar . Exemplar { } ,
wantNextIndex : 0 ,
} ,
{
name : "shrink to zero" ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
} ,
resize : 0 ,
wantExemplars : [ ] exemplar . Exemplar { } ,
wantNextIndex : 0 ,
} ,
{
name : "multiple series, shrink" ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series2 , Value : 1.1 , Ts : 2 } ,
{ Labels : series1 , Value : 0.2 , Ts : 3 } ,
{ Labels : series2 , Value : 1.2 , Ts : 4 } ,
} ,
resize : 2 ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.2 , Ts : 3 } ,
{ Labels : series2 , Value : 1.2 , Ts : 4 } ,
} ,
wantNextIndex : 0 ,
} ,
{
name : "shrink to one" ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
} ,
resize : 1 ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
} ,
wantNextIndex : 0 ,
} ,
{
name : "shrink to two" ,
exemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
} ,
resize : 2 ,
wantExemplars : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
} ,
2026-01-14 10:44:50 -05:00
wantNextIndex : 0 ,
2026-01-07 07:25:50 -05:00
} ,
}
2021-03-16 05:47:45 -04:00
2026-01-07 07:25:50 -05:00
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
exs , err := NewCircularExemplarStorage ( 3 , eMetrics , 100 )
require . NoError ( t , err )
es := exs . ( * CircularExemplarStorage )
2021-03-16 05:47:45 -04:00
2026-01-07 07:25:50 -05:00
for _ , ex := range tc . exemplars {
require . NoError ( t , es . AddExemplar ( ex . Labels , ex ) )
}
2021-05-06 16:53:52 -04:00
2026-01-07 07:25:50 -05:00
// Resize the circular buffer.
if testing . Verbose ( ) {
t . Logf ( "Buffer[before-resize]:\n%s" , debugCircularBuffer ( es ) )
}
es . Resize ( tc . resize )
if testing . Verbose ( ) {
t . Logf ( "Buffer[after-resize]:\n%s" , debugCircularBuffer ( es ) )
}
// Ensure exemplars are returned correctly and in-order.
gotExemplars , err := es . Select ( 0 , 1000 , matcher1 )
require . NoError ( t , err )
flat := make ( [ ] exemplar . Exemplar , 0 )
for _ , group := range gotExemplars {
flat = append ( flat , group . Exemplars ... )
}
sort . Slice ( flat , func ( i , j int ) bool {
return flat [ i ] . Ts < flat [ j ] . Ts
} )
require . Equal ( t , tc . wantExemplars , flat , "exemplar mismatch" )
require . Equal ( t , tc . wantNextIndex , es . nextIndex , "next index mismatch" )
} )
}
resizeTwiceCases := [ ] struct {
name string
addExemplars1 [ ] exemplar . Exemplar
resize1 int64
wantExemplars1 [ ] exemplar . Exemplar
resize2 int64
addExemplars2 [ ] exemplar . Exemplar
wantExemplars2 [ ] exemplar . Exemplar
} {
{
name : "shrink then grow ordered" ,
addExemplars1 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.4 , Ts : 4 } ,
} ,
resize1 : 2 ,
wantExemplars1 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.4 , Ts : 4 } ,
} ,
resize2 : 5 ,
addExemplars2 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.5 , Ts : 5 } ,
{ Labels : series1 , Value : 0.6 , Ts : 6 } ,
{ Labels : series1 , Value : 0.7 , Ts : 7 } ,
} ,
wantExemplars2 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.4 , Ts : 4 } ,
{ Labels : series1 , Value : 0.5 , Ts : 5 } ,
{ Labels : series1 , Value : 0.6 , Ts : 6 } ,
{ Labels : series1 , Value : 0.7 , Ts : 7 } ,
} ,
} ,
{
name : "shrink then grow out-of-order" ,
addExemplars1 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.4 , Ts : 4 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
} ,
resize1 : 2 ,
wantExemplars1 : [ ] exemplar . Exemplar {
// We delete in the order of ingestion, not temporally.
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
} ,
resize2 : 5 ,
addExemplars2 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.7 , Ts : 7 } ,
{ Labels : series1 , Value : 0.6 , Ts : 6 } ,
{ Labels : series1 , Value : 0.5 , Ts : 5 } ,
} ,
wantExemplars2 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.5 , Ts : 5 } ,
{ Labels : series1 , Value : 0.6 , Ts : 6 } ,
{ Labels : series1 , Value : 0.7 , Ts : 7 } ,
} ,
} ,
{
name : "grow then shrink ordered" ,
addExemplars1 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.4 , Ts : 4 } ,
} ,
resize1 : 5 ,
wantExemplars1 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.4 , Ts : 4 } ,
} ,
resize2 : 2 ,
addExemplars2 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.5 , Ts : 5 } ,
{ Labels : series1 , Value : 0.6 , Ts : 6 } ,
{ Labels : series1 , Value : 0.7 , Ts : 7 } ,
} ,
wantExemplars2 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.6 , Ts : 6 } ,
{ Labels : series1 , Value : 0.7 , Ts : 7 } ,
} ,
} ,
{
name : "grow then shrink out-of-order" ,
addExemplars1 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.4 , Ts : 4 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
} ,
resize1 : 5 ,
wantExemplars1 : [ ] exemplar . Exemplar {
// We delete in the order of ingestion, not temporally.
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.4 , Ts : 4 } ,
} ,
resize2 : 2 ,
addExemplars2 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.7 , Ts : 7 } ,
{ Labels : series1 , Value : 0.5 , Ts : 5 } ,
{ Labels : series1 , Value : 0.6 , Ts : 6 } ,
} ,
wantExemplars2 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.5 , Ts : 5 } ,
{ Labels : series1 , Value : 0.6 , Ts : 6 } ,
} ,
} ,
2026-01-14 10:44:50 -05:00
{
name : "grow non-full buffer then add entries" ,
addExemplars1 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
} ,
resize1 : 10 ,
wantExemplars1 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
} ,
resize2 : 10 ,
addExemplars2 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.4 , Ts : 4 } ,
} ,
wantExemplars2 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
{ Labels : series1 , Value : 0.3 , Ts : 3 } ,
{ Labels : series1 , Value : 0.4 , Ts : 4 } ,
} ,
} ,
{
name : "shrink non-full buffer then add entries" ,
addExemplars1 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
} ,
resize1 : 2 ,
wantExemplars1 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
} ,
resize2 : 2 ,
addExemplars2 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
} ,
wantExemplars2 : [ ] exemplar . Exemplar {
{ Labels : series1 , Value : 0.1 , Ts : 1 } ,
{ Labels : series1 , Value : 0.2 , Ts : 2 } ,
} ,
} ,
2026-01-07 07:25:50 -05:00
}
for _ , tc := range resizeTwiceCases {
t . Run ( tc . name , func ( t * testing . T ) {
exs , err := NewCircularExemplarStorage ( 3 , eMetrics , 100 )
require . NoError ( t , err )
es := exs . ( * CircularExemplarStorage )
for _ , ex := range tc . addExemplars1 {
require . NoError ( t , es . AddExemplar ( ex . Labels , ex ) )
}
es . Resize ( tc . resize1 )
gotExemplars , err := es . Select ( 0 , 1000 , matcher1 )
require . NoError ( t , err )
require . Len ( t , gotExemplars , 1 )
require . Equal ( t , tc . wantExemplars1 , gotExemplars [ 0 ] . Exemplars )
es . Resize ( tc . resize2 )
for _ , ex := range tc . addExemplars2 {
require . NoError ( t , es . AddExemplar ( ex . Labels , ex ) )
}
if testing . Verbose ( ) {
t . Logf ( "Buffer[after-resize2]:\n%s" , debugCircularBuffer ( es ) )
}
gotExemplars , err = es . Select ( 0 , 1000 , matcher1 )
require . NoError ( t , err )
require . Len ( t , gotExemplars , 1 )
require . Equal ( t , tc . wantExemplars2 , gotExemplars [ 0 ] . Exemplars )
} )
2021-05-06 16:53:52 -04:00
}
2021-03-16 05:47:45 -04:00
}
func TestStorageOverflow ( t * testing . T ) {
// Test that circular buffer index and assignment
// works properly, adding more exemplars than can
// be stored and then querying for them.
2026-01-07 07:25:50 -05:00
exs , err := NewCircularExemplarStorage ( 5 , eMetrics , 0 )
2021-03-16 05:47:45 -04:00
require . NoError ( t , err )
es := exs . ( * CircularExemplarStorage )
2022-03-09 17:17:29 -05:00
lName , lValue := "service" , "asdf"
l := labels . FromStrings ( lName , lValue )
2021-03-16 05:47:45 -04:00
var eList [ ] exemplar . Exemplar
for i := 0 ; i < len ( es . exemplars ) + 1 ; i ++ {
e := exemplar . Exemplar {
2024-02-15 09:19:54 -05:00
Labels : labels . FromStrings ( "trace_id" , "a" ) ,
2022-03-09 17:17:29 -05:00
Value : float64 ( i + 1 ) / 10 ,
Ts : int64 ( 101 + i ) ,
2021-03-16 05:47:45 -04:00
}
es . AddExemplar ( l , e )
eList = append ( eList , e )
}
require . True ( t , ( es . exemplars [ 0 ] . exemplar . Ts == 106 ) , "exemplar was not stored correctly" )
2022-03-09 17:17:29 -05:00
m , err := labels . NewMatcher ( labels . MatchEqual , lName , lValue )
2021-03-16 05:47:45 -04:00
require . NoError ( t , err , "error creating label matcher for exemplar query" )
ret , err := es . Select ( 100 , 110 , [ ] * labels . Matcher { m } )
require . NoError ( t , err )
2023-12-07 06:35:01 -05:00
require . Len ( t , ret , 1 , "select should have returned samples for a single series only" )
2021-03-16 05:47:45 -04:00
require . True ( t , reflect . DeepEqual ( eList [ 1 : ] , ret [ 0 ] . Exemplars ) , "select did not return expected exemplars\n\texpected: %+v\n\tactual: %+v\n" , eList [ 1 : ] , ret [ 0 ] . Exemplars )
}
func TestSelectExemplar ( t * testing . T ) {
2026-01-07 07:25:50 -05:00
exs , err := NewCircularExemplarStorage ( 5 , eMetrics , 0 )
2021-03-16 05:47:45 -04:00
require . NoError ( t , err )
es := exs . ( * CircularExemplarStorage )
2022-03-09 17:17:29 -05:00
lName , lValue := "service" , "asdf"
l := labels . FromStrings ( lName , lValue )
2021-03-16 05:47:45 -04:00
e := exemplar . Exemplar {
2024-02-15 09:19:54 -05:00
Labels : labels . FromStrings ( "trace_id" , "querty" ) ,
2022-03-09 17:17:29 -05:00
Value : 0.1 ,
Ts : 12 ,
2021-03-16 05:47:45 -04:00
}
err = es . AddExemplar ( l , e )
require . NoError ( t , err , "adding exemplar failed" )
require . True ( t , reflect . DeepEqual ( es . exemplars [ 0 ] . exemplar , e ) , "exemplar was not stored correctly" )
2022-03-09 17:17:29 -05:00
m , err := labels . NewMatcher ( labels . MatchEqual , lName , lValue )
2021-03-16 05:47:45 -04:00
require . NoError ( t , err , "error creating label matcher for exemplar query" )
ret , err := es . Select ( 0 , 100 , [ ] * labels . Matcher { m } )
require . NoError ( t , err )
2023-12-07 06:35:01 -05:00
require . Len ( t , ret , 1 , "select should have returned samples for a single series only" )
2021-03-16 05:47:45 -04:00
expectedResult := [ ] exemplar . Exemplar { e }
require . True ( t , reflect . DeepEqual ( expectedResult , ret [ 0 ] . Exemplars ) , "select did not return expected exemplars\n\texpected: %+v\n\tactual: %+v\n" , expectedResult , ret [ 0 ] . Exemplars )
}
func TestSelectExemplar_MultiSeries ( t * testing . T ) {
2026-01-07 07:25:50 -05:00
exs , err := NewCircularExemplarStorage ( 5 , eMetrics , 0 )
2021-03-16 05:47:45 -04:00
require . NoError ( t , err )
es := exs . ( * CircularExemplarStorage )
2022-03-09 17:17:29 -05:00
l1Name := "test_metric"
l1 := labels . FromStrings ( labels . MetricName , l1Name , "service" , "asdf" )
l2Name := "test_metric2"
l2 := labels . FromStrings ( labels . MetricName , l2Name , "service" , "qwer" )
2021-03-16 05:47:45 -04:00
for i := 0 ; i < len ( es . exemplars ) ; i ++ {
e1 := exemplar . Exemplar {
2024-02-15 09:19:54 -05:00
Labels : labels . FromStrings ( "trace_id" , "a" ) ,
2022-03-09 17:17:29 -05:00
Value : float64 ( i + 1 ) / 10 ,
Ts : int64 ( 101 + i ) ,
2021-03-16 05:47:45 -04:00
}
err = es . AddExemplar ( l1 , e1 )
require . NoError ( t , err )
e2 := exemplar . Exemplar {
2024-02-15 09:19:54 -05:00
Labels : labels . FromStrings ( "trace_id" , "b" ) ,
2022-03-09 17:17:29 -05:00
Value : float64 ( i + 1 ) / 10 ,
Ts : int64 ( 101 + i ) ,
2021-03-16 05:47:45 -04:00
}
err = es . AddExemplar ( l2 , e2 )
require . NoError ( t , err )
}
2022-03-09 17:17:29 -05:00
m , err := labels . NewMatcher ( labels . MatchEqual , labels . MetricName , l2Name )
2021-03-16 05:47:45 -04:00
require . NoError ( t , err , "error creating label matcher for exemplar query" )
ret , err := es . Select ( 100 , 200 , [ ] * labels . Matcher { m } )
require . NoError ( t , err )
2023-12-07 06:35:01 -05:00
require . Len ( t , ret , 1 , "select should have returned samples for a single series only" )
require . Len ( t , ret [ 0 ] . Exemplars , 3 , "didn't get expected 8 exemplars, got %d" , len ( ret [ 0 ] . Exemplars ) )
2021-03-16 05:47:45 -04:00
2022-03-09 17:17:29 -05:00
m , err = labels . NewMatcher ( labels . MatchEqual , labels . MetricName , l1Name )
2021-03-16 05:47:45 -04:00
require . NoError ( t , err , "error creating label matcher for exemplar query" )
ret , err = es . Select ( 100 , 200 , [ ] * labels . Matcher { m } )
require . NoError ( t , err )
2023-12-07 06:35:01 -05:00
require . Len ( t , ret , 1 , "select should have returned samples for a single series only" )
require . Len ( t , ret [ 0 ] . Exemplars , 2 , "didn't get expected 8 exemplars, got %d" , len ( ret [ 0 ] . Exemplars ) )
2021-03-16 05:47:45 -04:00
}
func TestSelectExemplar_TimeRange ( t * testing . T ) {
2021-07-20 00:52:57 -04:00
var lenEs int64 = 5
2026-01-07 07:25:50 -05:00
exs , err := NewCircularExemplarStorage ( lenEs , eMetrics , 0 )
2021-03-16 05:47:45 -04:00
require . NoError ( t , err )
es := exs . ( * CircularExemplarStorage )
2022-03-09 17:17:29 -05:00
lName , lValue := "service" , "asdf"
l := labels . FromStrings ( lName , lValue )
2021-03-16 05:47:45 -04:00
2021-07-20 00:52:57 -04:00
for i := 0 ; int64 ( i ) < lenEs ; i ++ {
2021-03-16 05:47:45 -04:00
err := es . AddExemplar ( l , exemplar . Exemplar {
2024-02-15 09:19:54 -05:00
Labels : labels . FromStrings ( "trace_id" , strconv . Itoa ( i ) ) ,
2022-03-09 17:17:29 -05:00
Value : 0.1 ,
Ts : int64 ( 101 + i ) ,
2021-03-16 05:47:45 -04:00
} )
require . NoError ( t , err )
2022-01-06 05:28:58 -05:00
require . Equal ( t , es . index [ string ( l . Bytes ( nil ) ) ] . newest , i , "exemplar was not stored correctly" )
2021-03-16 05:47:45 -04:00
}
2022-03-09 17:17:29 -05:00
m , err := labels . NewMatcher ( labels . MatchEqual , lName , lValue )
2021-03-16 05:47:45 -04:00
require . NoError ( t , err , "error creating label matcher for exemplar query" )
ret , err := es . Select ( 102 , 104 , [ ] * labels . Matcher { m } )
require . NoError ( t , err )
2023-12-07 06:35:01 -05:00
require . Len ( t , ret , 1 , "select should have returned samples for a single series only" )
require . Len ( t , ret [ 0 ] . Exemplars , 3 , "didn't get expected two exemplars %d, %+v" , len ( ret [ 0 ] . Exemplars ) , ret )
2021-03-16 05:47:45 -04:00
}
// Test to ensure that even though a series matches more than one matcher from the
// query that it's exemplars are only included in the result a single time.
func TestSelectExemplar_DuplicateSeries ( t * testing . T ) {
2026-01-07 07:25:50 -05:00
exs , err := NewCircularExemplarStorage ( 4 , eMetrics , 0 )
2021-03-16 05:47:45 -04:00
require . NoError ( t , err )
es := exs . ( * CircularExemplarStorage )
e := exemplar . Exemplar {
2024-02-15 09:19:54 -05:00
Labels : labels . FromStrings ( "trace_id" , "qwerty" ) ,
2022-03-09 17:17:29 -05:00
Value : 0.1 ,
Ts : 12 ,
2021-03-16 05:47:45 -04:00
}
2022-03-09 17:17:29 -05:00
lName0 , lValue0 := "service" , "asdf"
lName1 , lValue1 := "cluster" , "us-central1"
l := labels . FromStrings ( lName0 , lValue0 , lName1 , lValue1 )
2021-03-16 05:47:45 -04:00
// Lets just assume somehow the PromQL expression generated two separate lists of matchers,
// both of which can select this particular series.
m := [ ] [ ] * labels . Matcher {
{
2022-03-09 17:17:29 -05:00
labels . MustNewMatcher ( labels . MatchEqual , lName0 , lValue0 ) ,
2021-03-16 05:47:45 -04:00
} ,
{
2022-03-09 17:17:29 -05:00
labels . MustNewMatcher ( labels . MatchEqual , lName1 , lValue1 ) ,
2021-03-16 05:47:45 -04:00
} ,
}
err = es . AddExemplar ( l , e )
require . NoError ( t , err , "adding exemplar failed" )
require . True ( t , reflect . DeepEqual ( es . exemplars [ 0 ] . exemplar , e ) , "exemplar was not stored correctly" )
ret , err := es . Select ( 0 , 100 , m ... )
require . NoError ( t , err )
2023-12-07 06:35:01 -05:00
require . Len ( t , ret , 1 , "select should have returned samples for a single series only" )
2021-03-16 05:47:45 -04:00
}
func TestIndexOverwrite ( t * testing . T ) {
2026-01-07 07:25:50 -05:00
exs , err := NewCircularExemplarStorage ( 2 , eMetrics , 0 )
2021-03-16 05:47:45 -04:00
require . NoError ( t , err )
es := exs . ( * CircularExemplarStorage )
2022-03-09 17:17:29 -05:00
l1 := labels . FromStrings ( "service" , "asdf" )
l2 := labels . FromStrings ( "service" , "qwer" )
2021-03-16 05:47:45 -04:00
err = es . AddExemplar ( l1 , exemplar . Exemplar { Value : 1 , Ts : 1 } )
require . NoError ( t , err )
err = es . AddExemplar ( l2 , exemplar . Exemplar { Value : 2 , Ts : 2 } )
require . NoError ( t , err )
err = es . AddExemplar ( l2 , exemplar . Exemplar { Value : 3 , Ts : 3 } )
require . NoError ( t , err )
// Ensure index GC'ing is taking place, there should no longer be any
// index entry for series l1 since we just wrote two exemplars for series l2.
2022-01-06 05:28:58 -05:00
_ , ok := es . index [ string ( l1 . Bytes ( nil ) ) ]
2021-03-16 05:47:45 -04:00
require . False ( t , ok )
2022-01-06 05:28:58 -05:00
require . Equal ( t , & indexEntry { 1 , 0 , l2 } , es . index [ string ( l2 . Bytes ( nil ) ) ] )
2021-03-16 05:47:45 -04:00
err = es . AddExemplar ( l1 , exemplar . Exemplar { Value : 4 , Ts : 4 } )
require . NoError ( t , err )
2022-01-06 05:28:58 -05:00
i := es . index [ string ( l2 . Bytes ( nil ) ) ]
2021-05-05 13:51:16 -04:00
require . Equal ( t , & indexEntry { 0 , 0 , l2 } , i )
2021-03-16 05:47:45 -04:00
}
2021-07-20 00:52:57 -04:00
func TestResize ( t * testing . T ) {
testCases := [ ] struct {
name string
startSize int64
newCount int64
expectedSeries [ ] int
notExpectedSeries [ ] int
expectedMigrated int
} {
{
name : "Grow" ,
startSize : 100 ,
newCount : 200 ,
expectedSeries : [ ] int { 99 , 98 , 1 , 0 } ,
notExpectedSeries : [ ] int { 100 } ,
expectedMigrated : 100 ,
} ,
{
name : "Shrink" ,
startSize : 100 ,
newCount : 50 ,
expectedSeries : [ ] int { 99 , 98 , 50 } ,
notExpectedSeries : [ ] int { 49 , 1 , 0 } ,
expectedMigrated : 50 ,
} ,
{
2021-09-02 14:08:05 -04:00
name : "ShrinkToZero" ,
2021-07-20 00:52:57 -04:00
startSize : 100 ,
newCount : 0 ,
expectedSeries : [ ] int { } ,
notExpectedSeries : [ ] int { } ,
expectedMigrated : 0 ,
} ,
{
name : "Negative" ,
startSize : 100 ,
newCount : - 1 ,
expectedSeries : [ ] int { } ,
notExpectedSeries : [ ] int { } ,
expectedMigrated : 0 ,
} ,
{
name : "NegativeToNegative" ,
startSize : - 1 ,
newCount : - 2 ,
expectedSeries : [ ] int { } ,
notExpectedSeries : [ ] int { } ,
expectedMigrated : 0 ,
} ,
2021-09-02 14:08:05 -04:00
{
name : "GrowFromZero" ,
startSize : 0 ,
newCount : 10 ,
expectedSeries : [ ] int { } ,
notExpectedSeries : [ ] int { } ,
expectedMigrated : 0 ,
} ,
2021-07-20 00:52:57 -04:00
}
for _ , tc := range testCases {
t . Run ( tc . name , func ( t * testing . T ) {
2026-01-07 07:25:50 -05:00
exs , err := NewCircularExemplarStorage ( tc . startSize , eMetrics , 0 )
2021-07-20 00:52:57 -04:00
require . NoError ( t , err )
es := exs . ( * CircularExemplarStorage )
for i := 0 ; int64 ( i ) < tc . startSize ; i ++ {
err = es . AddExemplar ( labels . FromStrings ( "service" , strconv . Itoa ( i ) ) , exemplar . Exemplar {
Value : float64 ( i ) ,
2021-10-22 04:06:44 -04:00
Ts : int64 ( i ) ,
} )
2021-07-20 00:52:57 -04:00
require . NoError ( t , err )
}
2026-01-07 07:25:50 -05:00
if testing . Verbose ( ) {
t . Logf ( "Buffer[before-resize]:\n%s" , debugCircularBuffer ( es ) )
}
2021-07-20 00:52:57 -04:00
resized := es . Resize ( tc . newCount )
2026-01-07 07:25:50 -05:00
if testing . Verbose ( ) {
t . Logf ( "Buffer[after-resize]:\n%s" , debugCircularBuffer ( es ) )
}
2021-07-20 00:52:57 -04:00
require . Equal ( t , tc . expectedMigrated , resized )
q , err := es . Querier ( context . TODO ( ) )
require . NoError ( t , err )
matchers := [ ] * labels . Matcher { labels . MustNewMatcher ( labels . MatchEqual , "service" , "" ) }
for _ , expected := range tc . expectedSeries {
matchers [ 0 ] . Value = strconv . Itoa ( expected )
ex , err := q . Select ( 0 , math . MaxInt64 , matchers )
require . NoError ( t , err )
require . NotEmpty ( t , ex )
}
for _ , notExpected := range tc . notExpectedSeries {
matchers [ 0 ] . Value = strconv . Itoa ( notExpected )
ex , err := q . Select ( 0 , math . MaxInt64 , matchers )
require . NoError ( t , err )
require . Empty ( t , ex )
}
} )
}
}
2021-08-30 10:04:38 -04:00
func BenchmarkAddExemplar ( b * testing . B ) {
// We need to include these labels since we do length calculation
// before adding.
2024-02-15 09:19:54 -05:00
exLabels := labels . FromStrings ( "trace_id" , "89620921" )
2021-08-30 10:04:38 -04:00
2024-05-11 11:43:34 -04:00
for _ , capacity := range [ ] int { 1000 , 10000 , 100000 } {
for _ , n := range [ ] int { 10000 , 100000 , 1000000 } {
b . Run ( fmt . Sprintf ( "%d/%d" , n , capacity ) , func ( b * testing . B ) {
2025-11-04 00:13:49 -05:00
for b . Loop ( ) {
2024-05-11 11:43:34 -04:00
b . StopTimer ( )
2026-01-07 07:25:50 -05:00
exs , err := NewCircularExemplarStorage ( int64 ( capacity ) , eMetrics , 0 )
2024-05-11 11:43:34 -04:00
require . NoError ( b , err )
es := exs . ( * CircularExemplarStorage )
var l labels . Labels
b . StartTimer ( )
2025-08-27 08:38:54 -04:00
for i := range n {
2024-05-11 11:43:34 -04:00
if i % 100 == 0 {
l = labels . FromStrings ( "service" , strconv . Itoa ( i ) )
}
err = es . AddExemplar ( l , exemplar . Exemplar { Value : float64 ( i ) , Ts : int64 ( i ) , Labels : exLabels } )
if err != nil {
require . NoError ( b , err )
}
2022-01-06 05:28:58 -05:00
}
}
2024-05-11 11:43:34 -04:00
} )
}
2021-07-20 00:52:57 -04:00
}
}
2026-01-07 07:25:50 -05:00
func BenchmarkAddExemplar_OutOfOrder ( b * testing . B ) {
// We need to include these labels since we do length calculation
// before adding.
exLabels := labels . FromStrings ( "trace_id" , "89620921" )
const (
capacity = 5000
)
fillOneSeries := func ( es * CircularExemplarStorage ) {
for i := range capacity {
e := exemplar . Exemplar { Value : float64 ( i ) , Ts : int64 ( i ) , Labels : exLabels }
if err := es . AddExemplar ( exLabels , e ) ; err != nil {
panic ( err )
}
}
}
fillMultipleSeries := func ( es * CircularExemplarStorage ) {
for i := range capacity {
l := labels . FromStrings ( "service" , strconv . Itoa ( i ) )
e := exemplar . Exemplar { Value : float64 ( i ) , Ts : int64 ( i ) , Labels : l }
if err := es . AddExemplar ( l , e ) ; err != nil {
panic ( err )
}
}
}
outOfOrder := func ( ts * int64 , _ * labels . Labels ) {
switch * ts % 3 {
case 0 :
return
case 1 :
* ts = capacity - * ts
case 2 :
* ts = ( capacity - * ts ) + 100
}
}
reverseOrder := func ( ts * int64 , _ * labels . Labels ) {
* ts = capacity - * ts
}
multipleSeries := func ( f func ( * int64 , * labels . Labels ) ) func ( * int64 , * labels . Labels ) {
return func ( ts * int64 , l * labels . Labels ) {
f ( ts , l )
* l = labels . FromStrings ( "service" , strconv . Itoa ( int ( * ts ) ) )
}
}
for fillName , setup := range map [ string ] func ( es * CircularExemplarStorage ) {
"empty" : func ( * CircularExemplarStorage ) { } ,
"full-one" : fillOneSeries ,
"full-multiple" : fillMultipleSeries ,
} {
for orderName , forEach := range map [ string ] func ( ts * int64 , l * labels . Labels ) {
"in-order" : func ( * int64 , * labels . Labels ) { } ,
"reverse" : reverseOrder ,
"out-of-order" : outOfOrder ,
"multi-in-order" : multipleSeries ( func ( * int64 , * labels . Labels ) { } ) ,
"multi-reverse" : multipleSeries ( reverseOrder ) ,
"multi-out-of-order" : multipleSeries ( outOfOrder ) ,
} {
b . Run ( fmt . Sprintf ( "%s/%s" , fillName , orderName ) , func ( b * testing . B ) {
exs , err := NewCircularExemplarStorage ( int64 ( capacity ) , eMetrics , 100000 )
require . NoError ( b , err )
es := exs . ( * CircularExemplarStorage )
l := labels . FromStrings ( "service" , "0" )
setup ( es )
b . ResetTimer ( )
for b . Loop ( ) {
for i := range capacity {
ts := int64 ( i )
forEach ( & ts , & l )
err = es . AddExemplar ( l , exemplar . Exemplar { Value : float64 ( i ) , Ts : ts , Labels : l } )
if err != nil {
b . Fatalf ( "Failed to insert item %d %s: %v" , i , l , err )
}
}
}
} )
}
}
}
2021-07-20 00:52:57 -04:00
func BenchmarkResizeExemplars ( b * testing . B ) {
testCases := [ ] struct {
name string
startSize int64
endSize int64
numExemplars int
} {
{
name : "grow" ,
startSize : 100000 ,
endSize : 200000 ,
numExemplars : 150000 ,
} ,
{
name : "shrink" ,
startSize : 100000 ,
endSize : 50000 ,
numExemplars : 100000 ,
} ,
{
name : "grow" ,
startSize : 1000000 ,
endSize : 2000000 ,
numExemplars : 1500000 ,
} ,
{
name : "shrink" ,
startSize : 1000000 ,
endSize : 500000 ,
numExemplars : 1000000 ,
} ,
}
for _ , tc := range testCases {
2022-01-06 05:28:58 -05:00
b . Run ( fmt . Sprintf ( "%s-%d-to-%d" , tc . name , tc . startSize , tc . endSize ) , func ( b * testing . B ) {
2025-11-04 00:13:49 -05:00
for b . Loop ( ) {
2022-01-06 05:28:58 -05:00
b . StopTimer ( )
2026-01-07 07:25:50 -05:00
exs , err := NewCircularExemplarStorage ( tc . startSize , eMetrics , 0 )
2022-01-06 05:28:58 -05:00
require . NoError ( b , err )
es := exs . ( * CircularExemplarStorage )
2021-07-20 00:52:57 -04:00
2024-05-11 12:00:32 -04:00
var l labels . Labels
2022-01-06 05:28:58 -05:00
for i := 0 ; i < int ( float64 ( tc . startSize ) * float64 ( 1.5 ) ) ; i ++ {
2024-05-11 12:00:32 -04:00
if i % 100 == 0 {
l = labels . FromStrings ( "service" , strconv . Itoa ( i ) )
}
2021-07-20 00:52:57 -04:00
2022-01-06 05:28:58 -05:00
err = es . AddExemplar ( l , exemplar . Exemplar { Value : float64 ( i ) , Ts : int64 ( i ) } )
if err != nil {
require . NoError ( b , err )
}
}
b . StartTimer ( )
es . Resize ( tc . endSize )
}
2021-07-20 00:52:57 -04:00
} )
}
}
2024-10-29 05:40:46 -04:00
// TestCircularExemplarStorage_Concurrent_AddExemplar_Resize tries to provoke a data race between AddExemplar and Resize.
// Run with race detection enabled.
func TestCircularExemplarStorage_Concurrent_AddExemplar_Resize ( t * testing . T ) {
2026-01-07 07:25:50 -05:00
exs , err := NewCircularExemplarStorage ( 0 , eMetrics , 0 )
2024-10-29 05:40:46 -04:00
require . NoError ( t , err )
es := exs . ( * CircularExemplarStorage )
l := labels . FromStrings ( "service" , "asdf" )
e := exemplar . Exemplar {
Labels : labels . FromStrings ( "trace_id" , "qwerty" ) ,
Value : 0.1 ,
Ts : 1 ,
}
var wg sync . WaitGroup
wg . Add ( 1 )
t . Cleanup ( wg . Wait )
started := make ( chan struct { } )
go func ( ) {
defer wg . Done ( )
<- started
2025-08-27 08:38:54 -04:00
for range 100 {
2024-10-29 05:40:46 -04:00
require . NoError ( t , es . AddExemplar ( l , e ) )
}
} ( )
2025-08-27 08:38:54 -04:00
for i := range 100 {
2024-10-29 05:40:46 -04:00
es . Resize ( int64 ( i + 1 ) )
if i == 0 {
close ( started )
}
}
}
2026-01-07 07:25:50 -05:00
// debugCircularBuffer iterates all exemplars in the circular exemplar storage
// and returns them as a string. The textual representation contains index
// pointers and helps debugging exemplar storage.
func debugCircularBuffer ( ce * CircularExemplarStorage ) string {
var sb strings . Builder
for i , e := range ce . exemplars {
if e . ref == nil {
continue
}
2026-02-17 16:35:59 -05:00
fmt . Fprintf ( & sb , "i: %d, ts: %d, next: %d, prev: %d" ,
i , e . exemplar . Ts , e . next , e . prev )
2026-01-07 07:25:50 -05:00
for _ , idx := range ce . index {
if i == idx . newest {
sb . WriteString ( " <- newest " + idx . seriesLabels . String ( ) )
}
if i == idx . oldest {
sb . WriteString ( " <- oldest " + idx . seriesLabels . String ( ) )
}
}
sb . WriteString ( "\n" )
}
2026-02-17 16:35:59 -05:00
fmt . Fprintf ( & sb , "Next index: %d\n" , ce . nextIndex )
2026-01-07 07:25:50 -05:00
return sb . String ( )
}