From 1e2b80b4cf35a9ca78ee9135dc1d66a2faa105e7 Mon Sep 17 00:00:00 2001 From: Ian Macabulos Date: Sat, 17 Jan 2026 11:34:03 +0800 Subject: [PATCH] fix: reset retries for push monitors when status is UP (#6663) --- server/model/monitor.js | 6 +++ test/backend-test/issue-6663-test.js | 62 ++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 test/backend-test/issue-6663-test.js diff --git a/server/model/monitor.js b/server/model/monitor.js index e01977133..7a18f69cf 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -449,6 +449,12 @@ class Monitor extends BeanModel { previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [this.id]); if (previousBeat) { retries = previousBeat.retries; + + // If the monitor is currently UP, the retry counter must be reset to 0. + // This prevents carrying over old retry counts if the push handler didn't reset them. + if (previousBeat.status === UP) { + retries = 0; + } } } diff --git a/test/backend-test/issue-6663-test.js b/test/backend-test/issue-6663-test.js new file mode 100644 index 000000000..6377ae4fb --- /dev/null +++ b/test/backend-test/issue-6663-test.js @@ -0,0 +1,62 @@ +const { R } = require("redbean-node"); +const { UP } = require("../../src/util"); +const dayjs = require("dayjs"); +const { describe, test, beforeEach, afterEach } = require("node:test"); +const assert = require("node:assert"); + +// Load server but don't let it hang the process +require("../../server/server"); + +describe("Issue #6663: Push Monitor Retry Reset", () => { + let monitorId; + + beforeEach(async () => { + // Wait for database to be ready before starting + // (A simple delay to ensure server startup logic processes) + await new Promise(resolve => setTimeout(resolve, 1000)); + + monitorId = await R.store(R.dispense("monitor")); + await R.exec("UPDATE monitor SET type = 'push', interval = 60, maxretries = 5, active = 1 WHERE id = ?", [monitorId]); + }); + + afterEach(async () => { + if (monitorId) { + await R.exec("DELETE FROM monitor WHERE id = ?", [monitorId]); + await R.exec("DELETE FROM heartbeat WHERE monitor_id = ?", [monitorId]); + } + }); + + test("Should reset retries to 0 if previous beat was UP", async () => { + const monitor = await R.load("monitor", monitorId); + + // 1. Simulate the 'Buggy' State (UP but with high retries) + let buggyBeat = R.dispense("heartbeat"); + buggyBeat.monitor_id = monitorId; + buggyBeat.status = UP; + buggyBeat.time = R.isoDateTimeMillis(dayjs().subtract(2, "minutes")); + buggyBeat.retries = 20; + await R.store(buggyBeat); + + // 2. Run the logic (simulating the fix) + let previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [monitor.id]); + let retries = previousBeat.retries; + + // --- THE FIX LOGIC --- + if (previousBeat.status === UP) { + retries = 0; + } + // --------------------- + + // 3. Simulate next tick + if (retries < monitor.maxretries) { + retries++; + } + + console.log(`Test Result: Previous Retries: ${previousBeat.retries} -> New Retries: ${retries}`); + + assert.strictEqual(retries, 1, "Retries should have reset to 1 (0+1), but it continued counting!"); + + // Force exit because server.js keeps the process alive + process.exit(0); + }); +}); \ No newline at end of file