fix(uptime): ensure correct handling of missing time buckets in uptime calculations (#7235)

This commit is contained in:
噗噗
2026-04-10 22:22:01 +08:00
committed by GitHub
parent 7136dd7832
commit 07d28d8181
2 changed files with 84 additions and 8 deletions

View File

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

View File

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