Files
grafana/pkg/services/ngalert/schedule/sequence.go
Moustafa Baiou ca8324e62a Alerting: Add support for alpha rules apis in legacy storage
Rules created in the new api makes the rule have no group in the database, but the rule is returned in the old group api with a sentinel group name formatted with the rule uid for compatiblity with the old api.
This makes the UI continue to work with the rules without a group, and the ruler will continue to work with the rules without a group.

Rules are not allowed to be created in the provisioning api with a NoGroup sentinel mask, but NoGroup rules can be manipulated through both the new and old apis.

Co-authored-by: William Wernert <william.wernert@grafana.com>
2025-09-10 09:30:56 -04:00

133 lines
3.8 KiB
Go

package schedule
import (
"cmp"
"slices"
"strings"
models "github.com/grafana/grafana/pkg/services/ngalert/models"
)
// sequence represents a chain of rules that should be evaluated in order.
// It is a convience type that wraps readyToRunItem as an indicator of what
// is being represented.
type sequence readyToRunItem
type groupKey struct {
folderTitle string
folderUID string
groupName string
}
// buildSequences organizes rules into evaluation sequences where rules in the same group
// are chained together. The first rule in each group will trigger the evaluation of subsequent
// rules in that group through the afterEval callback.
//
// For example, if we have rules A, B, C in group G1 and rules D, E in group G2:
// - A will have afterEval set to evaluate B
// - B will have afterEval set to evaluate C
// - D will have afterEval set to evaluate E
//
// The function returns a slice of sequences, where each sequence represents a chain of rules
// that should be evaluated in order.
//
// NOTE: This currently only chains rules in imported groups.
func (sch *schedule) buildSequences(items []readyToRunItem, runJobFn func(next readyToRunItem, prev ...readyToRunItem) func()) []sequence {
// Step 1: Group rules by their folder and group name
groups := map[groupKey][]readyToRunItem{}
var keys []groupKey
for _, item := range items {
g := groupKey{
folderTitle: item.folderTitle,
folderUID: item.rule.NamespaceUID,
groupName: item.rule.RuleGroup,
}
i, ok := groups[g]
if !ok {
keys = append(keys, g)
}
groups[g] = append(i, item)
}
// Step 2: Sort group keys to ensure consistent ordering
slices.SortFunc(keys, func(a, b groupKey) int {
return cmp.Or(
cmp.Compare(a.folderTitle, b.folderTitle),
cmp.Compare(a.folderUID, b.folderUID),
cmp.Compare(a.groupName, b.groupName),
)
})
// Step 3: Build evaluation sequences for each group
result := make([]sequence, 0, len(items))
for _, key := range keys {
groupItems := groups[key]
if sch.shouldEvaluateSequentially(groupItems) {
result = append(result, sch.buildSequence(key, groupItems, runJobFn))
continue
}
for _, item := range groupItems {
result = append(result, sequence(item))
}
}
// sort the sequences by UID
slices.SortFunc(result, func(a, b sequence) int {
return strings.Compare(a.rule.UID, b.rule.UID)
})
return result
}
func (sch *schedule) buildSequence(groupKey groupKey, groupItems []readyToRunItem, runJobFn func(next readyToRunItem, prev ...readyToRunItem) func()) sequence {
if len(groupItems) < 2 {
return sequence(groupItems[0])
}
slices.SortFunc(groupItems, func(a, b readyToRunItem) int {
return models.RulesGroupComparer(a.rule, b.rule)
})
// iterate over the group items backwards to set the afterEval callback
for i := len(groupItems) - 2; i >= 0; i-- {
groupItems[i].afterEval = runJobFn(groupItems[i+1], groupItems[i])
}
uids := make([]string, 0, len(groupItems))
for _, item := range groupItems {
uids = append(uids, item.rule.UID)
}
sch.log.Debug("Sequence created", "folder", groupKey.folderTitle, "group", groupKey.groupName, "sequence", strings.Join(uids, "->"))
return sequence(groupItems[0])
}
func (sch *schedule) shouldEvaluateSequentially(groupItems []readyToRunItem) bool {
// the no group group shouldn't be evaluated sequentially
if len(groupItems) > 0 && models.IsNoGroupRuleGroup(groupItems[0].rule.RuleGroup) {
return false
}
// if jitter by rule is enabled, we can't evaluate rules sequentially
if sch.jitterEvaluations == JitterByRule {
return false
}
// if there is only one rule, there are no rules to chain
if len(groupItems) == 1 {
return false
}
// only evaluate rules in imported groups sequentially
for _, item := range groupItems {
if item.rule.ImportedPrometheusRule() {
return true
}
}
// default to false
return false
}