diff --git a/model/histogram/float_histogram.go b/model/histogram/float_histogram.go index 009dcc82cd..6d7365b8b6 100644 --- a/model/histogram/float_histogram.go +++ b/model/histogram/float_histogram.go @@ -346,7 +346,7 @@ func (h *FloatHistogram) Add(other *FloatHistogram) (res *FloatHistogram, counte if h.UsesCustomBuckets() != other.UsesCustomBuckets() { return nil, false, ErrHistogramsIncompatibleSchema } - if h.UsesCustomBuckets() && !FloatBucketsMatch(h.CustomValues, other.CustomValues) { + if h.UsesCustomBuckets() && !CustomBucketBoundsMatch(h.CustomValues, other.CustomValues) { return nil, false, ErrHistogramsIncompatibleBounds } @@ -422,7 +422,7 @@ func (h *FloatHistogram) Sub(other *FloatHistogram) (res *FloatHistogram, counte if h.UsesCustomBuckets() != other.UsesCustomBuckets() { return nil, false, ErrHistogramsIncompatibleSchema } - if h.UsesCustomBuckets() && !FloatBucketsMatch(h.CustomValues, other.CustomValues) { + if h.UsesCustomBuckets() && !CustomBucketBoundsMatch(h.CustomValues, other.CustomValues) { return nil, false, ErrHistogramsIncompatibleBounds } @@ -500,7 +500,7 @@ func (h *FloatHistogram) Equals(h2 *FloatHistogram) bool { } if h.UsesCustomBuckets() { - if !FloatBucketsMatch(h.CustomValues, h2.CustomValues) { + if !CustomBucketBoundsMatch(h.CustomValues, h2.CustomValues) { return false } } @@ -513,14 +513,14 @@ func (h *FloatHistogram) Equals(h2 *FloatHistogram) bool { if !spansMatch(h.NegativeSpans, h2.NegativeSpans) { return false } - if !FloatBucketsMatch(h.NegativeBuckets, h2.NegativeBuckets) { + if !floatBucketsMatch(h.NegativeBuckets, h2.NegativeBuckets) { return false } if !spansMatch(h.PositiveSpans, h2.PositiveSpans) { return false } - if !FloatBucketsMatch(h.PositiveBuckets, h2.PositiveBuckets) { + if !floatBucketsMatch(h.PositiveBuckets, h2.PositiveBuckets) { return false } @@ -639,7 +639,7 @@ func (h *FloatHistogram) DetectReset(previous *FloatHistogram) bool { if h.Count < previous.Count { return true } - if h.UsesCustomBuckets() != previous.UsesCustomBuckets() || (h.UsesCustomBuckets() && !FloatBucketsMatch(h.CustomValues, previous.CustomValues)) { + if h.UsesCustomBuckets() != previous.UsesCustomBuckets() || (h.UsesCustomBuckets() && !CustomBucketBoundsMatch(h.CustomValues, previous.CustomValues)) { // Mark that something has changed or that the application has been restarted. However, this does // not matter so much since the change in schema will be handled directly in the chunks and PromQL // functions. @@ -1352,7 +1352,9 @@ func addBuckets( return spansA, bucketsA } -func FloatBucketsMatch(b1, b2 []float64) bool { +// floatBucketsMatch compares bucket values of two float histograms using binary float comparison +// and returns true if all values match. +func floatBucketsMatch(b1, b2 []float64) bool { if len(b1) != len(b2) { return false } diff --git a/model/histogram/generic.go b/model/histogram/generic.go index afd761bf4f..f95d3b464e 100644 --- a/model/histogram/generic.go +++ b/model/histogram/generic.go @@ -80,6 +80,20 @@ func IsKnownSchema(s int32) bool { return IsCustomBucketsSchema(s) || IsExponentialSchemaReserved(s) } +// CustomBucketBoundsMatch compares histogram custom bucket bounds (CustomValues) +// and returns true if all values match. +func CustomBucketBoundsMatch(c1, c2 []float64) bool { + if len(c1) != len(c2) { + return false + } + for i, c := range c1 { + if c != c2[i] { + return false + } + } + return true +} + // BucketCount is a type constraint for the count in a bucket, which can be // float64 (for type FloatHistogram) or uint64 (for type Histogram). type BucketCount interface { diff --git a/model/histogram/generic_test.go b/model/histogram/generic_test.go index b49adbcadf..1651830e9d 100644 --- a/model/histogram/generic_test.go +++ b/model/histogram/generic_test.go @@ -207,3 +207,71 @@ func TestReduceResolutionFloatHistogram(t *testing.T) { require.Equal(t, buckets, tc.buckets[:len(buckets)]) } } + +func TestCustomBucketBoundsMatch(t *testing.T) { + tests := []struct { + name string + c1, c2 []float64 + want bool + }{ + { + name: "both nil", + c1: nil, + c2: nil, + want: true, + }, + { + name: "both empty", + c1: []float64{}, + c2: []float64{}, + want: true, + }, + { + name: "one empty one non-empty", + c1: []float64{}, + c2: []float64{1.0}, + want: false, + }, + { + name: "different lengths", + c1: []float64{1.0, 2.0}, + c2: []float64{1.0, 2.0, 3.0}, + want: false, + }, + { + name: "same single value", + c1: []float64{1.5}, + c2: []float64{1.5}, + want: true, + }, + { + name: "different single value", + c1: []float64{1.5}, + c2: []float64{2.5}, + want: false, + }, + { + name: "same multiple values", + c1: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + c2: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + want: true, + }, + { + name: "different values", + c1: []float64{1.0, 2.1, 3.0}, + c2: []float64{1.0, 2.0, 3.0}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := CustomBucketBoundsMatch(tt.c1, tt.c2) + require.Equal(t, tt.want, got) + + // Test commutativity (should be symmetric) + gotReverse := CustomBucketBoundsMatch(tt.c2, tt.c1) + require.Equal(t, got, gotReverse) + }) + } +} diff --git a/model/histogram/histogram.go b/model/histogram/histogram.go index 35c9797155..a7d9ce80f0 100644 --- a/model/histogram/histogram.go +++ b/model/histogram/histogram.go @@ -255,7 +255,7 @@ func (h *Histogram) Equals(h2 *Histogram) bool { } if h.UsesCustomBuckets() { - if !FloatBucketsMatch(h.CustomValues, h2.CustomValues) { + if !CustomBucketBoundsMatch(h.CustomValues, h2.CustomValues) { return false } } diff --git a/promql/promqltest/test.go b/promql/promqltest/test.go index 9c420b1ddc..41d8cdde20 100644 --- a/promql/promqltest/test.go +++ b/promql/promqltest/test.go @@ -1139,7 +1139,7 @@ func compareNativeHistogram(exp, cur *histogram.FloatHistogram) bool { } if exp.UsesCustomBuckets() { - if !histogram.FloatBucketsMatch(exp.CustomValues, cur.CustomValues) { + if !histogram.CustomBucketBoundsMatch(exp.CustomValues, cur.CustomValues) { return false } } diff --git a/tsdb/chunkenc/float_histogram.go b/tsdb/chunkenc/float_histogram.go index d80b1d9bcc..f9baa1df9c 100644 --- a/tsdb/chunkenc/float_histogram.go +++ b/tsdb/chunkenc/float_histogram.go @@ -258,7 +258,7 @@ func (a *FloatHistogramAppender) appendable(h *histogram.FloatHistogram) ( return } - if histogram.IsCustomBucketsSchema(h.Schema) && !histogram.FloatBucketsMatch(h.CustomValues, a.customValues) { + if histogram.IsCustomBucketsSchema(h.Schema) && !histogram.CustomBucketBoundsMatch(h.CustomValues, a.customValues) { counterReset = true return } @@ -495,7 +495,7 @@ func (a *FloatHistogramAppender) appendableGauge(h *histogram.FloatHistogram) ( return } - if histogram.IsCustomBucketsSchema(h.Schema) && !histogram.FloatBucketsMatch(h.CustomValues, a.customValues) { + if histogram.IsCustomBucketsSchema(h.Schema) && !histogram.CustomBucketBoundsMatch(h.CustomValues, a.customValues) { return } diff --git a/tsdb/chunkenc/histogram.go b/tsdb/chunkenc/histogram.go index 9c433fc5e5..0488c6554d 100644 --- a/tsdb/chunkenc/histogram.go +++ b/tsdb/chunkenc/histogram.go @@ -294,7 +294,7 @@ func (a *HistogramAppender) appendable(h *histogram.Histogram) ( return } - if histogram.IsCustomBucketsSchema(h.Schema) && !histogram.FloatBucketsMatch(h.CustomValues, a.customValues) { + if histogram.IsCustomBucketsSchema(h.Schema) && !histogram.CustomBucketBoundsMatch(h.CustomValues, a.customValues) { counterResetHint = CounterReset return } @@ -532,7 +532,7 @@ func (a *HistogramAppender) appendableGauge(h *histogram.Histogram) ( return } - if histogram.IsCustomBucketsSchema(h.Schema) && !histogram.FloatBucketsMatch(h.CustomValues, a.customValues) { + if histogram.IsCustomBucketsSchema(h.Schema) && !histogram.CustomBucketBoundsMatch(h.CustomValues, a.customValues) { return }