mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-23 14:54:05 +00:00
Feat: Retries persistence (#3814)
* Feat: Retries persistence * Fix: Set duration for first beat of push monitor * Feat: Update UptimeCalculator in push route * Fix: Handle resend in push route * Chore: Remove debug log
This commit is contained in:
parent
ac452bbcb9
commit
67250d6302
4 changed files with 125 additions and 34 deletions
15
db/knex_migrations/2023-09-29-0000-heartbeat-retires.js
Normal file
15
db/knex_migrations/2023-09-29-0000-heartbeat-retires.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
exports.up = function (knex) {
|
||||||
|
// Add new column heartbeat.retries
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("heartbeat", function (table) {
|
||||||
|
table.integer("retries").notNullable().defaultTo(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("heartbeat", function (table) {
|
||||||
|
table.dropColumn("retries");
|
||||||
|
});
|
||||||
|
};
|
|
@ -29,13 +29,14 @@ class Heartbeat extends BeanModel {
|
||||||
*/
|
*/
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
monitorID: this.monitor_id,
|
monitorID: this._monitorId,
|
||||||
status: this.status,
|
status: this._status,
|
||||||
time: this.time,
|
time: this._time,
|
||||||
msg: this.msg,
|
msg: this._msg,
|
||||||
ping: this.ping,
|
ping: this._ping,
|
||||||
important: this.important,
|
important: this._important,
|
||||||
duration: this.duration,
|
duration: this._duration,
|
||||||
|
retries: this._retries,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -361,6 +361,9 @@ class Monitor extends BeanModel {
|
||||||
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
|
||||||
this.id,
|
this.id,
|
||||||
]);
|
]);
|
||||||
|
if (previousBeat) {
|
||||||
|
retries = previousBeat.retries;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFirstBeat = !previousBeat;
|
const isFirstBeat = !previousBeat;
|
||||||
|
@ -632,6 +635,7 @@ class Monitor extends BeanModel {
|
||||||
// If the previous beat was down or pending we use the regular
|
// If the previous beat was down or pending we use the regular
|
||||||
// beatInterval/retryInterval in the setTimeout further below
|
// beatInterval/retryInterval in the setTimeout further below
|
||||||
if (previousBeat.status !== (this.isUpsideDown() ? DOWN : UP) || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
|
if (previousBeat.status !== (this.isUpsideDown() ? DOWN : UP) || msSinceLastBeat > beatInterval * 1000 + bufferTime) {
|
||||||
|
bean.duration = Math.round(msSinceLastBeat / 1000);
|
||||||
throw new Error("No heartbeat in the time window");
|
throw new Error("No heartbeat in the time window");
|
||||||
} else {
|
} else {
|
||||||
let timeout = beatInterval * 1000 - msSinceLastBeat;
|
let timeout = beatInterval * 1000 - msSinceLastBeat;
|
||||||
|
@ -647,6 +651,7 @@ class Monitor extends BeanModel {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
bean.duration = beatInterval;
|
||||||
throw new Error("No heartbeat in the time window");
|
throw new Error("No heartbeat in the time window");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -915,9 +920,14 @@ class Monitor extends BeanModel {
|
||||||
} else if ((this.maxretries > 0) && (retries < this.maxretries)) {
|
} else if ((this.maxretries > 0) && (retries < this.maxretries)) {
|
||||||
retries++;
|
retries++;
|
||||||
bean.status = PENDING;
|
bean.status = PENDING;
|
||||||
|
} else {
|
||||||
|
// Continue counting retries during DOWN
|
||||||
|
retries++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bean.retries = retries;
|
||||||
|
|
||||||
log.debug("monitor", `[${this.name}] Check isImportant`);
|
log.debug("monitor", `[${this.name}] Check isImportant`);
|
||||||
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
let isImportant = Monitor.isImportantBeat(isFirstBeat, previousBeat?.status, bean.status);
|
||||||
|
|
||||||
|
@ -1437,10 +1447,7 @@ class Monitor extends BeanModel {
|
||||||
* @returns {Promise<LooseObject<any>>} Previous heartbeat
|
* @returns {Promise<LooseObject<any>>} Previous heartbeat
|
||||||
*/
|
*/
|
||||||
static async getPreviousHeartbeat(monitorID) {
|
static async getPreviousHeartbeat(monitorID) {
|
||||||
return await R.getRow(`
|
return await R.findOne("heartbeat", " id = (select MAX(id) from heartbeat where monitor_id = ?)", [
|
||||||
SELECT ping, status, time FROM heartbeat
|
|
||||||
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
|
|
||||||
`, [
|
|
||||||
monitorID
|
monitorID
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,38 +64,57 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
|
|
||||||
const previousHeartbeat = await Monitor.getPreviousHeartbeat(monitor.id);
|
const previousHeartbeat = await Monitor.getPreviousHeartbeat(monitor.id);
|
||||||
|
|
||||||
if (monitor.isUpsideDown()) {
|
|
||||||
status = flipStatus(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
let isFirstBeat = true;
|
let isFirstBeat = true;
|
||||||
let previousStatus = status;
|
|
||||||
let duration = 0;
|
|
||||||
|
|
||||||
let bean = R.dispense("heartbeat");
|
let bean = R.dispense("heartbeat");
|
||||||
bean.time = R.isoDateTimeMillis(dayjs.utc());
|
bean.time = R.isoDateTimeMillis(dayjs.utc());
|
||||||
|
bean.monitor_id = monitor.id;
|
||||||
|
bean.ping = ping;
|
||||||
|
bean.msg = msg;
|
||||||
|
bean.downCount = previousHeartbeat?.downCount || 0;
|
||||||
|
|
||||||
if (previousHeartbeat) {
|
if (previousHeartbeat) {
|
||||||
isFirstBeat = false;
|
isFirstBeat = false;
|
||||||
previousStatus = previousHeartbeat.status;
|
bean.duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
||||||
duration = dayjs(bean.time).diff(dayjs(previousHeartbeat.time), "second");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await Monitor.isUnderMaintenance(monitor.id)) {
|
if (await Monitor.isUnderMaintenance(monitor.id)) {
|
||||||
msg = "Monitor under maintenance";
|
msg = "Monitor under maintenance";
|
||||||
status = MAINTENANCE;
|
bean.status = MAINTENANCE;
|
||||||
|
} else {
|
||||||
|
determineStatus(status, previousHeartbeat, monitor.maxretries, monitor.isUpsideDown(), bean);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
|
// Calculate uptime
|
||||||
log.debug("router", "PreviousStatus: " + previousStatus);
|
let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitor.id);
|
||||||
log.debug("router", "Current Status: " + status);
|
let endTimeDayjs = await uptimeCalculator.update(bean.status, parseFloat(bean.ping));
|
||||||
|
bean.end_time = R.isoDateTimeMillis(endTimeDayjs);
|
||||||
|
|
||||||
bean.important = Monitor.isImportantBeat(isFirstBeat, previousStatus, status);
|
log.debug("router", `/api/push/ called at ${dayjs().format("YYYY-MM-DD HH:mm:ss.SSS")}`);
|
||||||
bean.monitor_id = monitor.id;
|
log.debug("router", "PreviousStatus: " + previousHeartbeat?.status);
|
||||||
bean.status = status;
|
log.debug("router", "Current Status: " + bean.status);
|
||||||
bean.msg = msg;
|
|
||||||
bean.ping = ping;
|
bean.important = Monitor.isImportantBeat(isFirstBeat, previousHeartbeat?.status, status);
|
||||||
bean.duration = duration;
|
|
||||||
|
if (Monitor.isImportantForNotification(isFirstBeat, previousHeartbeat?.status, status)) {
|
||||||
|
// Reset down count
|
||||||
|
bean.downCount = 0;
|
||||||
|
|
||||||
|
log.debug("monitor", `[${this.name}] sendNotification`);
|
||||||
|
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
||||||
|
} else {
|
||||||
|
if (bean.status === DOWN && this.resendInterval > 0) {
|
||||||
|
++bean.downCount;
|
||||||
|
if (bean.downCount >= this.resendInterval) {
|
||||||
|
// Send notification again, because we are still DOWN
|
||||||
|
log.debug("monitor", `[${this.name}] sendNotification again: Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||||
|
await Monitor.sendNotification(isFirstBeat, this, bean);
|
||||||
|
|
||||||
|
// Reset down count
|
||||||
|
bean.downCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
|
@ -107,11 +126,6 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
response.json({
|
response.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Monitor.isImportantForNotification(isFirstBeat, previousStatus, status)) {
|
|
||||||
await Monitor.sendNotification(isFirstBeat, monitor, bean);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
response.status(404).json({
|
response.status(404).json({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
@ -562,4 +576,58 @@ router.get("/api/badge/:id/response", cache("5 minutes"), async (request, respon
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the status of the next beat in the push route handling.
|
||||||
|
* @param {string} status - The reported new status.
|
||||||
|
* @param {object} previousHeartbeat - The previous heartbeat object.
|
||||||
|
* @param {number} maxretries - The maximum number of retries allowed.
|
||||||
|
* @param {boolean} isUpsideDown - Indicates if the monitor is upside down.
|
||||||
|
* @param {object} bean - The new heartbeat object.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function determineStatus(status, previousHeartbeat, maxretries, isUpsideDown, bean) {
|
||||||
|
if (isUpsideDown) {
|
||||||
|
status = flipStatus(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousHeartbeat) {
|
||||||
|
if (previousHeartbeat.status === UP && status === DOWN) {
|
||||||
|
// Going Down
|
||||||
|
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 if (previousHeartbeat.status === PENDING && status === DOWN && previousHeartbeat.retries < maxretries) {
|
||||||
|
// Retries available
|
||||||
|
bean.retries = previousHeartbeat.retries + 1;
|
||||||
|
bean.status = PENDING;
|
||||||
|
} else {
|
||||||
|
// No more retries or not pending
|
||||||
|
if (status === DOWN) {
|
||||||
|
bean.retries = previousHeartbeat.retries + 1;
|
||||||
|
bean.status = status;
|
||||||
|
} else {
|
||||||
|
bean.retries = 0;
|
||||||
|
bean.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// First beat?
|
||||||
|
if (status === DOWN && maxretries > 0) {
|
||||||
|
// Retries available
|
||||||
|
bean.retries = 1;
|
||||||
|
bean.status = PENDING;
|
||||||
|
} else {
|
||||||
|
// Retires not enabled
|
||||||
|
bean.retries = 0;
|
||||||
|
bean.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
Loading…
Reference in a new issue