@@ -24,6 +24,7 @@ import (
2424 "github.com/thanos-io/promql-engine/engine"
2525 "github.com/thanos-io/promql-engine/execution/model"
2626 "github.com/thanos-io/promql-engine/execution/warnings"
27+ "github.com/thanos-io/promql-engine/extlabels"
2728 "github.com/thanos-io/promql-engine/logicalplan"
2829 "github.com/thanos-io/promql-engine/query"
2930 "github.com/thanos-io/promql-engine/storage/prometheus"
@@ -2314,6 +2315,21 @@ or
23142315 end : time .UnixMilli (124000 ),
23152316 step : 15 * time .Second ,
23162317 },
2318+ {
2319+ name : "fuzz native histogram approx float comparison" ,
2320+ load : `load 2m
2321+ http_request_duration_seconds{pod="nginx-1"} {{schema:0 count:30 sum:14.00 buckets:[27 2 1]}}+{{schema:0 count:30 buckets:[27 2 1]}}x20
2322+ http_request_duration_seconds{pod="nginx-2"} {{schema:-2 count:58 sum:4368.00 buckets:[54 2 2]}}+{{schema:-2 count:58 buckets:[54 2 2]}}x30` ,
2323+ query : `
2324+ -(
2325+ -{__name__="http_request_duration_seconds"}
2326+ /
2327+ histogram_stdvar({__name__="http_request_duration_seconds"})
2328+ )` ,
2329+ start : time .UnixMilli (83000 ),
2330+ end : time .UnixMilli (160000 ),
2331+ step : time .Minute + 16 * time .Second ,
2332+ },
23172333 }
23182334
23192335 disableOptimizerOpts := []bool {true , false }
@@ -5995,12 +6011,91 @@ func (b samplesByLabels) Len() int { return len(b) }
59956011func (b samplesByLabels ) Swap (i , j int ) { b [i ], b [j ] = b [j ], b [i ] }
59966012func (b samplesByLabels ) Less (i , j int ) bool { return labels .Compare (b [i ].Metric , b [j ].Metric ) < 0 }
59976013
6014+ const epsilon = 1e-6
6015+ const fraction = 1e-10
6016+
6017+ func floatsMatch (f1 , f2 []float64 ) bool {
6018+ if len (f1 ) != len (f2 ) {
6019+ return false
6020+ }
6021+ for i , f := range f1 {
6022+ if ! cmp .Equal (f , f2 [i ], cmpopts .EquateNaNs (), cmpopts .EquateApprox (fraction , epsilon )) {
6023+ return false
6024+ }
6025+ }
6026+ return true
6027+ }
6028+
6029+ // spansMatch returns true if both spans represent the same bucket layout
6030+ // after combining zero length spans with the next non-zero length span.
6031+ // Copied from: https://github.com/prometheus/prometheus/blob/3d245e31d31774f62ff18c36039315fa55fe252c/model/histogram/histogram.go#L287
6032+ func spansMatch (s1 , s2 []histogram.Span ) bool {
6033+ if len (s1 ) == 0 && len (s2 ) == 0 {
6034+ return true
6035+ }
6036+
6037+ s1idx , s2idx := 0 , 0
6038+ for {
6039+ if s1idx >= len (s1 ) {
6040+ return allEmptySpans (s2 [s2idx :])
6041+ }
6042+ if s2idx >= len (s2 ) {
6043+ return allEmptySpans (s1 [s1idx :])
6044+ }
6045+
6046+ currS1 , currS2 := s1 [s1idx ], s2 [s2idx ]
6047+ s1idx ++
6048+ s2idx ++
6049+ if currS1 .Length == 0 {
6050+ // This span is zero length, so we add consecutive such spans
6051+ // until we find a non-zero span.
6052+ for ; s1idx < len (s1 ) && s1 [s1idx ].Length == 0 ; s1idx ++ {
6053+ currS1 .Offset += s1 [s1idx ].Offset
6054+ }
6055+ if s1idx < len (s1 ) {
6056+ currS1 .Offset += s1 [s1idx ].Offset
6057+ currS1 .Length = s1 [s1idx ].Length
6058+ s1idx ++
6059+ }
6060+ }
6061+ if currS2 .Length == 0 {
6062+ // This span is zero length, so we add consecutive such spans
6063+ // until we find a non-zero span.
6064+ for ; s2idx < len (s2 ) && s2 [s2idx ].Length == 0 ; s2idx ++ {
6065+ currS2 .Offset += s2 [s2idx ].Offset
6066+ }
6067+ if s2idx < len (s2 ) {
6068+ currS2 .Offset += s2 [s2idx ].Offset
6069+ currS2 .Length = s2 [s2idx ].Length
6070+ s2idx ++
6071+ }
6072+ }
6073+
6074+ if currS1 .Length == 0 && currS2 .Length == 0 {
6075+ // The last spans of both set are zero length. Previous spans match.
6076+ return true
6077+ }
6078+
6079+ if currS1 .Offset != currS2 .Offset || currS1 .Length != currS2 .Length {
6080+ return false
6081+ }
6082+ }
6083+ }
6084+
6085+ func allEmptySpans (s []histogram.Span ) bool {
6086+ for _ , ss := range s {
6087+ if ss .Length > 0 {
6088+ return false
6089+ }
6090+ }
6091+ return true
6092+ }
6093+
59986094var (
59996095 // comparer should be used to compare promql results between engines.
60006096 comparer = cmp .Comparer (func (x , y * promql.Result ) bool {
60016097 compareFloats := func (l , r float64 ) bool {
6002- const epsilon = 1e-6
6003- return cmp .Equal (l , r , cmpopts .EquateNaNs (), cmpopts .EquateApprox (0 , epsilon ))
6098+ return cmp .Equal (l , r , cmpopts .EquateNaNs (), cmpopts .EquateApprox (fraction , epsilon ))
60046099 }
60056100 compareHistograms := func (l , r * histogram.FloatHistogram ) bool {
60066101 if l == nil && r == nil {
@@ -6011,7 +6106,39 @@ var (
60116106 return false
60126107 }
60136108
6014- return l .Equals (r )
6109+ // Copied from https://github.com/prometheus/prometheus/blob/3d245e31d31774f62ff18c36039315fa55fe252c/model/histogram/float_histogram.go#L471
6110+ // and extended to use approx comparison instead of exact match.
6111+ if l .Schema != r .Schema || ! compareFloats (l .Count , r .Count ) || ! compareFloats (l .Sum , r .Sum ) {
6112+ return false
6113+ }
6114+
6115+ if l .UsesCustomBuckets () {
6116+ if ! floatsMatch (l .CustomValues , r .CustomValues ) {
6117+ return false
6118+ }
6119+ }
6120+
6121+ if l .ZeroThreshold != r .ZeroThreshold || ! compareFloats (l .ZeroCount , r .ZeroCount ) {
6122+ return false
6123+ }
6124+
6125+ if ! spansMatch (l .NegativeSpans , r .NegativeSpans ) {
6126+ return false
6127+ }
6128+
6129+ if ! floatsMatch (l .NegativeBuckets , r .NegativeBuckets ) {
6130+ return false
6131+ }
6132+
6133+ if ! spansMatch (l .PositiveSpans , r .PositiveSpans ) {
6134+ return false
6135+ }
6136+
6137+ if ! floatsMatch (l .PositiveBuckets , r .PositiveBuckets ) {
6138+ return false
6139+ }
6140+
6141+ return true
60156142 }
60166143 compareAnnotations := func (l , r annotations.Annotations ) bool {
60176144 // TODO: discard promql annotations for now, once we support them we should add them back
@@ -6038,14 +6165,65 @@ var (
60386165 }
60396166 return true
60406167 }
6168+ compareValueMetrics := func (l , r labels.Labels ) (valueMetric bool , equals bool ) {
6169+ // For count_value() float values embedded in the labels should be extracted out and compared separately from other labels.
6170+ lLabels := l .Copy ()
6171+ rLabels := r .Copy ()
6172+ var (
6173+ lVal , rVal string
6174+ lFloat , rFloat float64
6175+ err error
6176+ )
6177+
6178+ if lVal = lLabels .Get ("value" ); lVal == "" {
6179+ return false , false
6180+ }
6181+
6182+ if rVal = rLabels .Get ("value" ); rVal == "" {
6183+ return false , false
6184+ }
6185+
6186+ if lFloat , err = strconv .ParseFloat (lVal , 64 ); err != nil {
6187+ return false , false
6188+ }
6189+ if rFloat , err = strconv .ParseFloat (rVal , 64 ); err != nil {
6190+ return false , false
6191+ }
6192+
6193+ // Exclude the value label in comparison.
6194+ lLabels = lLabels .MatchLabels (false , "value" )
6195+ rLabels = rLabels .MatchLabels (false , "value" )
6196+
6197+ if ! labels .Equal (lLabels , rLabels ) {
6198+ return false , false
6199+ }
6200+
6201+ return true , compareFloats (lFloat , rFloat )
6202+ }
60416203 compareMetrics := func (l , r labels.Labels ) bool {
6204+ if valueMetric , equals := compareValueMetrics (l , r ); valueMetric {
6205+ return equals
6206+ }
60426207 return l .Hash () == r .Hash ()
60436208 }
60446209
6045- if x .Err != nil && y .Err != nil {
6046- return cmp .Equal (x .Err .Error (), y .Err .Error ())
6047- } else if x .Err != nil || y .Err != nil {
6048- return false
6210+ compareErrors := func (l , r error ) (stop bool , result bool ) {
6211+ if l == nil && r == nil {
6212+ return false , true
6213+ }
6214+ if l != nil && r != nil {
6215+ return true , l .Error () == r .Error ()
6216+ }
6217+ err := l
6218+ if err == nil {
6219+ err = r
6220+ }
6221+ // Thanos engine handles duplicate label check differently than Prometheus engine.
6222+ return true , err .Error () == extlabels .ErrDuplicateLabelSet .Error ()
6223+ }
6224+
6225+ if stop , result := compareErrors (x .Err , y .Err ); stop {
6226+ return result
60496227 }
60506228
60516229 if ! compareAnnotations (x .Warnings , y .Warnings ) {
0 commit comments