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:
Nelson Chan 2023-11-24 18:11:36 +08:00 committed by GitHub
parent ac452bbcb9
commit 67250d6302
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 125 additions and 34 deletions

View 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");
});
};

View file

@ -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,
}; };
} }

View file

@ -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
]); ]);
} }

View file

@ -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;