From 3fb881af261361a6083eedb329dd3aef45a377fe Mon Sep 17 00:00:00 2001 From: Dimitar Dimitrov Date: Sun, 11 Sep 2022 11:21:03 +0200 Subject: [PATCH 1/2] Simplify rule group's EvalTimestamp formula I found it hard to understand how EvalTimestamp works, so I wanted to simplify the math there. This PR should be a noop. Current formula is: ``` offset = g.hash % g.interval adjNow = startTime - offset base = adjNow - (adjNow % g.interval) EvalTimestamp = base + offset ``` I simplify `EvalTimestamp` ``` EvalTimestamp = base + offset # expand base = adjNow - (adjNow % g.interval) + offset # expand adjNow = startTime - offset - ((startTime - offset) % g.interval) + offset # cancel out offset = startTime - ((startTime - offset) % g.interval) # expand A+B (mod M) = (A (mod M) + B (mod M)) (mod M) = startTime - (startTime % g.interval - offset % g.interval) % g.interval # expand offset = startTime - (startTime % g.interval - ((g.hash % g.interval) % g.interval)) % g.interval # remove redundant mod g.interval = startTime - (startTime % g.interval - g.hash % g.interval) % g.interval # simplify (A (mod M) + B (mod M)) (mod M) = A+B (mod M) = startTime - (startTime - g.hash) % g.interval offset = (startTime - g.hash) % g.interval EvalTimestamp = startTime - offset ``` Signed-off-by: Dimitar Dimitrov --- rules/manager.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/rules/manager.go b/rules/manager.go index 5eed4bfb0..140fb45b7 100644 --- a/rules/manager.go +++ b/rules/manager.go @@ -532,13 +532,9 @@ func (g *Group) setLastEvaluation(ts time.Time) { // EvalTimestamp returns the immediately preceding consistently slotted evaluation time. func (g *Group) EvalTimestamp(startTime int64) time.Time { - var ( - offset = int64(g.hash() % uint64(g.interval)) - adjNow = startTime - offset - base = adjNow - (adjNow % int64(g.interval)) - ) + offset := (uint64(startTime) - g.hash()) % uint64(g.interval) - return time.Unix(0, base+offset).UTC() + return time.Unix(0, startTime-int64(offset)).UTC() } func nameAndLabels(rule Rule) string { From 03ab8dcca0040674de5c1a6a9caabdf5ab4c1d4b Mon Sep 17 00:00:00 2001 From: Dimitar Dimitrov Date: Wed, 12 Oct 2022 14:12:03 +0200 Subject: [PATCH 2/2] Add comments on EvalTimestamp Signed-off-by: Dimitar Dimitrov --- rules/manager.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/rules/manager.go b/rules/manager.go index 140fb45b7..e24eee2c0 100644 --- a/rules/manager.go +++ b/rules/manager.go @@ -532,9 +532,28 @@ func (g *Group) setLastEvaluation(ts time.Time) { // EvalTimestamp returns the immediately preceding consistently slotted evaluation time. func (g *Group) EvalTimestamp(startTime int64) time.Time { - offset := (uint64(startTime) - g.hash()) % uint64(g.interval) + var ( + offset = int64(g.hash() % uint64(g.interval)) - return time.Unix(0, startTime-int64(offset)).UTC() + // 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 + + // 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, next).UTC() } func nameAndLabels(rule Rule) string {