diff --git a/server/uptime-calculator.js b/server/uptime-calculator.js index d0ad1742..3e44cb87 100644 --- a/server/uptime-calculator.js +++ b/server/uptime-calculator.js @@ -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, diff --git a/test/backend-test/test-uptime-calculator.js b/test/backend-test/test-uptime-calculator.js index c1439209..0826fbac 100644 --- a/test/backend-test/test-uptime-calculator.js +++ b/test/backend-test/test-uptime-calculator.js @@ -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"));