| 1 | package host |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | "time" |
| 6 | ) |
| 7 | |
| 8 | // The edges file schedule (#174): a pass within one watch tick of a registry |
| 9 | // change, the old timer kept, and a failed change pass retried. |
| 10 | |
| 11 | func newTestSchedule(t0 time.Time) *edgesFileSchedule { |
| 12 | return newEdgesFileSchedule(t0, func() time.Duration { return 5 * time.Minute }) |
| 13 | } |
| 14 | |
| 15 | // TestEdgesFileScheduleFirstFingerprintOnlyBaselines — arming must not |
| 16 | // publish in the loop's first two seconds; the first timed pass stays a |
| 17 | // minute after arm, as it was before the change trigger existed. |
| 18 | func TestEdgesFileScheduleFirstFingerprintOnlyBaselines(t *testing.T) { |
| 19 | t0 := time.Unix(1_000_000, 0) |
| 20 | s := newTestSchedule(t0) |
| 21 | if s.due(t0.Add(2*time.Second), "fp-a", true) { |
| 22 | t.Fatal("the first fingerprint triggered a pass; it should only baseline") |
| 23 | } |
| 24 | if s.due(t0.Add(30*time.Second), "fp-a", true) { |
| 25 | t.Fatal("an unchanged fingerprint triggered a pass before the timer") |
| 26 | } |
| 27 | if !s.due(t0.Add(time.Minute), "fp-a", true) { |
| 28 | t.Fatal("the first timed pass did not come due a minute after arm") |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | // TestEdgesFileScheduleChangeTriggersAPass is the headline: a route written |
| 33 | // to the registry is published on the next tick, not up to six minutes |
| 34 | // later. |
| 35 | func TestEdgesFileScheduleChangeTriggersAPass(t *testing.T) { |
| 36 | t0 := time.Unix(1_000_000, 0) |
| 37 | s := newTestSchedule(t0) |
| 38 | s.due(t0.Add(2*time.Second), "fp-a", true) |
| 39 | now := t0.Add(4 * time.Second) |
| 40 | if !s.due(now, "fp-b", true) { |
| 41 | t.Fatal("a fingerprint change did not trigger a pass") |
| 42 | } |
| 43 | s.done(now, true) |
| 44 | if s.due(t0.Add(6*time.Second), "fp-b", true) { |
| 45 | t.Fatal("the same fingerprint triggered a second pass after a good one") |
| 46 | } |
| 47 | // A change pass does not move the timer: it is still due at arm + 1min. |
| 48 | if !s.due(t0.Add(time.Minute), "fp-b", true) { |
| 49 | t.Fatal("a change pass pushed the timer back") |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | // TestEdgesFileScheduleFailedChangePassIsRetried — the baseline advances only |
| 54 | // on a successful pass, so a failed bucket write is retried after the first |
| 55 | // backoff step (2s) instead of waiting for the timer. |
| 56 | func TestEdgesFileScheduleFailedChangePassIsRetried(t *testing.T) { |
| 57 | t0 := time.Unix(1_000_000, 0) |
| 58 | s := newTestSchedule(t0) |
| 59 | s.due(t0.Add(2*time.Second), "fp-a", true) |
| 60 | s.due(t0.Add(4*time.Second), "fp-b", true) |
| 61 | s.done(t0.Add(4*time.Second), false) |
| 62 | if !s.due(t0.Add(6*time.Second), "fp-b", true) { |
| 63 | t.Fatal("a change whose pass failed was not retried on the next tick") |
| 64 | } |
| 65 | s.done(t0.Add(6*time.Second), true) |
| 66 | if s.due(t0.Add(8*time.Second), "fp-b", true) { |
| 67 | t.Fatal("a change was passed again after its retry succeeded") |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | // TestEdgesFileScheduleTimerSurvivesAnUnreadableRegistry — a fingerprint |
| 72 | // error never triggers a change pass, and never stops the timer. |
| 73 | func TestEdgesFileScheduleTimerSurvivesAnUnreadableRegistry(t *testing.T) { |
| 74 | t0 := time.Unix(1_000_000, 0) |
| 75 | s := newTestSchedule(t0) |
| 76 | s.due(t0.Add(2*time.Second), "fp-a", true) |
| 77 | if s.due(t0.Add(4*time.Second), "", false) { |
| 78 | t.Fatal("an unreadable fingerprint triggered a pass") |
| 79 | } |
| 80 | now := t0.Add(time.Minute) |
| 81 | if !s.due(now, "", false) { |
| 82 | t.Fatal("the timer did not fire while the fingerprint was unreadable") |
| 83 | } |
| 84 | s.done(now, true) |
| 85 | // The timed pass could not vouch for a fingerprint, so the baseline is |
| 86 | // still fp-a: a later read of fp-a is not a change, fp-b is. |
| 87 | if s.due(now.Add(2*time.Second), "fp-a", true) { |
| 88 | t.Fatal("the baseline moved on a pass that saw no fingerprint") |
| 89 | } |
| 90 | if !s.due(now.Add(4*time.Second), "fp-b", true) { |
| 91 | t.Fatal("a change after an unreadable spell was missed") |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | // TestEdgesFileScheduleTimerAdvancesOnFailure — a failed timed pass moves the |
| 96 | // timer on by the cadence, exactly as the old sleep loop did. |
| 97 | func TestEdgesFileScheduleTimerAdvancesOnFailure(t *testing.T) { |
| 98 | t0 := time.Unix(1_000_000, 0) |
| 99 | s := newTestSchedule(t0) |
| 100 | now := t0.Add(time.Minute) |
| 101 | if !s.due(now, "fp-a", true) { |
| 102 | t.Fatal("timer not due") |
| 103 | } |
| 104 | s.done(now, false) |
| 105 | if s.due(now.Add(2*time.Minute), "fp-a", true) { |
| 106 | t.Fatal("a failed timed pass did not move the timer on") |
| 107 | } |
| 108 | if !s.due(now.Add(5*time.Minute), "fp-a", true) { |
| 109 | t.Fatal("the timer did not come due one cadence after the failed pass") |
| 110 | } |
| 111 | } |
| 112 | |
| 113 | // TestEdgesFileScheduleFailedChangePassesBackOff — a bucket refusing every |
| 114 | // write is retried on a doubling backoff, not every tick, and a success |
| 115 | // resets it. |
| 116 | func TestEdgesFileScheduleFailedChangePassesBackOff(t *testing.T) { |
| 117 | t0 := time.Unix(1_000_000, 0) |
| 118 | s := newTestSchedule(t0) |
| 119 | s.due(t0, "fp-a", true) |
| 120 | now := t0.Add(2 * time.Second) |
| 121 | if !s.due(now, "fp-b", true) { |
| 122 | t.Fatal("change not due") |
| 123 | } |
| 124 | s.done(now, false) // retry no sooner than +2s |
| 125 | if s.due(now.Add(time.Second), "fp-b", true) { |
| 126 | t.Fatal("a failed change pass was retried inside its backoff") |
| 127 | } |
| 128 | now = now.Add(2 * time.Second) |
| 129 | if !s.due(now, "fp-b", true) { |
| 130 | t.Fatal("not retried once the 2s backoff passed") |
| 131 | } |
| 132 | s.done(now, false) // now +4s |
| 133 | if s.due(now.Add(3*time.Second), "fp-b", true) { |
| 134 | t.Fatal("the backoff did not double") |
| 135 | } |
| 136 | now = now.Add(4 * time.Second) |
| 137 | if !s.due(now, "fp-b", true) { |
| 138 | t.Fatal("not retried once the 4s backoff passed") |
| 139 | } |
| 140 | s.done(now, true) |
| 141 | s.due(now.Add(2*time.Second), "fp-b", true) |
| 142 | later := now.Add(4 * time.Second) |
| 143 | if !s.due(later, "fp-c", true) { |
| 144 | t.Fatal("a success did not reset the backoff") |
| 145 | } |
| 146 | for i := 0; i < 20; i++ { |
| 147 | s.done(later, false) |
| 148 | } |
| 149 | if s.backoff > edgesFileRetryMax { |
| 150 | t.Fatalf("backoff %s grew past its cap %s", s.backoff, edgesFileRetryMax) |
| 151 | } |
| 152 | } |
| 153 | |