Repository: louislam/uptime-kuma
File: server/model/monitor.js (JavaScript)

Code before:
                            let timeout = beatInterval * 1000 - msSinceLastBeat;
                            if (timeout < 0) {
                                timeout = bufferTime;
                            } else {
                                timeout += bufferTime;
                            }
                            // No need to insert successful heartbeat for push type, so end here
                            retries = 0;
                            log.debug(`[${this.name}] timeout = ${timeout}`);
                            this.heartbeatInterval = setTimeout(beat, timeout);
                            return;
                        }
                    }

                } else if (this.type === "steam") {
                    const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
                    const steamAPIKey = await setting("steamAPIKey");
                    const filter = `addr\\${this.hostname}:${this.port}`;

                    if (!steamAPIKey) {
                        throw new Error("Steam API Key not found");
                    }

                    let res = await axios.get(steamApiUrl, {
                        timeout: this.interval * 1000 * 0.8,
                        headers: {
                            "Accept": "*/*",
                            "User-Agent": "Uptime-Kuma/" + version,
                        },
                        httpsAgent: new https.Agent({
                            maxCachedSessions: 0,      // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
                            rejectUnauthorized: !this.getIgnoreTls(),
                        }),
                        maxRedirects: this.maxredirects,
                        validateStatus: (status) => {
                            return checkStatusCode(status, this.getAcceptedStatuscodes());
                        },
                        params: {
                            filter: filter,
                            key: steamAPIKey,
                        }
                    });

                    if (res.data.response && res.data.response.servers && res.data.response.servers.length > 0) {
                        bean.status = UP;
                        bean.msg = res.data.response.servers[0].name;

                        try {
                            bean.ping = await ping(this.hostname);
                        } catch (_) { }
                    } else {

Review comment (bug):
The math here is not correct - for example - I apologize if this ends up confusing

beatInterval = 1 second

So assuming the ticks are every second interval begins at 10:00:00.000  - that should mean that pushes arriving at 10:00:00.000, 10:00:00.800 and 10:00:02:200 should work because the first one needs to arrive by 10:00:00.000 and the second by 10:00:01.000 and the third at 10:00:02.000 but the third one is valid because we are allowing for a "buffer" now of 1 second.

The math here however doesn't work - the second one arrives at 10:00:00.800 meaning that it has a timeout of 1 * 1000 - 800 (difference between the first two ticks) - that equals 200 which means line 367 sets the timeout to 1200ms which means at 10:00:02.000 we will mark it as down before the third push arrives within the correct allowed window (1400 ms later). 

The same type of issue applies to something like pushes at 10:00:00.000, 10:00:01.200 and 10:00:02.600 because of the logic of line 365 

I had believed that line 363 should be

`let timeout = beatInterval * 2 * 1000 - msSinceLastBeat  + bufferTime`

That gives us (going back to the ifrst set of pushes)

1 * 2 * 1000 - 800 + 1000 = 2200ms for the 10:00:00.800 push (which would be correct)
1 * 2 * 1000 - 1400 + 1000 = 1600ms for the 10:00:02.200 push (which is incorrect)

So at this point I know the math is incorrect as is - but I do not have something that appears to work properly for all cases.


Code after:
                            this.id
                        ]);
                    }

                    bean.msg = dnsMessage;
                    bean.status = UP;
                } else if (this.type === "push") {      // Type: Push
                    log.debug("monitor", `[${this.name}] Checking monitor at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
                    const bufferTime = 1000; // 1s buffer to accommodate clock differences

                    if (previousBeat) {
                        const msSinceLastBeat = dayjs.utc().valueOf() - dayjs.utc(previousBeat.time).valueOf();

                        log.debug("monitor", `[${this.name}] msSinceLastBeat = ${msSinceLastBeat}`);

                        // If the previous beat was down or pending we use the regular
                        // beatInterval/retryInterval in the setTimeout further below
                        if (previousBeat.status !== UP || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
                            throw new Error("No heartbeat in the time window");
                        } else {
                            let timeout = beatInterval * 1000 - msSinceLastBeat;
                            if (timeout < 0) {
                                timeout = bufferTime;
                            } else {
                                timeout += bufferTime;
                            }
                            // No need to insert successful heartbeat for push type, so end here
                            retries = 0;
                            log.debug("monitor", `[${this.name}] timeout = ${timeout}`);
                            this.heartbeatInterval = setTimeout(beat, timeout);
                            return;
                        }
                    } else {
                        throw new Error("No heartbeat in the time window");
                    }

                } else if (this.type === "steam") {
                    const steamApiUrl = "https://api.steampowered.com/IGameServersService/GetServerList/v1/";
                    const steamAPIKey = await setting("steamAPIKey");
                    const filter = `addr\\${this.hostname}:${this.port}`;

                    if (!steamAPIKey) {
                        throw new Error("Steam API Key not found");
                    }

                    let res = await axios.get(steamApiUrl, {
                        timeout: this.interval * 1000 * 0.8,
                        headers: {
                            "Accept": "*/*",
                            "User-Agent": "Uptime-Kuma/" + version,
                        },