mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-05-17 08:26:56 +03:00
fix(uptime): ensure correct handling of missing time buckets in uptime calculations (#7235)
This commit is contained in:
@@ -361,12 +361,12 @@ class UptimeCalculator {
|
||||
log.debug("uptime_calc", "Remove old data");
|
||||
await R.exec("DELETE FROM stat_minutely WHERE monitor_id = ? AND timestamp < ?", [
|
||||
this.monitorID,
|
||||
this.getMinutelyKey(currentDate.subtract(this.statMinutelyKeepHour, "hour")),
|
||||
this.getMinutelyKey(currentDate.subtract(this.statMinutelyKeepHour, "hour"), false),
|
||||
]);
|
||||
|
||||
await R.exec("DELETE FROM stat_hourly WHERE monitor_id = ? AND timestamp < ?", [
|
||||
this.monitorID,
|
||||
this.getHourlyKey(currentDate.subtract(this.statHourlyKeepDay, "day")),
|
||||
this.getHourlyKey(currentDate.subtract(this.statHourlyKeepDay, "day"), false),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -442,16 +442,17 @@ class UptimeCalculator {
|
||||
/**
|
||||
* Convert timestamp to minutely key
|
||||
* @param {dayjs.Dayjs} date The heartbeat date
|
||||
* @param {boolean} createIfMissing Whether to create a missing bucket, defaults to true
|
||||
* @returns {number} Timestamp
|
||||
*/
|
||||
getMinutelyKey(date) {
|
||||
getMinutelyKey(date, createIfMissing = true) {
|
||||
// Truncate value to minutes (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)) {
|
||||
if (createIfMissing && !(divisionKey in this.minutelyUptimeDataList)) {
|
||||
this.minutelyUptimeDataList.push(divisionKey, {
|
||||
up: 0,
|
||||
down: 0,
|
||||
@@ -467,16 +468,17 @@ class UptimeCalculator {
|
||||
/**
|
||||
* Convert timestamp to hourly key
|
||||
* @param {dayjs.Dayjs} date The heartbeat date
|
||||
* @param {boolean} createIfMissing Whether to create a missing bucket, defaults to true
|
||||
* @returns {number} Timestamp
|
||||
*/
|
||||
getHourlyKey(date) {
|
||||
getHourlyKey(date, createIfMissing = true) {
|
||||
// Truncate value to hours (e.g. 2021-01-01 12:34:56 -> 2021-01-01 12:00:00)
|
||||
date = date.startOf("hour");
|
||||
|
||||
// Convert to timestamp in second
|
||||
let divisionKey = date.unix();
|
||||
|
||||
if (!(divisionKey in this.hourlyUptimeDataList)) {
|
||||
if (createIfMissing && !(divisionKey in this.hourlyUptimeDataList)) {
|
||||
this.hourlyUptimeDataList.push(divisionKey, {
|
||||
up: 0,
|
||||
down: 0,
|
||||
@@ -492,15 +494,16 @@ class UptimeCalculator {
|
||||
/**
|
||||
* Convert timestamp to daily key
|
||||
* @param {dayjs.Dayjs} date The heartbeat date
|
||||
* @param {boolean} createIfMissing Whether to create a missing bucket, defaults to true
|
||||
* @returns {number} Timestamp
|
||||
*/
|
||||
getDailyKey(date) {
|
||||
getDailyKey(date, createIfMissing = true) {
|
||||
// Truncate value to start of 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]) {
|
||||
if (createIfMissing && !this.dailyUptimeDataList[dailyKey]) {
|
||||
this.dailyUptimeDataList.push(dailyKey, {
|
||||
up: 0,
|
||||
down: 0,
|
||||
|
||||
@@ -68,6 +68,79 @@ describe("Uptime Calculator", () => {
|
||||
assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
|
||||
});
|
||||
|
||||
test("missing cleanup buckets are not created when createIfMissing is false", () => {
|
||||
let c2 = new UptimeCalculator();
|
||||
|
||||
c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:59"));
|
||||
c2.getHourlyKey(dayjs.utc("2023-08-12 20:46:59"));
|
||||
c2.getDailyKey(dayjs.utc("2023-08-12 20:46:59"));
|
||||
|
||||
let minutelyCleanupKey = c2.getMinutelyKey(dayjs.utc("2023-08-11 20:46:59"), false);
|
||||
let hourlyCleanupKey = c2.getHourlyKey(dayjs.utc("2023-07-13 20:46:59"), false);
|
||||
let dailyCleanupKey = c2.getDailyKey(dayjs.utc("2022-08-12 20:46:59"), false);
|
||||
|
||||
assert.strictEqual(c2.minutelyUptimeDataList.length(), 1);
|
||||
assert.strictEqual(c2.hourlyUptimeDataList.length(), 1);
|
||||
assert.strictEqual(c2.dailyUptimeDataList.length(), 1);
|
||||
assert.strictEqual(c2.minutelyUptimeDataList[minutelyCleanupKey], undefined);
|
||||
assert.strictEqual(c2.hourlyUptimeDataList[hourlyCleanupKey], undefined);
|
||||
assert.strictEqual(c2.dailyUptimeDataList[dailyCleanupKey], undefined);
|
||||
});
|
||||
|
||||
test("cleanup lookup should not create missing minutely/hourly buckets", () => {
|
||||
let startDate = dayjs.utc("2023-08-12 00:00:00");
|
||||
|
||||
// First test the broken version that creates missing buckets during cleanup lookup.
|
||||
let broken = new UptimeCalculator();
|
||||
let minutelyQueueLimit = broken.minutelyUptimeDataList.__limit;
|
||||
let hourlyQueueLimit = broken.hourlyUptimeDataList.__limit;
|
||||
let totalTicks = Math.max(minutelyQueueLimit, hourlyQueueLimit);
|
||||
|
||||
let minutelyEndDate = startDate;
|
||||
let hourlyEndDate = startDate;
|
||||
for (let tick = 0; tick < totalTicks; tick++) {
|
||||
minutelyEndDate = startDate.add(tick, "minute");
|
||||
hourlyEndDate = startDate.add(tick, "hour");
|
||||
|
||||
// Simulate normal key lookup that creates buckets.
|
||||
broken.getMinutelyKey(minutelyEndDate);
|
||||
broken.getHourlyKey(hourlyEndDate);
|
||||
|
||||
// Simulate pre-fix cleanup key lookup that accidentally creates missing buckets.
|
||||
broken.getMinutelyKey(minutelyEndDate.subtract(broken.statMinutelyKeepHour, "hour"));
|
||||
broken.getHourlyKey(hourlyEndDate.subtract(broken.statHourlyKeepDay, "day"));
|
||||
}
|
||||
|
||||
UptimeCalculator.currentDate = minutelyEndDate;
|
||||
assert.strictEqual(broken.getDataArray(minutelyQueueLimit, "minute").length, minutelyQueueLimit / 2);
|
||||
|
||||
UptimeCalculator.currentDate = hourlyEndDate;
|
||||
assert.strictEqual(broken.getDataArray(hourlyQueueLimit, "hour").length, hourlyQueueLimit / 2);
|
||||
|
||||
// Now test the fixed version that should not create missing buckets.
|
||||
let fixed = new UptimeCalculator();
|
||||
let fixedMinutelyTickDate = startDate;
|
||||
let fixedHourlyTickDate = startDate;
|
||||
for (let tick = 0; tick < totalTicks; tick++) {
|
||||
fixedMinutelyTickDate = startDate.add(tick, "minute");
|
||||
fixedHourlyTickDate = startDate.add(tick, "hour");
|
||||
|
||||
// Simulate normal key lookup that creates buckets.
|
||||
fixed.getMinutelyKey(fixedMinutelyTickDate);
|
||||
fixed.getHourlyKey(fixedHourlyTickDate);
|
||||
|
||||
// Simulate pre-fix cleanup key lookup that should not create missing buckets.
|
||||
fixed.getMinutelyKey(fixedMinutelyTickDate.subtract(fixed.statMinutelyKeepHour, "hour"), false);
|
||||
fixed.getHourlyKey(fixedHourlyTickDate.subtract(fixed.statHourlyKeepDay, "day"), false);
|
||||
}
|
||||
|
||||
UptimeCalculator.currentDate = minutelyEndDate;
|
||||
assert.strictEqual(fixed.getDataArray(minutelyQueueLimit, "minute").length, minutelyQueueLimit);
|
||||
|
||||
UptimeCalculator.currentDate = hourlyEndDate;
|
||||
assert.strictEqual(fixed.getDataArray(hourlyQueueLimit, "hour").length, hourlyQueueLimit);
|
||||
});
|
||||
|
||||
test("getDailyKey() returns correct timestamp for start of day", () => {
|
||||
let c2 = new UptimeCalculator();
|
||||
let dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 20:46:00"));
|
||||
|
||||
Reference in New Issue
Block a user