Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions asap-precompute-go/matcher_lifecycle_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package precompute

import "testing"

func TestRuntimeConfigOwnsCompiledMatchers(t *testing.T) {
source := &PrecomputeConfig{AggID: 1, Matchers: []LabelMatcher{{Name: "zone", Op: MatchRegex, Value: "a|b"}}}
p := New(source, newFakeFactory(), &fakeObserver{}).(*precompute)
if p.activeConfig().Matchers[0].compiled == nil {
t.Fatal("runtime matcher was not compiled at installation")
}
source.Matchers[0].Value = "never"
if !p.activeConfig().Matches(&Observation{Labels: []KeyValue{{Key: "zone", Value: "a"}}}) {
t.Fatal("caller mutation changed installed matcher")
}
if err := ValidateMatchers([]LabelMatcher{{Op: MatchRegex, Value: "["}}); err == nil {
t.Fatal("invalid regexp accepted")
}
}

func TestInvalidMatcherConfigFailsClosed(t *testing.T) {
invalid := &PrecomputeConfig{AggID: 1, Matchers: []LabelMatcher{{Op: MatchRegex, Value: "["}}}
p := New(invalid, newFakeFactory(), &fakeObserver{}).(*precompute)
if p.activeConfig() != nil {
t.Fatal("invalid initial config became active")
}
valid := &PrecomputeConfig{AggID: 1, Matchers: []LabelMatcher{{Op: MatchEqual, Value: "m"}}}
p = New(valid, newFakeFactory(), &fakeObserver{}).(*precompute)
p.UpdateConfig(&PrecomputeConfigSet{Configs: []PrecomputeConfig{*invalid}})
if p.activeConfig().Matchers[0].Op != MatchEqual {
t.Fatal("invalid update replaced active config")
}
}
44 changes: 25 additions & 19 deletions asap-precompute-go/matchers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package precompute

import (
"fmt"
"regexp"
"sort"
"strconv"
Expand Down Expand Up @@ -54,40 +55,45 @@ type LabelMatcher struct {
// pattern (Regex/NotRegex).
Value string
// Op picks Equal / NotEqual / Regex / NotRegex.
Op MatchOp
Op MatchOp
compiled *regexp.Regexp
prepared bool
}

// regexCache memoizes compiled, fully-anchored regexps keyed by the raw
// pattern string. LabelMatcher is a value type embedded in
// PrecomputeConfig, so the matcher can't hold a *regexp.Regexp without
// breaking config copies / comparisons; instead Matches() looks the
// compiled form up here. A failed compile is cached as a nil regexp so
// repeated bad patterns don't recompile (and deterministically fail to
// match).
var regexCache sync.Map // map[string]*regexp.Regexp (nil ⇒ compile failed)
// ValidateMatchers rejects malformed regex configuration before activation.
func ValidateMatchers(matchers []LabelMatcher) error {
for i, matcher := range matchers {
if matcher.Op != MatchRegex && matcher.Op != MatchNotRegex {
continue
}
if _, err := regexp.Compile("^(?:" + matcher.Value + ")$"); err != nil {
return fmt.Errorf("matcher %d: %w", i, err)
}
}
return nil
}

// compileAnchored returns the compiled, Prometheus-anchored regexp for
// pattern (matching the full string via ^(?:...)$), using a process-wide
// cache. Returns nil if the pattern fails to compile.
func compileAnchored(pattern string) *regexp.Regexp {
if v, ok := regexCache.Load(pattern); ok {
if v == nil {
return nil
}
return v.(*regexp.Regexp)
}
// Anchor like Prometheus: the pattern must match the entire value.
// Wrap in a non-capturing group so top-level alternation (a|b)
// anchors as a whole rather than ^a|b$.
re, err := regexp.Compile("^(?:" + pattern + ")$")
if err != nil {
regexCache.Store(pattern, (*regexp.Regexp)(nil))
return nil
}
regexCache.Store(pattern, re)
return re
}

func (m LabelMatcher) regexp() *regexp.Regexp {
if m.prepared {
return m.compiled
}
return compileAnchored(m.Value)
}

// Matches returns true iff the observation satisfies all matchers.
//
// Semantics replicate today's per-processor matchesMatchers:
Expand Down Expand Up @@ -130,7 +136,7 @@ func (cfg *PrecomputeConfig) Matches(obs *Observation) bool {
if !present {
return false
}
re := compileAnchored(m.Value)
re := m.regexp()
if re == nil || !re.MatchString(v) {
return false
}
Expand All @@ -139,7 +145,7 @@ func (cfg *PrecomputeConfig) Matches(obs *Observation) bool {
// Present-and-matching fails. A bad pattern (nil re) can't
// match anything, so the negation passes.
if present {
if re := compileAnchored(m.Value); re != nil && re.MatchString(v) {
if re := m.regexp(); re != nil && re.MatchString(v) {
return false
}
}
Expand Down
19 changes: 16 additions & 3 deletions asap-precompute-go/precompute.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,11 @@ func New(initialCfg *PrecomputeConfig, sketchFactory SketchFactory, observer Ske
p.window.snapshotCache = p.snapshotCache
p.window.sketchSink = &p.sketchSink
if initialCfg != nil {
cfgCopy := clonePrecomputeConfig(initialCfg)
p.cfg.Store(cfgCopy)
p.sketchType = initialCfg.SketchType
if ValidateMatchers(initialCfg.Matchers) == nil {
cfgCopy := clonePrecomputeConfig(initialCfg)
p.cfg.Store(cfgCopy)
p.sketchType = initialCfg.SketchType
}
}
return p
}
Expand Down Expand Up @@ -1047,6 +1049,9 @@ func (p *precompute) UpdateConfig(cs *PrecomputeConfigSet) {
if chosen == nil {
chosen = &cs.Configs[0]
}
if ValidateMatchers(chosen.Matchers) != nil {
return
}
cfgCopy := clonePrecomputeConfig(chosen)
// An in-flight window is owned by its current immutable config. Stage the
// replacement until Tick/Drain closes that generation; otherwise old sketch
Expand All @@ -1070,6 +1075,14 @@ func clonePrecomputeConfig(source *PrecomputeConfig) *PrecomputeConfig {
}
cloned := *source
cloned.Matchers = append([]LabelMatcher(nil), source.Matchers...)
for i := range cloned.Matchers {
matcher := &cloned.Matchers[i]
matcher.prepared = true
matcher.compiled = nil
if matcher.Op == MatchRegex || matcher.Op == MatchNotRegex {
matcher.compiled = compileAnchored(matcher.Value)
}
}
cloned.AggregateBy = append([]string(nil), source.AggregateBy...)
cloned.Quantiles = append([]float64(nil), source.Quantiles...)
if source.SketchParams != nil {
Expand Down
Loading