From 2c3e9e2e8723bf0edb4e82e89c344b1e17bbeef2 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Sat, 16 May 2015 13:33:03 +0200 Subject: [PATCH 1/3] Extract AND operation into own eval method. --- promql/engine.go | 58 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 7b53f661d..917661208 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -592,6 +592,9 @@ func (ev *evaluator) eval(expr Expr) Value { } case lt == ExprVector && rt == ExprVector: + if e.Op == itemLAND { + return ev.vectorAnd(lhs.(Vector), rhs.(Vector), e.VectorMatching) + } return ev.vectorBinop(e.Op, lhs.(Vector), rhs.(Vector), e.VectorMatching) case lt == ExprVector && rt == ExprScalar: @@ -698,6 +701,38 @@ func (ev *evaluator) matrixSelectorBounds(node *MatrixSelector) Matrix { return Matrix(sampleStreams) } +func (ev *evaluator) vectorAnd(lhs, rhs Vector, matching *VectorMatching) Vector { + if matching.Card != CardManyToMany { + panic("logical operations must always be many-to-many matching") + } + // If no matching labels are specified, match by all labels. + signature := func(m clientmodel.COWMetric) uint64 { + return clientmodel.SignatureForLabels(m.Metric, matching.On) + } + if len(matching.On) == 0 { + signature = func(m clientmodel.COWMetric) uint64 { + m.Delete(clientmodel.MetricNameLabel) + return uint64(m.Metric.Fingerprint()) + } + } + + var result Vector + // The set of signatures for the right-hand side vector. + rightSigs := map[uint64]struct{}{} + // Add all rhs samples to a map so we can easily find matches later. + for _, rs := range rhs { + rightSigs[signature(rs.Metric)] = struct{}{} + } + + for _, ls := range lhs { + // If there's a matching entry in the right-hand side vector, add the sample. + if _, ok := rightSigs[signature(ls.Metric)]; ok { + result = append(result, ls) + } + } + return result +} + // vectorBinop evaluates a binary operation between two vector values. func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorMatching) Vector { result := make(Vector, 0, len(rhs)) @@ -748,21 +783,16 @@ func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorM var value clientmodel.SampleValue var keep bool - if op == itemLAND { - value = ls.Value - keep = true - } else { - if _, exists := added[hash]; matching.Card == CardOneToOne && exists { - // Many-to-one matching must be explicit. - ev.errorf("many-to-one matching must be explicit") - } - // Account for potentially swapped sidedness. - vl, vr := ls.Value, rs.Value - if matching.Card == CardOneToMany { - vl, vr = vr, vl - } - value, keep = vectorElemBinop(op, vl, vr) + if _, exists := added[hash]; matching.Card == CardOneToOne && exists { + // Many-to-one matching must be explicit. + ev.errorf("many-to-one matching must be explicit") } + // Account for potentially swapped sidedness. + vl, vr := ls.Value, rs.Value + if matching.Card == CardOneToMany { + vl, vr = vr, vl + } + value, keep = vectorElemBinop(op, vl, vr) if keep { metric := resultMetric(op, ls, rs, matching) From 8a109e061b4815b778129ab2e4b877e9e02a9228 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Sat, 16 May 2015 14:00:11 +0200 Subject: [PATCH 2/3] Extract OR operation into own eval method. --- promql/engine.go | 53 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 917661208..4eb647d5c 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -595,6 +595,9 @@ func (ev *evaluator) eval(expr Expr) Value { if e.Op == itemLAND { return ev.vectorAnd(lhs.(Vector), rhs.(Vector), e.VectorMatching) } + if e.Op == itemLOR { + return ev.vectorOr(lhs.(Vector), rhs.(Vector), e.VectorMatching) + } return ev.vectorBinop(e.Op, lhs.(Vector), rhs.(Vector), e.VectorMatching) case lt == ExprVector && rt == ExprScalar: @@ -733,6 +736,37 @@ func (ev *evaluator) vectorAnd(lhs, rhs Vector, matching *VectorMatching) Vector return result } +func (ev *evaluator) vectorOr(lhs, rhs Vector, matching *VectorMatching) Vector { + if matching.Card != CardManyToMany { + panic("logical operations must always be many-to-many matching") + } + // If no matching labels are specified, match by all labels. + signature := func(m clientmodel.COWMetric) uint64 { + return clientmodel.SignatureForLabels(m.Metric, matching.On) + } + if len(matching.On) == 0 { + signature = func(m clientmodel.COWMetric) uint64 { + m.Delete(clientmodel.MetricNameLabel) + return uint64(m.Metric.Fingerprint()) + } + } + + var result Vector + leftSigs := map[uint64]struct{}{} + // Add everything from the left-hand-side vector. + for _, ls := range lhs { + leftSigs[signature(ls.Metric)] = struct{}{} + result = append(result, ls) + } + // Add all right-hand side elements which have not been added from the left-hand side. + for _, rs := range rhs { + if _, ok := leftSigs[signature(rs.Metric)]; !ok { + result = append(result, rs) + } + } + return result +} + // vectorBinop evaluates a binary operation between two vector values. func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorMatching) Vector { result := make(Vector, 0, len(rhs)) @@ -755,7 +789,7 @@ func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorM // The rhs is guaranteed to be the 'one' side. Having multiple samples // with the same hash means that the matching is many-to-many, // which is not supported. - if _, found := rm[hash]; matching.Card != CardManyToMany && found { + if _, found := rm[hash]; found { // Many-to-many matching not allowed. ev.errorf("many-to-many matching not allowed") } @@ -768,13 +802,6 @@ func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorM // the binary operation. for _, ls := range lhs { hash := hashForMetric(ls.Metric.Metric, matching.On) - // Any lhs sample we encounter in an OR operation belongs to the result. - if op == itemLOR { - ls.Metric = resultMetric(op, ls, nil, matching) - result = append(result, ls) - added[hash] = nil // Ensure matching rhs sample is not added later. - continue - } rs, found := rm[hash] // Look for a match in the rhs vector. if !found { @@ -820,16 +847,6 @@ func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorM } } - // Add all remaining samples in the rhs in an OR operation if they - // have not been matched up with a lhs sample. - if op == itemLOR { - for hash, rs := range rm { - if _, exists := added[hash]; !exists { - rs.Metric = resultMetric(op, rs, nil, matching) - result = append(result, rs) - } - } - } return result } From ce487f763efd73398b767c5625b4b5265ef001e3 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Sat, 16 May 2015 16:35:52 +0200 Subject: [PATCH 3/3] Simplify vector binary evaluation logic --- promql/engine.go | 227 ++++++++++++++++++++--------------------------- 1 file changed, 95 insertions(+), 132 deletions(-) diff --git a/promql/engine.go b/promql/engine.go index 4eb647d5c..2a206da65 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -592,14 +592,14 @@ func (ev *evaluator) eval(expr Expr) Value { } case lt == ExprVector && rt == ExprVector: - if e.Op == itemLAND { + switch e.Op { + case itemLAND: return ev.vectorAnd(lhs.(Vector), rhs.(Vector), e.VectorMatching) - } - if e.Op == itemLOR { + case itemLOR: return ev.vectorOr(lhs.(Vector), rhs.(Vector), e.VectorMatching) + default: + return ev.vectorBinop(e.Op, lhs.(Vector), rhs.(Vector), e.VectorMatching) } - return ev.vectorBinop(e.Op, lhs.(Vector), rhs.(Vector), e.VectorMatching) - case lt == ExprVector && rt == ExprScalar: return ev.vectorScalarBinop(e.Op, lhs.(Vector), rhs.(*Scalar), false) @@ -709,27 +709,19 @@ func (ev *evaluator) vectorAnd(lhs, rhs Vector, matching *VectorMatching) Vector panic("logical operations must always be many-to-many matching") } // If no matching labels are specified, match by all labels. - signature := func(m clientmodel.COWMetric) uint64 { - return clientmodel.SignatureForLabels(m.Metric, matching.On) - } - if len(matching.On) == 0 { - signature = func(m clientmodel.COWMetric) uint64 { - m.Delete(clientmodel.MetricNameLabel) - return uint64(m.Metric.Fingerprint()) - } - } + sigf := signatureFunc(matching.On...) var result Vector // The set of signatures for the right-hand side vector. rightSigs := map[uint64]struct{}{} // Add all rhs samples to a map so we can easily find matches later. for _, rs := range rhs { - rightSigs[signature(rs.Metric)] = struct{}{} + rightSigs[sigf(rs.Metric)] = struct{}{} } for _, ls := range lhs { // If there's a matching entry in the right-hand side vector, add the sample. - if _, ok := rightSigs[signature(ls.Metric)]; ok { + if _, ok := rightSigs[sigf(ls.Metric)]; ok { result = append(result, ls) } } @@ -740,114 +732,143 @@ func (ev *evaluator) vectorOr(lhs, rhs Vector, matching *VectorMatching) Vector if matching.Card != CardManyToMany { panic("logical operations must always be many-to-many matching") } - // If no matching labels are specified, match by all labels. - signature := func(m clientmodel.COWMetric) uint64 { - return clientmodel.SignatureForLabels(m.Metric, matching.On) - } - if len(matching.On) == 0 { - signature = func(m clientmodel.COWMetric) uint64 { - m.Delete(clientmodel.MetricNameLabel) - return uint64(m.Metric.Fingerprint()) - } - } + sigf := signatureFunc(matching.On...) var result Vector leftSigs := map[uint64]struct{}{} // Add everything from the left-hand-side vector. for _, ls := range lhs { - leftSigs[signature(ls.Metric)] = struct{}{} + leftSigs[sigf(ls.Metric)] = struct{}{} result = append(result, ls) } // Add all right-hand side elements which have not been added from the left-hand side. for _, rs := range rhs { - if _, ok := leftSigs[signature(rs.Metric)]; !ok { + if _, ok := leftSigs[sigf(rs.Metric)]; !ok { result = append(result, rs) } } return result } -// vectorBinop evaluates a binary operation between two vector values. +// vectorBinop evaluates a binary operation between two vector, excluding AND and OR. func (ev *evaluator) vectorBinop(op itemType, lhs, rhs Vector, matching *VectorMatching) Vector { - result := make(Vector, 0, len(rhs)) + if matching.Card == CardManyToMany { + panic("many-to-many only allowed for AND and OR") + } + var ( + result = Vector{} + sigf = signatureFunc(matching.On...) + resultLabels = append(matching.On, matching.Include...) + ) + // The control flow below handles one-to-one or many-to-one matching. // For one-to-many, swap sidedness and account for the swap when calculating // values. if matching.Card == CardOneToMany { lhs, rhs = rhs, lhs } + // All samples from the rhs hashed by the matching label/values. - rm := map[uint64]*Sample{} - // Maps the hash of the label values used for matching to the hashes of the label - // values of the include labels (if any). It is used to keep track of already - // inserted samples. - added := map[uint64][]uint64{} + rightSigs := map[uint64]*Sample{} // Add all rhs samples to a map so we can easily find matches later. for _, rs := range rhs { - hash := hashForMetric(rs.Metric.Metric, matching.On) + sig := sigf(rs.Metric) // The rhs is guaranteed to be the 'one' side. Having multiple samples - // with the same hash means that the matching is many-to-many, - // which is not supported. - if _, found := rm[hash]; found { + // with the same signature means that the matching is many-to-many. + if _, found := rightSigs[sig]; found { // Many-to-many matching not allowed. - ev.errorf("many-to-many matching not allowed") + ev.errorf("many-to-many matching not allowed: matching labels must be unique on one side") } - // In many-to-many matching the entry is simply overwritten. It can thus only - // be used to check whether any matching rhs entry exists but not retrieve them all. - rm[hash] = rs + rightSigs[sig] = rs } + // Tracks the match-signature. For one-to-one operations the value is nil. For many-to-one + // the value is a set of signatures to detect duplicated result elements. + matchedSigs := map[uint64]map[uint64]struct{}{} + // For all lhs samples find a respective rhs sample and perform // the binary operation. for _, ls := range lhs { - hash := hashForMetric(ls.Metric.Metric, matching.On) + sig := sigf(ls.Metric) - rs, found := rm[hash] // Look for a match in the rhs vector. + rs, found := rightSigs[sig] // Look for a match in the rhs vector. if !found { continue } - var value clientmodel.SampleValue - var keep bool - if _, exists := added[hash]; matching.Card == CardOneToOne && exists { - // Many-to-one matching must be explicit. - ev.errorf("many-to-one matching must be explicit") - } // Account for potentially swapped sidedness. vl, vr := ls.Value, rs.Value if matching.Card == CardOneToMany { vl, vr = vr, vl } - value, keep = vectorElemBinop(op, vl, vr) + value, keep := vectorElemBinop(op, vl, vr) + if !keep { + continue + } + metric := resultMetric(ls.Metric, op, resultLabels...) - if keep { - metric := resultMetric(op, ls, rs, matching) - // Check if the same label set has been added for a many-to-one matching before. - if matching.Card == CardManyToOne || matching.Card == CardOneToMany { - insHash := clientmodel.SignatureForLabels(metric.Metric, matching.Include) - if ihs, exists := added[hash]; exists { - for _, ih := range ihs { - if ih == insHash { - ev.errorf("metric with label set has already been matched") - } - } - added[hash] = append(ihs, insHash) - } else { - added[hash] = []uint64{insHash} - } + insertedSigs, exists := matchedSigs[sig] + if matching.Card == CardOneToOne { + if exists { + ev.errorf("multiple matches for labels: many-to-one matching must be explicit (group_left/group_right)") } - ns := &Sample{ - Metric: metric, - Value: value, - Timestamp: ev.Timestamp, + matchedSigs[sig] = nil // Set existance to true. + } else { + // In many-to-one matching the grouping labels have to ensure a unique metric + // for the result vector. Check whether those labels have already been added for + // the same matching labels. + insertSig := clientmodel.SignatureForLabels(metric.Metric, matching.Include) + if !exists { + insertedSigs = map[uint64]struct{}{} + matchedSigs[sig] = insertedSigs + } else if _, duplicate := insertedSigs[insertSig]; duplicate { + ev.errorf("multiple matches for labels: grouping labels must ensure unique matches") } - result = append(result, ns) - added[hash] = added[hash] // Set existance to true. + insertedSigs[insertSig] = struct{}{} + } + + result = append(result, &Sample{ + Metric: metric, + Value: value, + Timestamp: ev.Timestamp, + }) + } + return result +} + +// signatureFunc returns a function that calculates the signature for a metric +// based on the provided labels. +func signatureFunc(labels ...clientmodel.LabelName) func(m clientmodel.COWMetric) uint64 { + if len(labels) == 0 { + return func(m clientmodel.COWMetric) uint64 { + m.Delete(clientmodel.MetricNameLabel) + return uint64(m.Metric.Fingerprint()) } } + return func(m clientmodel.COWMetric) uint64 { + return clientmodel.SignatureForLabels(m.Metric, labels) + } +} - return result +// resultMetric returns the metric for the given sample(s) based on the vector +// binary operation and the matching options. +func resultMetric(met clientmodel.COWMetric, op itemType, labels ...clientmodel.LabelName) clientmodel.COWMetric { + if len(labels) == 0 { + if shouldDropMetricName(op) { + met.Delete(clientmodel.MetricNameLabel) + } + return met + } + // As we definitly write, creating a new metric is the easiest solution. + m := clientmodel.Metric{} + for _, ln := range labels { + // Included labels from the `group_x` modifier are taken from the "many"-side. + if v, ok := met.Metric[ln]; ok { + m[ln] = v + } + } + return clientmodel.COWMetric{Metric: m, Copied: false} } // vectorScalarBinop evaluates a binary operation between a vector and a scalar. @@ -1065,64 +1086,6 @@ func shouldDropMetricName(op itemType) bool { } } -// resultMetric returns the metric for the given sample(s) based on the vector -// binary operation and the matching options. -func resultMetric(op itemType, ls, rs *Sample, matching *VectorMatching) clientmodel.COWMetric { - if len(matching.On) == 0 || op == itemLOR || op == itemLAND { - if shouldDropMetricName(op) { - ls.Metric.Delete(clientmodel.MetricNameLabel) - } - return ls.Metric - } - - m := clientmodel.Metric{} - for _, ln := range matching.On { - m[ln] = ls.Metric.Metric[ln] - } - - for _, ln := range matching.Include { - // Included labels from the `group_x` modifier are taken from the "many"-side. - v, ok := ls.Metric.Metric[ln] - if ok { - m[ln] = v - } - } - return clientmodel.COWMetric{false, m} -} - -// hashForMetric calculates a hash value for the given metric based on the matching -// options for the binary operation. -func hashForMetric(metric clientmodel.Metric, withLabels clientmodel.LabelNames) uint64 { - var labels clientmodel.LabelNames - - if len(withLabels) > 0 { - var match bool - for _, ln := range withLabels { - if _, match = metric[ln]; !match { - break - } - } - // If the metric does not contain the labels to match on, build the hash - // over the whole metric to give it a unique hash. - if !match { - labels = make(clientmodel.LabelNames, 0, len(metric)) - for ln := range metric { - labels = append(labels, ln) - } - } else { - labels = withLabels - } - } else { - labels = make(clientmodel.LabelNames, 0, len(metric)) - for ln := range metric { - if ln != clientmodel.MetricNameLabel { - labels = append(labels, ln) - } - } - } - return clientmodel.SignatureForLabels(metric, labels) -} - // chooseClosestSample chooses the closest sample of a list of samples // surrounding a given target time. If samples are found both before and after // the target time, the sample value is interpolated between these. Otherwise,