Ruler Concurrency: Consider __name__ matchers (#16039)

When calculating dependencies between rules, we sometimes run into `{__name__...}` matchers
These can be used the same way as the actual rule names
This will enable even more rules to run concurrently

The new logic is also not slower:
```
julienduchesne@triceratops prometheus % benchstat test-old.txt test.txt
goos: darwin
goarch: arm64
pkg: github.com/prometheus/prometheus/rules
cpu: Apple M3 Pro
                 │ test-old.txt │              test.txt               │
                 │    sec/op    │   sec/op     vs base                │
DependencyMap-11    1.206µ ± 7%   1.024µ ± 7%  -15.10% (p=0.000 n=10)

                 │ test-old.txt │               test.txt               │
                 │     B/op     │     B/op      vs base                │
DependencyMap-11   1.720Ki ± 0%   1.438Ki ± 0%  -16.35% (p=0.000 n=10)

                 │ test-old.txt │              test.txt              │
                 │  allocs/op   │ allocs/op   vs base                │
DependencyMap-11     39.00 ± 0%   34.00 ± 0%  -12.82% (p=0.000 n=10)
```

Signed-off-by: Julien Duchesne <julien.duchesne@grafana.com>
This commit is contained in:
Julien Duchesne 2025-02-17 06:19:16 -05:00 committed by GitHub
parent a5ffa83be8
commit 77a5698190
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 37 additions and 20 deletions

View file

@ -1110,9 +1110,6 @@ func buildDependencyMap(rules []Rule) dependencyMap {
return dependencies
}
inputs := make(map[string][]Rule, len(rules))
outputs := make(map[string][]Rule, len(rules))
var indeterminate bool
for _, rule := range rules {
@ -1120,26 +1117,46 @@ func buildDependencyMap(rules []Rule) dependencyMap {
break
}
name := rule.Name()
outputs[name] = append(outputs[name], rule)
parser.Inspect(rule.Query(), func(node parser.Node, path []parser.Node) error {
if n, ok := node.(*parser.VectorSelector); ok {
// Find the name matcher for the rule.
var nameMatcher *labels.Matcher
if n.Name != "" {
nameMatcher = labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, n.Name)
} else {
for _, m := range n.LabelMatchers {
if m.Name == model.MetricNameLabel {
nameMatcher = m
break
}
}
}
// A wildcard metric expression means we cannot reliably determine if this rule depends on any other,
// which means we cannot safely run any rules concurrently.
if n.Name == "" && len(n.LabelMatchers) > 0 {
if nameMatcher == nil {
indeterminate = true
return nil
}
// Rules which depend on "meta-metrics" like ALERTS and ALERTS_FOR_STATE will have undefined behaviour
// if they run concurrently.
if n.Name == alertMetricName || n.Name == alertForStateMetricName {
if nameMatcher.Matches(alertMetricName) || nameMatcher.Matches(alertForStateMetricName) {
indeterminate = true
return nil
}
inputs[n.Name] = append(inputs[n.Name], rule)
// Find rules which depend on the output of this rule.
for _, other := range rules {
if other == rule {
continue
}
otherName := other.Name()
if nameMatcher.Matches(otherName) {
dependencies[other] = append(dependencies[other], rule)
}
}
}
return nil
})
@ -1149,13 +1166,5 @@ func buildDependencyMap(rules []Rule) dependencyMap {
return nil
}
for output, outRules := range outputs {
for _, outRule := range outRules {
if inRules, found := inputs[output]; found && len(inRules) > 0 {
dependencies[outRule] = append(dependencies[outRule], inRules...)
}
}
}
return dependencies
}

View file

@ -1601,10 +1601,14 @@ func TestDependencyMap(t *testing.T) {
require.NoError(t, err)
rule4 := NewRecordingRule("user:requests:increase1h", expr, labels.Labels{})
expr, err = parser.ParseExpr(`sum by (user) ({__name__=~"user:requests.+5m"})`)
require.NoError(t, err)
rule5 := NewRecordingRule("user:requests:sum5m", expr, labels.Labels{})
group := NewGroup(GroupOptions{
Name: "rule_group",
Interval: time.Second,
Rules: []Rule{rule, rule2, rule3, rule4},
Rules: []Rule{rule, rule2, rule3, rule4, rule5},
Opts: opts,
})
@ -1619,13 +1623,17 @@ func TestDependencyMap(t *testing.T) {
require.Equal(t, []Rule{rule}, depMap.dependencies(rule2))
require.False(t, depMap.isIndependent(rule2))
require.Zero(t, depMap.dependents(rule3))
require.Equal(t, []Rule{rule5}, depMap.dependents(rule3))
require.Zero(t, depMap.dependencies(rule3))
require.True(t, depMap.isIndependent(rule3))
require.False(t, depMap.isIndependent(rule3))
require.Zero(t, depMap.dependents(rule4))
require.Equal(t, []Rule{rule}, depMap.dependencies(rule4))
require.False(t, depMap.isIndependent(rule4))
require.Zero(t, depMap.dependents(rule5))
require.Equal(t, []Rule{rule3}, depMap.dependencies(rule5))
require.False(t, depMap.isIndependent(rule5))
}
func TestNoDependency(t *testing.T) {