diff --git a/rules/manager.go b/rules/manager.go index 4f848a74b..0d8d022cd 100644 --- a/rules/manager.go +++ b/rules/manager.go @@ -560,11 +560,26 @@ func (g *Group) setLastEvalTimestamp(ts time.Time) { func (g *Group) EvalTimestamp(startTime int64) time.Time { var ( offset = int64(g.hash() % uint64(g.interval)) + + // This group's evaluation times differ from the perfect time intervals by `offset` nanoseconds. + // But we can only use `% interval` to align with the interval. And `% interval` will always + // align with the perfect time intervals, instead of this group's. Because of this we add + // `offset` _after_ aligning with the perfect time interval. + // + // There can be cases where adding `offset` to the perfect evaluation time can yield a + // timestamp in the future, which is not what EvalTimestamp should do. + // So we subtract one `offset` to make sure that `now - (now % interval) + offset` gives an + // evaluation time in the past. adjNow = startTime - offset - base = adjNow - (adjNow % int64(g.interval)) + + // Adjust to perfect evaluation intervals. + base = adjNow - (adjNow % int64(g.interval)) + + // Add one offset to randomize the evaluation times of this group. + next = base + offset ) - return time.Unix(0, base+offset).UTC() + return time.Unix(0, next).UTC() } func nameAndLabels(rule Rule) string {