fix(push): reset retries after maxretries in push route

Fix push monitor retry state transitions when previous heartbeat is PENDING and retries reach maxretries. Add backend test covering /api/push retry behavior.
This commit is contained in:
john0030710 2026-01-13 05:26:21 +01:00
parent 4de99eb851
commit fe19f89c9d
2 changed files with 127 additions and 5 deletions

View File

@ -610,15 +610,22 @@ function determineStatus(status, previousHeartbeat, maxretries, isUpsideDown, be
bean.retries = 0;
bean.status = DOWN;
}
} else if (previousHeartbeat.status === PENDING && status === DOWN && previousHeartbeat.retries < maxretries) {
// Retries available
bean.retries = previousHeartbeat.retries + 1;
bean.status = PENDING;
} else if (previousHeartbeat.status === PENDING && status === DOWN) {
// Still down while pending
if (maxretries > 0 && previousHeartbeat.retries < maxretries) {
// Retries available
bean.retries = previousHeartbeat.retries + 1;
bean.status = PENDING;
} else {
// No more retries
bean.retries = 0;
bean.status = DOWN;
}
} else {
// No more retries or not pending
if (status === DOWN) {
bean.retries = previousHeartbeat.retries + 1;
bean.status = status;
bean.retries = 0;
} else {
bean.retries = 0;
bean.status = status;

View File

@ -0,0 +1,115 @@
process.env.NODE_ENV = "development";
process.env.UPTIME_KUMA_HIDE_LOG = ["info_db", "info_server"].join(",");
const { describe, test, before, after, mock } = require("node:test");
const assert = require("node:assert");
const express = require("express");
const dayjs = require("dayjs");
dayjs.extend(require("dayjs/plugin/utc"));
const TestDB = require("../mock-testdb");
const { R } = require("redbean-node");
const Monitor = require("../../server/model/monitor");
const { Settings } = require("../../server/settings");
const { Prometheus } = require("../../server/prometheus");
const testDb = new TestDB();
let userCounter = 0;
const isoNow = () => R.isoDateTimeMillis(dayjs.utc());
const createEntity = async (type, data) => {
const entity = R.dispense(type);
Object.assign(entity, data);
await R.store(entity);
return entity;
};
const createUser = () => createEntity("user", {
username: `test-${++userCounter}-${Date.now()}`,
password: "Tellorian2003105$",
active: 1
});
const createPushMonitor = (user, pushToken, maxretries) => createEntity("monitor", {
name: "Push Monitor",
active: 1,
user_id: user.id,
type: "push",
push_token: pushToken,
maxretries,
interval: 60
});
const createHeartbeat = ({ monitorId, status, retries, time }) => createEntity("heartbeat", {
important: 0,
monitor_id: monitorId,
status,
msg: "No heartbeat in the time window",
time,
ping: null,
duration: 0,
down_count: 0,
retries
});
const startApiApp = () => new Promise((resolve) => {
const router = require("../../server/routers/api-router");
const app = express().use(router);
const server = app.listen(0, () => {
const { port } = server.address();
resolve({ server, url: `http://127.0.0.1:${port}` });
});
});
describe("Push API determineStatus retries", () => {
let api;
before(async () => {
await testDb.create();
// Avoid side effects from notifications in /api/push
mock.method(Monitor, "sendNotification", async () => {});
// Prometheus metrics are not initialized in backend-test environment
mock.method(Prometheus.prototype, "update", async () => {});
api = await startApiApp();
});
after(async () => {
if (api?.server) {
api.server.close();
}
Settings.stopCacheCleaner();
mock.restoreAll();
await testDb.destroy();
});
test("PENDING + retries >= maxretries + status=down => DOWN and retries reset to 0", async () => {
const monitor = await createPushMonitor(await createUser(), "token-1", 3);
await createHeartbeat({ monitorId: monitor.id, status: 2, retries: 3, time: isoNow() });
const res = await fetch(`${api.url}/api/push/token-1?status=down&msg=test`);
assert.strictEqual(res.ok, true);
const latest = await R.findOne("heartbeat", " monitor_id = ? ORDER BY id DESC", [monitor.id]);
assert.ok(latest);
assert.strictEqual(latest.status, 0);
assert.strictEqual(latest.retries, 0);
});
test("PENDING + retries < maxretries + status=down => stays PENDING and retries increments", async () => {
const monitor = await createPushMonitor(await createUser(), "token-2", 3);
await createHeartbeat({ monitorId: monitor.id, status: 2, retries: 1, time: isoNow() });
const res = await fetch(`${api.url}/api/push/token-2?status=down&msg=test`);
assert.strictEqual(res.ok, true);
const latest = await R.findOne("heartbeat", " monitor_id = ? ORDER BY id DESC", [monitor.id]);
assert.ok(latest);
assert.strictEqual(latest.status, 2);
assert.strictEqual(latest.retries, 2);
});
});