diff --git a/lib/extension/availability.ts b/lib/extension/availability.ts index acb9e5a1e..71f066daf 100644 --- a/lib/extension/availability.ts +++ b/lib/extension/availability.ts @@ -9,6 +9,12 @@ import * as settings from "../util/settings"; import utils from "../util/utils"; import Extension from "./extension"; +/** + * Upper bound for a `setTimeout` delay. Node.js stores the delay as a 32-bit signed integer; anything above this + * is coerced to `1`, which would turn an ever-growing backoff into a tight loop instead of an ever-longer wait. + */ +const MAX_TIMEOUT = 2147483647; + const RETRIEVE_ON_RECONNECT: readonly {keys: string[]; condition?: (state: KeyValue) => boolean}[] = [ {keys: ["state"]}, {keys: ["brightness"], condition: (state: KeyValue): boolean => state.state === "ON"}, @@ -108,7 +114,10 @@ export default class Availability extends Extension { // If device did not check in, ping it, if that fails it will be marked as offline this.timers.set( device.ieeeAddr, - setTimeout(this.addToPingQueue.bind(this, device), (this.getTimeout(device) + utils.seconds(1) + jitter) * backoff), + setTimeout( + this.addToPingQueue.bind(this, device), + Math.min((this.getTimeout(device) + utils.seconds(1) + jitter) * backoff, MAX_TIMEOUT), + ), ); } } else { diff --git a/test/extensions/availability.test.ts b/test/extensions/availability.test.ts index 96eb76269..e3ce419de 100644 --- a/test/extensions/availability.test.ts +++ b/test/extensions/availability.test.ts @@ -633,6 +633,19 @@ describe("Extension: Availability", () => { expect(devices.QBKG03LM.ping).toHaveBeenCalledTimes(4); }); + it("clamps the ping delay to the maximum supported timeout", async () => { + // `setTimeout` takes a 32-bit signed integer and coerces anything above it to `1`. A delay can exceed + // that either directly, through a long `timeout`, or gradually, once `backoff` has multiplied a normal + // one over successive failures. Unclamped, that turns an ever-longer wait into a tight ping loop. + settings.set(["devices", devices.bulb_color.ieeeAddr, "availability"], {timeout: 40000, max_jitter: 0}); // ~27.8 days + await resetExtension(); + + // unclamped, the delay collapses to 1ms, so pings would already be looping by now + await setTimeAndAdvanceTimers(utils.seconds(1)); + + expect(devices.bulb_color.ping).not.toHaveBeenCalled(); + }); + it("allows to disable backoff", async () => { settings.set(["availability", "active", "max_jitter"], 0); // easier testing settings.set(["availability", "active", "backoff"], false);