package host import ( "testing" "time" ) // The edges file schedule (#174): a pass within one watch tick of a registry // change, the old timer kept, and a failed change pass retried. func newTestSchedule(t0 time.Time) *edgesFileSchedule { return newEdgesFileSchedule(t0, func() time.Duration { return 5 * time.Minute }) } // TestEdgesFileScheduleFirstFingerprintOnlyBaselines — arming must not // publish in the loop's first two seconds; the first timed pass stays a // minute after arm, as it was before the change trigger existed. func TestEdgesFileScheduleFirstFingerprintOnlyBaselines(t *testing.T) { t0 := time.Unix(1_000_000, 0) s := newTestSchedule(t0) if s.due(t0.Add(2*time.Second), "fp-a", true) { t.Fatal("the first fingerprint triggered a pass; it should only baseline") } if s.due(t0.Add(30*time.Second), "fp-a", true) { t.Fatal("an unchanged fingerprint triggered a pass before the timer") } if !s.due(t0.Add(time.Minute), "fp-a", true) { t.Fatal("the first timed pass did not come due a minute after arm") } } // TestEdgesFileScheduleChangeTriggersAPass is the headline: a route written // to the registry is published on the next tick, not up to six minutes // later. func TestEdgesFileScheduleChangeTriggersAPass(t *testing.T) { t0 := time.Unix(1_000_000, 0) s := newTestSchedule(t0) s.due(t0.Add(2*time.Second), "fp-a", true) now := t0.Add(4 * time.Second) if !s.due(now, "fp-b", true) { t.Fatal("a fingerprint change did not trigger a pass") } s.done(now, true) if s.due(t0.Add(6*time.Second), "fp-b", true) { t.Fatal("the same fingerprint triggered a second pass after a good one") } // A change pass does not move the timer: it is still due at arm + 1min. if !s.due(t0.Add(time.Minute), "fp-b", true) { t.Fatal("a change pass pushed the timer back") } } // TestEdgesFileScheduleFailedChangePassIsRetried — the baseline advances only // on a successful pass, so a failed bucket write is retried after the first // backoff step (2s) instead of waiting for the timer. func TestEdgesFileScheduleFailedChangePassIsRetried(t *testing.T) { t0 := time.Unix(1_000_000, 0) s := newTestSchedule(t0) s.due(t0.Add(2*time.Second), "fp-a", true) s.due(t0.Add(4*time.Second), "fp-b", true) s.done(t0.Add(4*time.Second), false) if !s.due(t0.Add(6*time.Second), "fp-b", true) { t.Fatal("a change whose pass failed was not retried on the next tick") } s.done(t0.Add(6*time.Second), true) if s.due(t0.Add(8*time.Second), "fp-b", true) { t.Fatal("a change was passed again after its retry succeeded") } } // TestEdgesFileScheduleTimerSurvivesAnUnreadableRegistry — a fingerprint // error never triggers a change pass, and never stops the timer. func TestEdgesFileScheduleTimerSurvivesAnUnreadableRegistry(t *testing.T) { t0 := time.Unix(1_000_000, 0) s := newTestSchedule(t0) s.due(t0.Add(2*time.Second), "fp-a", true) if s.due(t0.Add(4*time.Second), "", false) { t.Fatal("an unreadable fingerprint triggered a pass") } now := t0.Add(time.Minute) if !s.due(now, "", false) { t.Fatal("the timer did not fire while the fingerprint was unreadable") } s.done(now, true) // The timed pass could not vouch for a fingerprint, so the baseline is // still fp-a: a later read of fp-a is not a change, fp-b is. if s.due(now.Add(2*time.Second), "fp-a", true) { t.Fatal("the baseline moved on a pass that saw no fingerprint") } if !s.due(now.Add(4*time.Second), "fp-b", true) { t.Fatal("a change after an unreadable spell was missed") } } // TestEdgesFileScheduleTimerAdvancesOnFailure — a failed timed pass moves the // timer on by the cadence, exactly as the old sleep loop did. func TestEdgesFileScheduleTimerAdvancesOnFailure(t *testing.T) { t0 := time.Unix(1_000_000, 0) s := newTestSchedule(t0) now := t0.Add(time.Minute) if !s.due(now, "fp-a", true) { t.Fatal("timer not due") } s.done(now, false) if s.due(now.Add(2*time.Minute), "fp-a", true) { t.Fatal("a failed timed pass did not move the timer on") } if !s.due(now.Add(5*time.Minute), "fp-a", true) { t.Fatal("the timer did not come due one cadence after the failed pass") } } // TestEdgesFileScheduleFailedChangePassesBackOff — a bucket refusing every // write is retried on a doubling backoff, not every tick, and a success // resets it. func TestEdgesFileScheduleFailedChangePassesBackOff(t *testing.T) { t0 := time.Unix(1_000_000, 0) s := newTestSchedule(t0) s.due(t0, "fp-a", true) now := t0.Add(2 * time.Second) if !s.due(now, "fp-b", true) { t.Fatal("change not due") } s.done(now, false) // retry no sooner than +2s if s.due(now.Add(time.Second), "fp-b", true) { t.Fatal("a failed change pass was retried inside its backoff") } now = now.Add(2 * time.Second) if !s.due(now, "fp-b", true) { t.Fatal("not retried once the 2s backoff passed") } s.done(now, false) // now +4s if s.due(now.Add(3*time.Second), "fp-b", true) { t.Fatal("the backoff did not double") } now = now.Add(4 * time.Second) if !s.due(now, "fp-b", true) { t.Fatal("not retried once the 4s backoff passed") } s.done(now, true) s.due(now.Add(2*time.Second), "fp-b", true) later := now.Add(4 * time.Second) if !s.due(later, "fp-c", true) { t.Fatal("a success did not reset the backoff") } for i := 0; i < 20; i++ { s.done(later, false) } if s.backoff > edgesFileRetryMax { t.Fatalf("backoff %s grew past its cap %s", s.backoff, edgesFileRetryMax) } }