fix(display): retry every failed request with backoff, and never give up
A display tried each of its network paths once: - A registration that failed left it continuing from its persisted state, behind the offline banner, with no realtime, heartbeat or retry, until someone reloaded it. This is what the production kiosk showed after a restart. - A failed state refresh scheduled no further refresh, so a display whose realtime was also down never fetched state again. - A realtime stream the host reported closed was never subscribed again. Each path now retries on its own backoff: about 1 s, doubling to a one-minute ceiling, with jitter so a building's screens do not return in lockstep. There is no attempt limit, because a television has nobody to press Retry. Each resets when its request works again. - Registration retries until it works, then brings the display live exactly as a first-time success does. - A failed refresh schedules its own retry, with no realtime needed. - A closed realtime subscription is replaced. - Stopping clears every retry. The recovery test's virtual window shrinks from an hour to five minutes: eight failures back off for at most about 183 s, and an hour of offline-continuation timers made the test slow without proving more. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c41b3d02f1
commit
d01ffb13e6
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
DISPLAY_RETRY_BASE_MS,
|
||||
DISPLAY_RETRY_MAX_MS,
|
||||
displayRetryDelayMs,
|
||||
} from "../../src/browser/backoff.js";
|
||||
|
||||
describe("display retry backoff", () => {
|
||||
it("doubles from one second and stops at one minute", () => {
|
||||
const longest = (attempt: number) => displayRetryDelayMs(attempt, () => 1);
|
||||
expect([0, 1, 2, 3, 4, 5, 6, 7, 50].map(longest)).toEqual([
|
||||
1_000, 2_000, 4_000, 8_000, 16_000, 32_000, 60_000, 60_000, 60_000,
|
||||
]);
|
||||
});
|
||||
|
||||
it("jitters between half and all of each step, and never waits zero", () => {
|
||||
for (const attempt of [0, 3, 6, 40]) {
|
||||
const step = Math.min(DISPLAY_RETRY_MAX_MS, DISPLAY_RETRY_BASE_MS * 2 ** attempt);
|
||||
expect(displayRetryDelayMs(attempt, () => 0)).toBe(step / 2);
|
||||
expect(displayRetryDelayMs(attempt, () => 1)).toBe(step);
|
||||
for (let index = 0; index < 50; index += 1) {
|
||||
const delay = displayRetryDelayMs(attempt);
|
||||
expect(delay).toBeGreaterThanOrEqual(step / 2);
|
||||
expect(delay).toBeLessThanOrEqual(step);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("treats a nonsensical attempt or random value as the nearest sensible one", () => {
|
||||
expect(displayRetryDelayMs(-3, () => 1)).toBe(1_000);
|
||||
expect(displayRetryDelayMs(2, () => 7)).toBe(4_000);
|
||||
expect(displayRetryDelayMs(2, () => -1)).toBe(2_000);
|
||||
});
|
||||
});
|
||||
@@ -248,8 +248,9 @@ describe("a display recovers from every failed request without a reload", () =>
|
||||
const display = build();
|
||||
await display.start();
|
||||
await settle();
|
||||
// An hour of virtual time: enough for every backoff step, and the cap, to have been reached.
|
||||
await advance(60 * 60_000);
|
||||
// Eight failures back off for at most 1 + 2 + 4 + 8 + 16 + 32 + 60 + 60 seconds, the last two at
|
||||
// the one-minute ceiling. Five virtual minutes is enough for all of them and the success after.
|
||||
await advance(5 * 60_000);
|
||||
|
||||
expect(server.sessionRequests - sessionsBefore).toBe(9);
|
||||
expect(root.dataset.offline).toBe("false");
|
||||
@@ -299,7 +300,7 @@ describe("a display recovers from every failed request without a reload", () =>
|
||||
await display.stop();
|
||||
const sessionsAtStop = server.sessionRequests;
|
||||
|
||||
await advance(60 * 60_000);
|
||||
await advance(5 * 60_000);
|
||||
|
||||
expect(server.sessionRequests).toBe(sessionsAtStop);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user