Uptime calculation improvement and 1-year uptime (#2750)
parent
eec221247f
commit
076331bf00
@ -0,0 +1,41 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema
|
||||
.createTable("stat_minutely", function (table) {
|
||||
table.increments("id");
|
||||
table.comment("This table contains the minutely aggregate statistics for each monitor");
|
||||
table.integer("monitor_id").unsigned().notNullable()
|
||||
.references("id").inTable("monitor")
|
||||
.onDelete("CASCADE")
|
||||
.onUpdate("CASCADE");
|
||||
table.integer("timestamp")
|
||||
.notNullable()
|
||||
.comment("Unix timestamp rounded down to the nearest minute");
|
||||
table.float("ping").notNullable().comment("Average ping in milliseconds");
|
||||
table.smallint("up").notNullable();
|
||||
table.smallint("down").notNullable();
|
||||
|
||||
table.unique([ "monitor_id", "timestamp" ]);
|
||||
})
|
||||
.createTable("stat_daily", function (table) {
|
||||
table.increments("id");
|
||||
table.comment("This table contains the daily aggregate statistics for each monitor");
|
||||
table.integer("monitor_id").unsigned().notNullable()
|
||||
.references("id").inTable("monitor")
|
||||
.onDelete("CASCADE")
|
||||
.onUpdate("CASCADE");
|
||||
table.integer("timestamp")
|
||||
.notNullable()
|
||||
.comment("Unix timestamp rounded down to the nearest day");
|
||||
table.float("ping").notNullable().comment("Average ping in milliseconds");
|
||||
table.smallint("up").notNullable();
|
||||
table.smallint("down").notNullable();
|
||||
|
||||
table.unique([ "monitor_id", "timestamp" ]);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema
|
||||
.dropTable("stat_minutely")
|
||||
.dropTable("stat_daily");
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
exports.up = function (knex) {
|
||||
// Add new column heartbeat.end_time
|
||||
return knex.schema
|
||||
.alterTable("heartbeat", function (table) {
|
||||
table.datetime("end_time").nullable().defaultTo(null);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
// Rename heartbeat.start_time to heartbeat.time
|
||||
return knex.schema
|
||||
.alterTable("heartbeat", function (table) {
|
||||
table.dropColumn("end_time");
|
||||
});
|
||||
};
|
@ -1,51 +0,0 @@
|
||||
const { log } = require("../src/util");
|
||||
class UptimeCacheList {
|
||||
/**
|
||||
* list[monitorID][duration]
|
||||
*/
|
||||
static list = {};
|
||||
|
||||
/**
|
||||
* Get the uptime for a specific period
|
||||
* @param {number} monitorID ID of monitor to query
|
||||
* @param {number} duration Duration to query
|
||||
* @returns {(number|null)} Uptime for provided duration, if it exists
|
||||
*/
|
||||
static getUptime(monitorID, duration) {
|
||||
if (UptimeCacheList.list[monitorID] && UptimeCacheList.list[monitorID][duration]) {
|
||||
log.debug("UptimeCacheList", "getUptime: " + monitorID + " " + duration);
|
||||
return UptimeCacheList.list[monitorID][duration];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add uptime for specified monitor
|
||||
* @param {number} monitorID ID of monitor to insert for
|
||||
* @param {number} duration Duration to insert for
|
||||
* @param {number} uptime Uptime to add
|
||||
* @returns {void}
|
||||
*/
|
||||
static addUptime(monitorID, duration, uptime) {
|
||||
log.debug("UptimeCacheList", "addUptime: " + monitorID + " " + duration);
|
||||
if (!UptimeCacheList.list[monitorID]) {
|
||||
UptimeCacheList.list[monitorID] = {};
|
||||
}
|
||||
UptimeCacheList.list[monitorID][duration] = uptime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cache for specified monitor
|
||||
* @param {number} monitorID ID of monitor to clear
|
||||
* @returns {void}
|
||||
*/
|
||||
static clearCache(monitorID) {
|
||||
log.debug("UptimeCacheList", "clearCache: " + monitorID);
|
||||
delete UptimeCacheList.list[monitorID];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
UptimeCacheList,
|
||||
};
|
@ -0,0 +1,483 @@
|
||||
const dayjs = require("dayjs");
|
||||
const { UP, MAINTENANCE, DOWN, PENDING } = require("../src/util");
|
||||
const { LimitQueue } = require("./utils/limit-queue");
|
||||
const { log } = require("../src/util");
|
||||
const { R } = require("redbean-node");
|
||||
|
||||
/**
|
||||
* Calculates the uptime of a monitor.
|
||||
*/
|
||||
class UptimeCalculator {
|
||||
|
||||
static list = {};
|
||||
|
||||
/**
|
||||
* For testing purposes, we can set the current date to a specific date.
|
||||
* @type {dayjs.Dayjs}
|
||||
*/
|
||||
static currentDate = null;
|
||||
|
||||
monitorID;
|
||||
|
||||
/**
|
||||
* Recent 24-hour uptime, each item is a 1-minute interval
|
||||
* Key: {number} DivisionKey
|
||||
*/
|
||||
minutelyUptimeDataList = new LimitQueue(24 * 60);
|
||||
|
||||
/**
|
||||
* Daily uptime data,
|
||||
* Key: {number} DailyKey
|
||||
*/
|
||||
dailyUptimeDataList = new LimitQueue(365);
|
||||
|
||||
lastDailyUptimeData = null;
|
||||
lastUptimeData = null;
|
||||
|
||||
lastDailyStatBean = null;
|
||||
lastMinutelyStatBean = null;
|
||||
|
||||
/**
|
||||
* @param monitorID
|
||||
* @returns {Promise<UptimeCalculator>}
|
||||
*/
|
||||
static async getUptimeCalculator(monitorID) {
|
||||
if (!UptimeCalculator.list[monitorID]) {
|
||||
UptimeCalculator.list[monitorID] = new UptimeCalculator();
|
||||
await UptimeCalculator.list[monitorID].init(monitorID);
|
||||
}
|
||||
return UptimeCalculator.list[monitorID];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param monitorID
|
||||
*/
|
||||
static async remove(monitorID) {
|
||||
delete UptimeCalculator.list[monitorID];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor() {
|
||||
if (process.env.TEST_BACKEND) {
|
||||
// Override the getCurrentDate() method to return a specific date
|
||||
// Only for testing
|
||||
this.getCurrentDate = () => {
|
||||
if (UptimeCalculator.currentDate) {
|
||||
return UptimeCalculator.currentDate;
|
||||
} else {
|
||||
return dayjs.utc();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} monitorID
|
||||
*/
|
||||
async init(monitorID) {
|
||||
this.monitorID = monitorID;
|
||||
|
||||
let now = this.getCurrentDate();
|
||||
|
||||
// Load minutely data from database (recent 24 hours only)
|
||||
let minutelyStatBeans = await R.find("stat_minutely", " monitor_id = ? AND timestamp > ? ORDER BY timestamp", [
|
||||
monitorID,
|
||||
this.getMinutelyKey(now.subtract(24, "hour")),
|
||||
]);
|
||||
|
||||
for (let bean of minutelyStatBeans) {
|
||||
let key = bean.timestamp;
|
||||
this.minutelyUptimeDataList.push(key, {
|
||||
up: bean.up,
|
||||
down: bean.down,
|
||||
avgPing: bean.ping,
|
||||
});
|
||||
}
|
||||
|
||||
// Load daily data from database (recent 365 days only)
|
||||
let dailyStatBeans = await R.find("stat_daily", " monitor_id = ? AND timestamp > ? ORDER BY timestamp", [
|
||||
monitorID,
|
||||
this.getDailyKey(now.subtract(365, "day").unix()),
|
||||
]);
|
||||
|
||||
for (let bean of dailyStatBeans) {
|
||||
let key = bean.timestamp;
|
||||
this.dailyUptimeDataList.push(key, {
|
||||
up: bean.up,
|
||||
down: bean.down,
|
||||
avgPing: bean.ping,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} status status
|
||||
* @param {number} ping Ping
|
||||
* @returns {dayjs.Dayjs} date
|
||||
* @throws {Error} Invalid status
|
||||
*/
|
||||
async update(status, ping = 0) {
|
||||
let date = this.getCurrentDate();
|
||||
|
||||
// Don't count MAINTENANCE into uptime
|
||||
if (status === MAINTENANCE) {
|
||||
return date;
|
||||
}
|
||||
|
||||
let flatStatus = this.flatStatus(status);
|
||||
|
||||
if (flatStatus === DOWN && ping > 0) {
|
||||
log.warn("uptime-calc", "The ping is not effective when the status is DOWN");
|
||||
}
|
||||
|
||||
let divisionKey = this.getMinutelyKey(date);
|
||||
let dailyKey = this.getDailyKey(divisionKey);
|
||||
|
||||
let minutelyData = this.minutelyUptimeDataList[divisionKey];
|
||||
let dailyData = this.dailyUptimeDataList[dailyKey];
|
||||
|
||||
if (flatStatus === UP) {
|
||||
minutelyData.up += 1;
|
||||
dailyData.up += 1;
|
||||
|
||||
// Only UP status can update the ping
|
||||
if (!isNaN(ping)) {
|
||||
// Add avg ping
|
||||
// The first beat of the minute, the ping is the current ping
|
||||
if (minutelyData.up === 1) {
|
||||
minutelyData.avgPing = ping;
|
||||
} else {
|
||||
minutelyData.avgPing = (minutelyData.avgPing * (minutelyData.up - 1) + ping) / minutelyData.up;
|
||||
}
|
||||
|
||||
// Add avg ping (daily)
|
||||
// The first beat of the day, the ping is the current ping
|
||||
if (minutelyData.up === 1) {
|
||||
dailyData.avgPing = ping;
|
||||
} else {
|
||||
dailyData.avgPing = (dailyData.avgPing * (dailyData.up - 1) + ping) / dailyData.up;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
minutelyData.down += 1;
|
||||
dailyData.down += 1;
|
||||
}
|
||||
|
||||
if (dailyData !== this.lastDailyUptimeData) {
|
||||
this.lastDailyUptimeData = dailyData;
|
||||
}
|
||||
|
||||
if (minutelyData !== this.lastUptimeData) {
|
||||
this.lastUptimeData = minutelyData;
|
||||
}
|
||||
|
||||
// Don't store data in test mode
|
||||
if (process.env.TEST_BACKEND) {
|
||||
log.debug("uptime-calc", "Skip storing data in test mode");
|
||||
return date;
|
||||
}
|
||||
|
||||
let dailyStatBean = await this.getDailyStatBean(dailyKey);
|
||||
dailyStatBean.up = dailyData.up;
|
||||
dailyStatBean.down = dailyData.down;
|
||||
dailyStatBean.ping = dailyData.ping;
|
||||
await R.store(dailyStatBean);
|
||||
|
||||
let minutelyStatBean = await this.getMinutelyStatBean(divisionKey);
|
||||
minutelyStatBean.up = minutelyData.up;
|
||||
minutelyStatBean.down = minutelyData.down;
|
||||
minutelyStatBean.ping = minutelyData.ping;
|
||||
await R.store(minutelyStatBean);
|
||||
|
||||
// Remove the old data
|
||||
log.debug("uptime-calc", "Remove old data");
|
||||
await R.exec("DELETE FROM stat_minutely WHERE monitor_id = ? AND timestamp < ?", [
|
||||
this.monitorID,
|
||||
this.getMinutelyKey(date.subtract(24, "hour")),
|
||||
]);
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the daily stat bean
|
||||
* @param {number} timestamp milliseconds
|
||||
* @returns {Promise<import("redbean-node").Bean>} stat_daily bean
|
||||
*/
|
||||
async getDailyStatBean(timestamp) {
|
||||
if (this.lastDailyStatBean && this.lastDailyStatBean.timestamp === timestamp) {
|
||||
return this.lastDailyStatBean;
|
||||
}
|
||||
|
||||
let bean = await R.findOne("stat_daily", " monitor_id = ? AND timestamp = ?", [
|
||||
this.monitorID,
|
||||
timestamp,
|
||||
]);
|
||||
|
||||
if (!bean) {
|
||||
bean = R.dispense("stat_daily");
|
||||
bean.monitor_id = this.monitorID;
|
||||
bean.timestamp = timestamp;
|
||||
}
|
||||
|
||||
this.lastDailyStatBean = bean;
|
||||
return this.lastDailyStatBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the minutely stat bean
|
||||
* @param {number} timestamp milliseconds
|
||||
* @returns {Promise<import("redbean-node").Bean>} stat_minutely bean
|
||||
*/
|
||||
async getMinutelyStatBean(timestamp) {
|
||||
if (this.lastMinutelyStatBean && this.lastMinutelyStatBean.timestamp === timestamp) {
|
||||
return this.lastMinutelyStatBean;
|
||||
}
|
||||
|
||||
let bean = await R.findOne("stat_minutely", " monitor_id = ? AND timestamp = ?", [
|
||||
this.monitorID,
|
||||
timestamp,
|
||||
]);
|
||||
|
||||
if (!bean) {
|
||||
bean = R.dispense("stat_minutely");
|
||||
bean.monitor_id = this.monitorID;
|
||||
bean.timestamp = timestamp;
|
||||
}
|
||||
|
||||
this.lastMinutelyStatBean = bean;
|
||||
return this.lastMinutelyStatBean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {dayjs.Dayjs} date The heartbeat date
|
||||
* @returns {number} Timestamp
|
||||
*/
|
||||
getMinutelyKey(date) {
|
||||
// Convert the current date to the nearest minute (e.g. 2021-01-01 12:34:56 -> 2021-01-01 12:34:00)
|
||||
date = date.startOf("minute");
|
||||
|
||||
// Convert to timestamp in second
|
||||
let divisionKey = date.unix();
|
||||
|
||||
if (! (divisionKey in this.minutelyUptimeDataList)) {
|
||||
let last = this.minutelyUptimeDataList.getLastKey();
|
||||
if (last && last > divisionKey) {
|
||||
log.warn("uptime-calc", "The system time has been changed? The uptime data may be inaccurate.");
|
||||
}
|
||||
|
||||
this.minutelyUptimeDataList.push(divisionKey, {
|
||||
up: 0,
|
||||
down: 0,
|
||||
avgPing: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return divisionKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert timestamp to daily key
|
||||
* @param {number} timestamp Timestamp
|
||||
* @returns {number} Timestamp
|
||||
*/
|
||||
getDailyKey(timestamp) {
|
||||
let date = dayjs.unix(timestamp);
|
||||
|
||||
// Convert the date to the nearest day (e.g. 2021-01-01 12:34:56 -> 2021-01-01 00:00:00)
|
||||
// Considering if the user keep changing could affect the calculation, so use UTC time to avoid this problem.
|
||||
date = date.utc().startOf("day");
|
||||
let dailyKey = date.unix();
|
||||
|
||||
if (!this.dailyUptimeDataList[dailyKey]) {
|
||||
let last = this.dailyUptimeDataList.getLastKey();
|
||||
if (last && last > dailyKey) {
|
||||
log.warn("uptime-calc", "The system time has been changed? The uptime data may be inaccurate.");
|
||||
}
|
||||
|
||||
this.dailyUptimeDataList.push(dailyKey, {
|
||||
up: 0,
|
||||
down: 0,
|
||||
avgPing: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return dailyKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat status to UP or DOWN
|
||||
* @param {number} status
|
||||
* @returns {number}
|
||||
* @throws {Error} Invalid status
|
||||
*/
|
||||
flatStatus(status) {
|
||||
switch (status) {
|
||||
case UP:
|
||||
// case MAINTENANCE:
|
||||
return UP;
|
||||
case DOWN:
|
||||
case PENDING:
|
||||
return DOWN;
|
||||
}
|
||||
throw new Error("Invalid status");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} num
|
||||
* @param {string} type "day" | "minute"
|
||||
*/
|
||||
getData(num, type = "day") {
|
||||
let key;
|
||||
|
||||
if (type === "day") {
|
||||
key = this.getDailyKey(this.getCurrentDate().unix());
|
||||
} else {
|
||||
if (num > 24 * 60) {
|
||||
throw new Error("The maximum number of minutes is 1440");
|
||||
}
|
||||
key = this.getMinutelyKey(this.getCurrentDate());
|
||||
}
|
||||
|
||||
let total = {
|
||||
up: 0,
|
||||
down: 0,
|
||||
};
|
||||
|
||||
let totalPing = 0;
|
||||
let endTimestamp;
|
||||
|
||||
if (type === "day") {
|
||||
endTimestamp = key - 86400 * (num - 1);
|
||||
} else {
|
||||
endTimestamp = key - 60 * (num - 1);
|
||||
}
|
||||
|
||||
// Sum up all data in the specified time range
|
||||
while (key >= endTimestamp) {
|
||||
let data;
|
||||
|
||||
if (type === "day") {
|
||||
data = this.dailyUptimeDataList[key];
|
||||
} else {
|
||||
data = this.minutelyUptimeDataList[key];
|
||||
}
|
||||
|
||||
if (data) {
|
||||
total.up += data.up;
|
||||
total.down += data.down;
|
||||
totalPing += data.avgPing * data.up;
|
||||
}
|
||||
|
||||
// Previous day
|
||||
if (type === "day") {
|
||||
key -= 86400;
|
||||
} else {
|
||||
key -= 60;
|
||||
}
|
||||
}
|
||||
|
||||
let uptimeData = new UptimeDataResult();
|
||||
|
||||
if (total.up === 0 && total.down === 0) {
|
||||
if (type === "day" && this.lastDailyUptimeData) {
|
||||
total = this.lastDailyUptimeData;
|
||||
totalPing = total.avgPing * total.up;
|
||||
} else if (type === "minute" && this.lastUptimeData) {
|
||||
total = this.lastUptimeData;
|
||||
totalPing = total.avgPing * total.up;
|
||||
} else {
|
||||
uptimeData.uptime = 0;
|
||||
uptimeData.avgPing = null;
|
||||
return uptimeData;
|
||||
}
|
||||
}
|
||||
|
||||
let avgPing;
|
||||
|
||||
if (total.up === 0) {
|
||||
avgPing = null;
|
||||
} else {
|
||||
avgPing = totalPing / total.up;
|
||||
}
|
||||
|
||||
uptimeData.uptime = total.up / (total.up + total.down);
|
||||
uptimeData.avgPing = avgPing;
|
||||
return uptimeData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the uptime data by duration
|
||||
* @param {'24h'|'30d'|'1y'} duration Only accept 24h, 30d, 1y
|
||||
* @returns {UptimeDataResult} UptimeDataResult
|
||||
* @throws {Error} Invalid duration
|
||||
*/
|
||||
getDataByDuration(duration) {
|
||||
if (duration === "24h") {
|
||||
return this.get24Hour();
|
||||
} else if (duration === "30d") {
|
||||
return this.get30Day();
|
||||
} else if (duration === "1y") {
|
||||
return this.get1Year();
|
||||
} else {
|
||||
throw new Error("Invalid duration");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1440 = 24 * 60mins
|
||||
* @returns {UptimeDataResult} UptimeDataResult
|
||||
*/
|
||||
get24Hour() {
|
||||
return this.getData(1440, "minute");
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {UptimeDataResult} UptimeDataResult
|
||||
*/
|
||||
get7Day() {
|
||||
return this.getData(7);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {UptimeDataResult} UptimeDataResult
|
||||
*/
|
||||
get30Day() {
|
||||
return this.getData(30);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {UptimeDataResult} UptimeDataResult
|
||||
*/
|
||||
get1Year() {
|
||||
return this.getData(365);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {dayjs.Dayjs} Current date
|
||||
*/
|
||||
getCurrentDate() {
|
||||
return dayjs.utc();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class UptimeDataResult {
|
||||
/**
|
||||
* @type {number} Uptime
|
||||
*/
|
||||
uptime;
|
||||
|
||||
/**
|
||||
* @type {number} Average ping
|
||||
*/
|
||||
avgPing;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
UptimeCalculator,
|
||||
UptimeDataResult,
|
||||
};
|
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* An object that can be used as an array with a key
|
||||
* Like PHP's array
|
||||
*/
|
||||
class ArrayWithKey {
|
||||
__stack = [];
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
constructor() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
push(key, value) {
|
||||
this[key] = value;
|
||||
this.__stack.push(key);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
pop() {
|
||||
let key = this.__stack.pop();
|
||||
let prop = this[key];
|
||||
delete this[key];
|
||||
return prop;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
getLastKey() {
|
||||
if (this.__stack.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return this.__stack[this.__stack.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
shift() {
|
||||
let key = this.__stack.shift();
|
||||
let value = this[key];
|
||||
delete this[key];
|
||||
return {
|
||||
key,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
length() {
|
||||
return this.__stack.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last element
|
||||
* @returns {*|null} The last element, or null if the array is empty
|
||||
*/
|
||||
last() {
|
||||
let key = this.getLastKey();
|
||||
if (key === null) {
|
||||
return null;
|
||||
}
|
||||
return this[key];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ArrayWithKey
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
const { ArrayWithKey } = require("./array-with-key");
|
||||
|
||||
/**
|
||||
* Limit Queue
|
||||
* The first element will be removed when the length exceeds the limit
|
||||
*/
|
||||
class LimitQueue extends ArrayWithKey {
|
||||
|
||||
__limit;
|
||||
__onExceed = null;
|
||||
|
||||
/**
|
||||
* @param {number} limit
|
||||
*/
|
||||
constructor(limit) {
|
||||
super();
|
||||
this.__limit = limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
push(key, value) {
|
||||
super.push(key, value);
|
||||
if (this.length() > this.__limit) {
|
||||
let item = this.shift();
|
||||
if (this.__onExceed) {
|
||||
this.__onExceed(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
LimitQueue
|
||||
};
|
@ -0,0 +1,20 @@
|
||||
// Check Node.js version
|
||||
const semver = require("semver");
|
||||
const childProcess = require("child_process");
|
||||
|
||||
const nodeVersion = process.versions.node;
|
||||
console.log("Node.js version: " + nodeVersion);
|
||||
|
||||
|
||||
|
||||
// Node.js version >= 18
|
||||
if (semver.satisfies(nodeVersion, ">= 18")) {
|
||||
console.log("Use the native test runner: `node --test`");
|
||||
childProcess.execSync("npm run test-backend:18", { stdio: "inherit" });
|
||||
} else {
|
||||
// 14 - 16 here
|
||||
console.log("Use `test` package: `node--test`")
|
||||
childProcess.execSync("npm run test-backend:14", { stdio: "inherit" });
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,423 @@
|
||||
const semver = require("semver");
|
||||
let test;
|
||||
const nodeVersion = process.versions.node;
|
||||
// Node.js version >= 18
|
||||
if (semver.satisfies(nodeVersion, ">= 18")) {
|
||||
test = require("node:test");
|
||||
} else {
|
||||
test = require("test");
|
||||
}
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { UptimeCalculator } = require("../../server/uptime-calculator");
|
||||
const dayjs = require("dayjs");
|
||||
const { UP, DOWN, PENDING, MAINTENANCE } = require("../../src/util");
|
||||
dayjs.extend(require("dayjs/plugin/utc"));
|
||||
dayjs.extend(require("../../server/modules/dayjs/plugin/timezone"));
|
||||
dayjs.extend(require("dayjs/plugin/customParseFormat"));
|
||||
|
||||
test("Test Uptime Calculator - custom date", async (t) => {
|
||||
let c1 = new UptimeCalculator();
|
||||
|
||||
// Test custom date
|
||||
UptimeCalculator.currentDate = dayjs.utc("2021-01-01T00:00:00.000Z");
|
||||
assert.strictEqual(c1.getCurrentDate().unix(), dayjs.utc("2021-01-01T00:00:00.000Z").unix());
|
||||
});
|
||||
|
||||
test("Test update - UP", async (t) => {
|
||||
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
|
||||
let c2 = new UptimeCalculator();
|
||||
let date = await c2.update(UP);
|
||||
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:46:59").unix());
|
||||
});
|
||||
|
||||
test("Test update - MAINTENANCE", async (t) => {
|
||||
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
|
||||
let c2 = new UptimeCalculator();
|
||||
let date = await c2.update(MAINTENANCE);
|
||||
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
|
||||
});
|
||||
|
||||
test("Test update - DOWN", async (t) => {
|
||||
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
|
||||
let c2 = new UptimeCalculator();
|
||||
let date = await c2.update(DOWN);
|
||||
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
|
||||
});
|
||||
|
||||
test("Test update - PENDING", async (t) => {
|
||||
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
|
||||
let c2 = new UptimeCalculator();
|
||||
let date = await c2.update(PENDING);
|
||||
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
|
||||
});
|
||||
|
||||
test("Test flatStatus", async (t) => {
|
||||
let c2 = new UptimeCalculator();
|
||||
assert.strictEqual(c2.flatStatus(UP), UP);
|
||||
//assert.strictEqual(c2.flatStatus(MAINTENANCE), UP);
|
||||
assert.strictEqual(c2.flatStatus(DOWN), DOWN);
|
||||
assert.strictEqual(c2.flatStatus(PENDING), DOWN);
|
||||
});
|
||||
|
||||
test("Test getMinutelyKey", async (t) => {
|
||||
let c2 = new UptimeCalculator();
|
||||
let divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:00"));
|
||||
assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
|
||||
|
||||
// Edge case 1
|
||||
c2 = new UptimeCalculator();
|
||||
divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:01"));
|
||||
assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
|
||||
|
||||
// Edge case 2
|
||||
c2 = new UptimeCalculator();
|
||||
divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:59"));
|
||||
assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
|
||||
});
|
||||
|
||||
test("Test getDailyKey", async (t) => {
|
||||
let c2 = new UptimeCalculator();
|
||||
let dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 20:46:00").unix());
|
||||
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
||||
|
||||
c2 = new UptimeCalculator();
|
||||
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:45:30").unix());
|
||||
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
||||
|
||||
// Edge case 1
|
||||
c2 = new UptimeCalculator();
|
||||
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:59:59").unix());
|
||||
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
||||
|
||||
// Edge case 2
|
||||
c2 = new UptimeCalculator();
|
||||
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 00:00:00").unix());
|
||||
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
||||
});
|
||||
|
||||
test("Test lastDailyUptimeData", async (t) => {
|
||||
let c2 = new UptimeCalculator();
|
||||
await c2.update(UP);
|
||||
assert.strictEqual(c2.lastDailyUptimeData.up, 1);
|
||||
});
|
||||
|
||||
test("Test get24Hour Uptime and Avg Ping", async (t) => {
|
||||
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
|
||||
|
||||
// No data
|
||||
let c2 = new UptimeCalculator();
|
||||
let data = c2.get24Hour();
|
||||
assert.strictEqual(data.uptime, 0);
|
||||
assert.strictEqual(data.avgPing, null);
|
||||
|
||||
// 1 Up
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(UP, 100);
|
||||
let uptime = c2.get24Hour().uptime;
|
||||
assert.strictEqual(uptime, 1);
|
||||
assert.strictEqual(c2.get24Hour().avgPing, 100);
|
||||
|
||||
// 2 Up
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(UP, 100);
|
||||
await c2.update(UP, 200);
|
||||
uptime = c2.get24Hour().uptime;
|
||||
assert.strictEqual(uptime, 1);
|
||||
assert.strictEqual(c2.get24Hour().avgPing, 150);
|
||||
|
||||
// 3 Up
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(UP, 0);
|
||||
await c2.update(UP, 100);
|
||||
await c2.update(UP, 400);
|
||||
uptime = c2.get24Hour().uptime;
|
||||
assert.strictEqual(uptime, 1);
|
||||
assert.strictEqual(c2.get24Hour().avgPing, 166.66666666666666);
|
||||
|
||||
// 1 MAINTENANCE
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(MAINTENANCE);
|
||||
uptime = c2.get24Hour().uptime;
|
||||
assert.strictEqual(uptime, 0);
|
||||
assert.strictEqual(c2.get24Hour().avgPing, null);
|
||||
|
||||
// 1 PENDING
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(PENDING);
|
||||
uptime = c2.get24Hour().uptime;
|
||||
assert.strictEqual(uptime, 0);
|
||||
assert.strictEqual(c2.get24Hour().avgPing, null);
|
||||
|
||||
// 1 DOWN
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(DOWN);
|
||||
uptime = c2.get24Hour().uptime;
|
||||
assert.strictEqual(uptime, 0);
|
||||
assert.strictEqual(c2.get24Hour().avgPing, null);
|
||||
|
||||
// 2 DOWN
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(DOWN);
|
||||
await c2.update(DOWN);
|
||||
uptime = c2.get24Hour().uptime;
|
||||
assert.strictEqual(uptime, 0);
|
||||
assert.strictEqual(c2.get24Hour().avgPing, null);
|
||||
|
||||
// 1 DOWN, 1 UP
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(DOWN);
|
||||
await c2.update(UP, 0.5);
|
||||
uptime = c2.get24Hour().uptime;
|
||||
assert.strictEqual(uptime, 0.5);
|
||||
assert.strictEqual(c2.get24Hour().avgPing, 0.5);
|
||||
|
||||
// 1 UP, 1 DOWN
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(UP, 123);
|
||||
await c2.update(DOWN);
|
||||
uptime = c2.get24Hour().uptime;
|
||||
assert.strictEqual(uptime, 0.5);
|
||||
assert.strictEqual(c2.get24Hour().avgPing, 123);
|
||||
|
||||
// Add 24 hours
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(UP, 0);
|
||||
await c2.update(UP, 0);
|
||||
await c2.update(UP, 0);
|
||||
await c2.update(UP, 1);
|
||||
await c2.update(DOWN);
|
||||
uptime = c2.get24Hour().uptime;
|
||||
assert.strictEqual(uptime, 0.8);
|
||||
assert.strictEqual(c2.get24Hour().avgPing, 0.25);
|
||||
|
||||
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour");
|
||||
|
||||
// After 24 hours, even if there is no data, the uptime should be still 80%
|
||||
uptime = c2.get24Hour().uptime;
|
||||
assert.strictEqual(uptime, 0.8);
|
||||
assert.strictEqual(c2.get24Hour().avgPing, 0.25);
|
||||
|
||||
// Add more 24 hours (48 hours)
|
||||
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour");
|
||||
|
||||
// After 48 hours, even if there is no data, the uptime should be still 80%
|
||||
uptime = c2.get24Hour().uptime;
|
||||
assert.strictEqual(uptime, 0.8);
|
||||
assert.strictEqual(c2.get24Hour().avgPing, 0.25);
|
||||
});
|
||||
|
||||
test("Test get7DayUptime", async (t) => {
|
||||
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
|
||||
|
||||
// No data
|
||||
let c2 = new UptimeCalculator();
|
||||
let uptime = c2.get7Day().uptime;
|
||||
assert.strictEqual(uptime, 0);
|
||||
|
||||
// 1 Up
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(UP);
|
||||
uptime = c2.get7Day().uptime;
|
||||
assert.strictEqual(uptime, 1);
|
||||
|
||||
// 2 Up
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(UP);
|
||||
await c2.update(UP);
|
||||
uptime = c2.get7Day().uptime;
|
||||
assert.strictEqual(uptime, 1);
|
||||
|
||||
// 3 Up
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(UP);
|
||||
await c2.update(UP);
|
||||
await c2.update(UP);
|
||||
uptime = c2.get7Day().uptime;
|
||||
assert.strictEqual(uptime, 1);
|
||||
|
||||
// 1 MAINTENANCE
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(MAINTENANCE);
|
||||
uptime = c2.get7Day().uptime;
|
||||
assert.strictEqual(uptime, 0);
|
||||
|
||||
// 1 PENDING
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(PENDING);
|
||||
uptime = c2.get7Day().uptime;
|
||||
assert.strictEqual(uptime, 0);
|
||||
|
||||
// 1 DOWN
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(DOWN);
|
||||
uptime = c2.get7Day().uptime;
|
||||
assert.strictEqual(uptime, 0);
|
||||
|
||||
// 2 DOWN
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(DOWN);
|
||||
await c2.update(DOWN);
|
||||
uptime = c2.get7Day().uptime;
|
||||
assert.strictEqual(uptime, 0);
|
||||
|
||||
// 1 DOWN, 1 UP
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(DOWN);
|
||||
await c2.update(UP);
|
||||
uptime = c2.get7Day().uptime;
|
||||
assert.strictEqual(uptime, 0.5);
|
||||
|
||||
// 1 UP, 1 DOWN
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(UP);
|
||||
await c2.update(DOWN);
|
||||
uptime = c2.get7Day().uptime;
|
||||
assert.strictEqual(uptime, 0.5);
|
||||
|
||||
// Add 7 days
|
||||
c2 = new UptimeCalculator();
|
||||
await c2.update(UP);
|
||||
await c2.update(UP);
|
||||
await c2.update(UP);
|
||||
await c2.update(UP);
|
||||
await c2.update(DOWN);
|
||||
uptime = c2.get7Day().uptime;
|
||||
assert.strictEqual(uptime, 0.8);
|
||||
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(7, "day");
|
||||
|
||||
// After 7 days, even if there is no data, the uptime should be still 80%
|
||||
uptime = c2.get7Day().uptime;
|
||||
assert.strictEqual(uptime, 0.8);
|
||||
|
||||
});
|
||||
|
||||
test("Test get30DayUptime (1 check per day)", async (t) => {
|
||||
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
|
||||
|
||||
let c2 = new UptimeCalculator();
|
||||
let uptime = c2.get30Day().uptime;
|
||||
assert.strictEqual(uptime, 0);
|
||||
|
||||
let up = 0;
|
||||
let down = 0;
|
||||
let flip = true;
|
||||
for (let i = 0; i < 30; i++) {
|
||||
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day");
|
||||
|
||||
if (flip) {
|
||||
await c2.update(UP);
|
||||
up++;
|
||||
} else {
|
||||
await c2.update(DOWN);
|
||||
down++;
|
||||
}
|
||||
|
||||
uptime = c2.get30Day().uptime;
|
||||
assert.strictEqual(uptime, up / (up + down));
|
||||
|
||||
flip = !flip;
|
||||
}
|
||||
|
||||
// Last 7 days
|
||||
// Down, Up, Down, Up, Down, Up, Down
|
||||
// So 3 UP
|
||||
assert.strictEqual(c2.get7Day().uptime, 3 / 7);
|
||||
});
|
||||
|
||||
test("Test get1YearUptime (1 check per day)", async (t) => {
|
||||
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
|
||||
|
||||
let c2 = new UptimeCalculator();
|
||||
let uptime = c2.get1Year().uptime;
|
||||
assert.strictEqual(uptime, 0);
|
||||
|
||||
let flip = true;
|
||||
for (let i = 0; i < 365; i++) {
|
||||
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day");
|
||||
|
||||
if (flip) {
|
||||
await c2.update(UP);
|
||||
} else {
|
||||
await c2.update(DOWN);
|
||||
}
|
||||
|
||||
uptime = c2.get30Day().time;
|
||||
flip = !flip;
|
||||
}
|
||||
|
||||
assert.strictEqual(c2.get1Year().uptime, 183 / 365);
|
||||
assert.strictEqual(c2.get30Day().uptime, 15 / 30);
|
||||
assert.strictEqual(c2.get7Day().uptime, 4 / 7);
|
||||
});
|
||||
|
||||
/**
|
||||
* Code from here: https://stackoverflow.com/a/64550489/1097815
|
||||
*/
|
||||
function memoryUsage() {
|
||||
const formatMemoryUsage = (data) => `${Math.round(data / 1024 / 1024 * 100) / 100} MB`;
|
||||
const memoryData = process.memoryUsage();
|
||||
|
||||
const memoryUsage = {
|
||||
rss: `${formatMemoryUsage(memoryData.rss)} -> Resident Set Size - total memory allocated for the process execution`,
|
||||
heapTotal: `${formatMemoryUsage(memoryData.heapTotal)} -> total size of the allocated heap`,
|
||||
heapUsed: `${formatMemoryUsage(memoryData.heapUsed)} -> actual memory used during the execution`,
|
||||
external: `${formatMemoryUsage(memoryData.external)} -> V8 external memory`,
|
||||
};
|
||||
return memoryUsage;
|
||||
}
|
||||
|
||||
test("Worst case", async (t) => {
|
||||
console.log("Memory usage before preparation", memoryUsage());
|
||||
|
||||
let c = new UptimeCalculator();
|
||||
let up = 0;
|
||||
let down = 0;
|
||||
let interval = 20;
|
||||
|
||||
await t.test("Prepare data", async () => {
|
||||
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
|
||||
|
||||
// Since 2023-08-12 will be out of 365 range, it starts from 2023-08-13 actually
|
||||
let actualStartDate = dayjs.utc("2023-08-13 00:00:00").unix();
|
||||
|
||||
// Simulate 1s interval for a year
|
||||
for (let i = 0; i < 365 * 24 * 60 * 60; i += interval) {
|
||||
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(interval, "second");
|
||||
|
||||
//Randomly UP, DOWN, MAINTENANCE, PENDING
|
||||
let rand = Math.random();
|
||||
if (rand < 0.25) {
|
||||
c.update(UP);
|
||||
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
|
||||
up++;
|
||||
}
|
||||
} else if (rand < 0.5) {
|
||||
c.update(DOWN);
|
||||
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
|
||||
down++;
|
||||
}
|
||||
} else if (rand < 0.75) {
|
||||
c.update(MAINTENANCE);
|
||||
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
|
||||
//up++;
|
||||
}
|
||||
} else {
|
||||
c.update(PENDING);
|
||||
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
|
||||
down++;
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("Final Date: ", UptimeCalculator.currentDate.format("YYYY-MM-DD HH:mm:ss"));
|
||||
console.log("Memory usage before preparation", memoryUsage());
|
||||
|
||||
assert.strictEqual(c.minutelyUptimeDataList.length(), 1440);
|
||||
assert.strictEqual(c.dailyUptimeDataList.length(), 365);
|
||||
});
|
||||
|
||||
await t.test("get1YearUptime()", async () => {
|
||||
assert.strictEqual(c.get1Year().uptime, up / (up + down));
|
||||
});
|
||||
|
||||
});
|
Loading…
Reference in new issue