mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Fix panic in backtesting API when the testing interval is not times of evaluation interval (#68727)
* add test for the bug * update backtesting evaluators to accept a number of evaluations instead of `to` to have control over the number evaluations in one place
This commit is contained in:
parent
d88046d3d4
commit
30fc075cd7
@ -25,10 +25,10 @@ var (
|
|||||||
backtestingEvaluatorFactory = newBacktestingEvaluator
|
backtestingEvaluatorFactory = newBacktestingEvaluator
|
||||||
)
|
)
|
||||||
|
|
||||||
type callbackFunc = func(now time.Time, results eval.Results) error
|
type callbackFunc = func(evaluationIndex int, now time.Time, results eval.Results) error
|
||||||
|
|
||||||
type backtestingEvaluator interface {
|
type backtestingEvaluator interface {
|
||||||
Eval(ctx context.Context, from, to time.Time, interval time.Duration, callback callbackFunc) error
|
Eval(ctx context.Context, from time.Time, interval time.Duration, evaluations int, callback callbackFunc) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type stateManager interface {
|
type stateManager interface {
|
||||||
@ -84,8 +84,11 @@ func (e *Engine) Test(ctx context.Context, user *user.SignedInUser, rule *models
|
|||||||
tsField := data.NewField("Time", nil, make([]time.Time, length))
|
tsField := data.NewField("Time", nil, make([]time.Time, length))
|
||||||
valueFields := make(map[string]*data.Field)
|
valueFields := make(map[string]*data.Field)
|
||||||
|
|
||||||
err = evaluator.Eval(ruleCtx, from, to, time.Duration(rule.IntervalSeconds)*time.Second, func(currentTime time.Time, results eval.Results) error {
|
err = evaluator.Eval(ruleCtx, from, time.Duration(rule.IntervalSeconds)*time.Second, length, func(idx int, currentTime time.Time, results eval.Results) error {
|
||||||
idx := int(currentTime.Sub(from).Seconds()) / int(rule.IntervalSeconds)
|
if idx >= length {
|
||||||
|
logger.Info("Unexpected evaluation. Skipping", "from", from, "to", to, "interval", rule.IntervalSeconds, "evaluationTime", currentTime, "evaluationIndex", idx, "expectedEvaluations", length)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
states := stateManager.ProcessEvalResults(ruleCtx, currentTime, rule, results, nil)
|
states := stateManager.ProcessEvalResults(ruleCtx, currentTime, rule, results, nil)
|
||||||
tsField.Set(idx, currentTime)
|
tsField.Set(idx, currentTime)
|
||||||
for _, s := range states {
|
for _, s := range states {
|
||||||
@ -110,7 +113,7 @@ func (e *Engine) Test(ctx context.Context, user *user.SignedInUser, rule *models
|
|||||||
for _, f := range valueFields {
|
for _, f := range valueFields {
|
||||||
fields = append(fields, f)
|
fields = append(fields, f)
|
||||||
}
|
}
|
||||||
result := data.NewFrame("Backtesting results", fields...)
|
result := data.NewFrame("Testing results", fields...)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -263,6 +263,36 @@ func TestEvaluatorTest(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("should not fail if 'to-from' is not times of interval", func(t *testing.T) {
|
||||||
|
from := time.Unix(0, 0)
|
||||||
|
to := from.Add(5 * ruleInterval)
|
||||||
|
|
||||||
|
states := []state.StateTransition{
|
||||||
|
{
|
||||||
|
State: &state.State{
|
||||||
|
CacheID: "state-1",
|
||||||
|
Labels: models.GenerateAlertLabels(rand.Intn(5)+1, "test-"),
|
||||||
|
State: eval.Normal,
|
||||||
|
StateReason: util.GenerateShortUID(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.stateCallback = func(now time.Time) []state.StateTransition {
|
||||||
|
return states
|
||||||
|
}
|
||||||
|
|
||||||
|
frame, err := engine.Test(context.Background(), nil, rule, from, to)
|
||||||
|
require.NoError(t, err)
|
||||||
|
expectedLen := frame.Rows()
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
jitter := time.Duration(rand.Int63n(ruleInterval.Milliseconds())) * time.Millisecond
|
||||||
|
frame, err = engine.Test(context.Background(), nil, rule, from, to.Add(jitter))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equalf(t, expectedLen, frame.Rows(), "jitter %v caused result to be different that base-line", jitter)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("should backfill field with nulls if a new dimension created in the middle", func(t *testing.T) {
|
t.Run("should backfill field with nulls if a new dimension created in the middle", func(t *testing.T) {
|
||||||
from := time.Unix(0, 0)
|
from := time.Unix(0, 0)
|
||||||
|
|
||||||
@ -359,18 +389,16 @@ type fakeBacktestingEvaluator struct {
|
|||||||
evalCallback func(now time.Time) (eval.Results, error)
|
evalCallback func(now time.Time) (eval.Results, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeBacktestingEvaluator) Eval(_ context.Context, from, to time.Time, interval time.Duration, callback callbackFunc) error {
|
func (f *fakeBacktestingEvaluator) Eval(_ context.Context, from time.Time, interval time.Duration, evaluations int, callback callbackFunc) error {
|
||||||
idx := 0
|
for idx, now := 0, from; idx < evaluations; idx, now = idx+1, now.Add(interval) {
|
||||||
for now := from; now.Before(to); now = now.Add(interval) {
|
|
||||||
results, err := f.evalCallback(now)
|
results, err := f.evalCallback(now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = callback(now, results)
|
err = callback(idx, now, results)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
idx++
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -37,10 +37,9 @@ func newDataEvaluator(refID string, frame *data.Frame) (*dataEvaluator, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *dataEvaluator) Eval(_ context.Context, from, to time.Time, interval time.Duration, callback callbackFunc) error {
|
func (d *dataEvaluator) Eval(_ context.Context, from time.Time, interval time.Duration, evaluations int, callback callbackFunc) error {
|
||||||
var resampled = make([]mathexp.Series, 0, len(d.data))
|
var resampled = make([]mathexp.Series, 0, len(d.data))
|
||||||
|
to := from.Add(time.Duration(evaluations) * interval)
|
||||||
iterations := 0
|
|
||||||
for _, s := range d.data {
|
for _, s := range d.data {
|
||||||
// making sure the input data frame is aligned with the interval
|
// making sure the input data frame is aligned with the interval
|
||||||
r, err := s.Resample(d.refID, interval, d.downsampleFunction, d.upsampleFunction, from, to.Add(-interval)) // we want to query [from,to)
|
r, err := s.Resample(d.refID, interval, d.downsampleFunction, d.upsampleFunction, from, to.Add(-interval)) // we want to query [from,to)
|
||||||
@ -48,10 +47,9 @@ func (d *dataEvaluator) Eval(_ context.Context, from, to time.Time, interval tim
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
resampled = append(resampled, r)
|
resampled = append(resampled, r)
|
||||||
iterations = r.Len()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < iterations; i++ {
|
for i := 0; i < evaluations; i++ {
|
||||||
result := make([]eval.Result, 0, len(resampled))
|
result := make([]eval.Result, 0, len(resampled))
|
||||||
var now time.Time
|
var now time.Time
|
||||||
for _, series := range resampled {
|
for _, series := range resampled {
|
||||||
@ -87,7 +85,7 @@ func (d *dataEvaluator) Eval(_ context.Context, from, to time.Time, interval tim
|
|||||||
EvaluatedAt: now,
|
EvaluatedAt: now,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
err := callback(now, result)
|
err := callback(i, now, result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -96,11 +96,11 @@ func TestDataEvaluator_Eval(t *testing.T) {
|
|||||||
t.Run("should use data points when frame resolution matches evaluation interval", func(t *testing.T) {
|
t.Run("should use data points when frame resolution matches evaluation interval", func(t *testing.T) {
|
||||||
r := make([]results, 0, frame.Rows())
|
r := make([]results, 0, frame.Rows())
|
||||||
|
|
||||||
invterval := time.Second
|
interval := time.Second
|
||||||
|
|
||||||
resultsCount := int(to.Sub(from).Seconds() / invterval.Seconds())
|
resultsCount := int(to.Sub(from).Seconds() / interval.Seconds())
|
||||||
|
|
||||||
err = evaluator.Eval(context.Background(), from, to, time.Second, func(now time.Time, res eval.Results) error {
|
err = evaluator.Eval(context.Background(), from, time.Second, resultsCount, func(idx int, now time.Time, res eval.Results) error {
|
||||||
r = append(r, results{
|
r = append(r, results{
|
||||||
now, res,
|
now, res,
|
||||||
})
|
})
|
||||||
@ -164,7 +164,7 @@ func TestDataEvaluator_Eval(t *testing.T) {
|
|||||||
size := to.Sub(from).Milliseconds() / interval.Milliseconds()
|
size := to.Sub(from).Milliseconds() / interval.Milliseconds()
|
||||||
r := make([]results, 0, size)
|
r := make([]results, 0, size)
|
||||||
|
|
||||||
err = evaluator.Eval(context.Background(), from, to, interval, func(now time.Time, res eval.Results) error {
|
err = evaluator.Eval(context.Background(), from, interval, int(size), func(idx int, now time.Time, res eval.Results) error {
|
||||||
r = append(r, results{
|
r = append(r, results{
|
||||||
now, res,
|
now, res,
|
||||||
})
|
})
|
||||||
@ -195,7 +195,7 @@ func TestDataEvaluator_Eval(t *testing.T) {
|
|||||||
size := int(to.Sub(from).Seconds() / interval.Seconds())
|
size := int(to.Sub(from).Seconds() / interval.Seconds())
|
||||||
r := make([]results, 0, size)
|
r := make([]results, 0, size)
|
||||||
|
|
||||||
err = evaluator.Eval(context.Background(), from, to, interval, func(now time.Time, res eval.Results) error {
|
err = evaluator.Eval(context.Background(), from, interval, size, func(idx int, now time.Time, res eval.Results) error {
|
||||||
r = append(r, results{
|
r = append(r, results{
|
||||||
now, res,
|
now, res,
|
||||||
})
|
})
|
||||||
@ -230,7 +230,7 @@ func TestDataEvaluator_Eval(t *testing.T) {
|
|||||||
t.Run("should be noData until the frame interval", func(t *testing.T) {
|
t.Run("should be noData until the frame interval", func(t *testing.T) {
|
||||||
newFrom := from.Add(-10 * time.Second)
|
newFrom := from.Add(-10 * time.Second)
|
||||||
r := make([]results, 0, int(to.Sub(newFrom).Seconds()))
|
r := make([]results, 0, int(to.Sub(newFrom).Seconds()))
|
||||||
err = evaluator.Eval(context.Background(), newFrom, to, time.Second, func(now time.Time, res eval.Results) error {
|
err = evaluator.Eval(context.Background(), newFrom, time.Second, cap(r), func(idx int, now time.Time, res eval.Results) error {
|
||||||
r = append(r, results{
|
r = append(r, results{
|
||||||
now, res,
|
now, res,
|
||||||
})
|
})
|
||||||
@ -258,7 +258,7 @@ func TestDataEvaluator_Eval(t *testing.T) {
|
|||||||
t.Run("should be the last value after the frame interval", func(t *testing.T) {
|
t.Run("should be the last value after the frame interval", func(t *testing.T) {
|
||||||
newTo := to.Add(10 * time.Second)
|
newTo := to.Add(10 * time.Second)
|
||||||
r := make([]results, 0, int(newTo.Sub(from).Seconds()))
|
r := make([]results, 0, int(newTo.Sub(from).Seconds()))
|
||||||
err = evaluator.Eval(context.Background(), from, newTo, time.Second, func(now time.Time, res eval.Results) error {
|
err = evaluator.Eval(context.Background(), from, time.Second, cap(r), func(idx int, now time.Time, res eval.Results) error {
|
||||||
r = append(r, results{
|
r = append(r, results{
|
||||||
now, res,
|
now, res,
|
||||||
})
|
})
|
||||||
@ -282,12 +282,10 @@ func TestDataEvaluator_Eval(t *testing.T) {
|
|||||||
})
|
})
|
||||||
t.Run("should stop if callback error", func(t *testing.T) {
|
t.Run("should stop if callback error", func(t *testing.T) {
|
||||||
expectedError := errors.New("error")
|
expectedError := errors.New("error")
|
||||||
evals := 0
|
err = evaluator.Eval(context.Background(), from, time.Second, 6, func(idx int, now time.Time, res eval.Results) error {
|
||||||
err = evaluator.Eval(context.Background(), from, to, time.Second, func(now time.Time, res eval.Results) error {
|
if idx == 5 {
|
||||||
if evals > 5 {
|
|
||||||
return expectedError
|
return expectedError
|
||||||
}
|
}
|
||||||
evals++
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
require.ErrorIs(t, err, expectedError)
|
require.ErrorIs(t, err, expectedError)
|
||||||
|
@ -12,13 +12,13 @@ type queryEvaluator struct {
|
|||||||
eval eval.ConditionEvaluator
|
eval eval.ConditionEvaluator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *queryEvaluator) Eval(ctx context.Context, from, to time.Time, interval time.Duration, callback callbackFunc) error {
|
func (d *queryEvaluator) Eval(ctx context.Context, from time.Time, interval time.Duration, evaluations int, callback callbackFunc) error {
|
||||||
for now := from; now.Before(to); now = now.Add(interval) {
|
for idx, now := 0, from; idx < evaluations; idx, now = idx+1, now.Add(interval) {
|
||||||
results, err := d.eval.Evaluate(ctx, now)
|
results, err := d.eval.Evaluate(ctx, now)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = callback(now, results)
|
err = callback(idx, now, results)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
@ -18,8 +19,7 @@ func TestQueryEvaluator_Eval(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
interval := time.Duration(rand.Int63n(9)+1) * time.Second
|
interval := time.Duration(rand.Int63n(9)+1) * time.Second
|
||||||
times := rand.Intn(11) + 5
|
times := rand.Intn(11) + 5
|
||||||
to := time.Now()
|
from := time.Now().Add(-time.Duration(times) * interval)
|
||||||
from := to.Add(-time.Duration(times) * interval)
|
|
||||||
|
|
||||||
t.Run("should evaluate query", func(t *testing.T) {
|
t.Run("should evaluate query", func(t *testing.T) {
|
||||||
m := &eval_mocks.ConditionEvaluatorMock{}
|
m := &eval_mocks.ConditionEvaluatorMock{}
|
||||||
@ -29,15 +29,20 @@ func TestQueryEvaluator_Eval(t *testing.T) {
|
|||||||
eval: m,
|
eval: m,
|
||||||
}
|
}
|
||||||
|
|
||||||
intervals := make([]time.Time, 0, times)
|
intervals := make([]time.Time, times)
|
||||||
|
|
||||||
err := evaluator.Eval(ctx, from, to, interval, func(now time.Time, results eval.Results) error {
|
err := evaluator.Eval(ctx, from, interval, times, func(idx int, now time.Time, results eval.Results) error {
|
||||||
intervals = append(intervals, now)
|
intervals[idx] = now
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, intervals, times)
|
require.Len(t, intervals, times)
|
||||||
|
|
||||||
|
expected := from
|
||||||
|
for idx, actual := range intervals {
|
||||||
|
assert.Equalf(t, expected, actual, "item at index %d is not times of interval %v", idx, interval)
|
||||||
|
expected = expected.Add(interval)
|
||||||
|
}
|
||||||
m.AssertNumberOfCalls(t, "Evaluate", times)
|
m.AssertNumberOfCalls(t, "Evaluate", times)
|
||||||
for _, now := range intervals {
|
for _, now := range intervals {
|
||||||
m.AssertCalled(t, "Evaluate", ctx, now)
|
m.AssertCalled(t, "Evaluate", ctx, now)
|
||||||
@ -57,7 +62,7 @@ func TestQueryEvaluator_Eval(t *testing.T) {
|
|||||||
|
|
||||||
intervals := make([]time.Time, 0, times)
|
intervals := make([]time.Time, 0, times)
|
||||||
|
|
||||||
err := evaluator.Eval(ctx, from, to, interval, func(now time.Time, results eval.Results) error {
|
err := evaluator.Eval(ctx, from, interval, times, func(idx int, now time.Time, results eval.Results) error {
|
||||||
intervals = append(intervals, now)
|
intervals = append(intervals, now)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@ -76,7 +81,7 @@ func TestQueryEvaluator_Eval(t *testing.T) {
|
|||||||
|
|
||||||
intervals := make([]time.Time, 0, times)
|
intervals := make([]time.Time, 0, times)
|
||||||
|
|
||||||
err := evaluator.Eval(ctx, from, to, interval, func(now time.Time, results eval.Results) error {
|
err := evaluator.Eval(ctx, from, interval, times, func(idx int, now time.Time, results eval.Results) error {
|
||||||
if len(intervals) > 3 {
|
if len(intervals) > 3 {
|
||||||
return expectedError
|
return expectedError
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user