From b2da5cd3f002e14a902205741ad90d94bb41d17f Mon Sep 17 00:00:00 2001 From: Ionys <9364594+Ionys320@users.noreply.github.com> Date: Fri, 13 Jun 2025 18:53:21 +0200 Subject: [PATCH 1/2] fix(maintenance): Avoid using interval for Croner --- ...5-06-13-0000-maintenance-add-last-start.js | 13 +++++++ server/model/maintenance.js | 39 +++++++++++++++---- 2 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 db/knex_migrations/2025-06-13-0000-maintenance-add-last-start.js diff --git a/db/knex_migrations/2025-06-13-0000-maintenance-add-last-start.js b/db/knex_migrations/2025-06-13-0000-maintenance-add-last-start.js new file mode 100644 index 000000000..95412173e --- /dev/null +++ b/db/knex_migrations/2025-06-13-0000-maintenance-add-last-start.js @@ -0,0 +1,13 @@ +// Add column last_start_date to maintenance table +exports.up = function (knex) { + return knex.schema + .alterTable("maintenance", function (table) { + table.datetime("last_start_date"); + }); +}; + +exports.down = function (knex) { + return knex.schema.alterTable("maintenance", function (table) { + table.dropColumn("last_start_date"); + }); +}; diff --git a/server/model/maintenance.js b/server/model/maintenance.js index 7111a18cb..828c1f757 100644 --- a/server/model/maintenance.js +++ b/server/model/maintenance.js @@ -192,7 +192,7 @@ class Maintenance extends BeanModel { * @returns {void} */ static validateCron(cron) { - let job = new Cron(cron, () => {}); + let job = new Cron(cron, () => { }); job.stop(); } @@ -209,8 +209,8 @@ class Maintenance extends BeanModel { log.debug("maintenance", "Run maintenance id: " + this.id); - // 1.21.2 migration - if (!this.cron) { + // 1.21.2 migration and 2.0.0-beta.4 migration + if (!this.cron || this.strategy === "recurring-interval" && this.cron === "* * * * *") { await this.generateCron(); if (!this.timezone) { this.timezone = "UTC"; @@ -229,6 +229,8 @@ class Maintenance extends BeanModel { apicache.clear(); }); } else if (this.cron != null) { + let current = dayjs(); + // Here should be cron or recurring try { this.beanMeta.status = "scheduled"; @@ -248,6 +250,10 @@ class Maintenance extends BeanModel { this.beanMeta.status = "scheduled"; UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id); }, duration); + + // Set last start date to current time + this.last_start_date = current.toISOString(); + R.store(this); }; // Create Cron @@ -258,9 +264,24 @@ class Maintenance extends BeanModel { const startDateTime = startDate.hour(hour).minute(minute); this.beanMeta.job = new Cron(this.cron, { timezone: await this.getTimezone(), - interval: this.interval_day * 24 * 60 * 60, startAt: startDateTime.toISOString(), - }, startEvent); + }, () => { + if (!this.lastStartDate || this.interval_day === 1) { + return startEvent(); + } + + // If last start date is set, it means the maintenance has been started before + let lastStartDate = dayjs(this.lastStartDate); + + // Check if the interval is enough + if (current.diff(lastStartDate, "day") < this.interval_day) { + log.debug("maintenance", "Maintenance id: " + this.id + " is still in the window, skipping start event"); + return; + } + + log.debug("maintenance", "Maintenance id: " + this.id + " is not in the window, starting event"); + return startEvent(); + }); } else { this.beanMeta.job = new Cron(this.cron, { timezone: await this.getTimezone(), @@ -269,7 +290,6 @@ class Maintenance extends BeanModel { // Continue if the maintenance is still in the window let runningTimeslot = this.getRunningTimeslot(); - let current = dayjs(); if (runningTimeslot) { let duration = dayjs(runningTimeslot.endDate).diff(current, "second") * 1000; @@ -413,8 +433,11 @@ class Maintenance extends BeanModel { } else if (!this.strategy.startsWith("recurring-")) { this.cron = ""; } else if (this.strategy === "recurring-interval") { - // For intervals, the pattern is calculated in the run function as the interval-option is set - this.cron = "* * * * *"; + // For intervals, the pattern is used to check if the execution should be started + let array = this.start_time.split(":"); + let hour = parseInt(array[0]); + let minute = parseInt(array[1]); + this.cron = minute + " " + hour + " * * *"; this.duration = this.calcDuration(); log.debug("maintenance", "Cron: " + this.cron); log.debug("maintenance", "Duration: " + this.duration); From 3075bc9bd9b46c393cc45cbe4fafce7dbf298fe9 Mon Sep 17 00:00:00 2001 From: Ionys <9364594+Ionys320@users.noreply.github.com> Date: Sat, 14 Jun 2025 20:44:46 +0200 Subject: [PATCH 2/2] feat(maintenance): Perform the CRON migration in the migration file, and add an espilon for check safety --- ...5-06-13-0000-maintenance-add-last-start.js | 25 +++++++++++++++++-- server/model/maintenance.js | 9 ++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/db/knex_migrations/2025-06-13-0000-maintenance-add-last-start.js b/db/knex_migrations/2025-06-13-0000-maintenance-add-last-start.js index 95412173e..3cb28d968 100644 --- a/db/knex_migrations/2025-06-13-0000-maintenance-add-last-start.js +++ b/db/knex_migrations/2025-06-13-0000-maintenance-add-last-start.js @@ -1,9 +1,30 @@ // Add column last_start_date to maintenance table -exports.up = function (knex) { - return knex.schema +exports.up = async function (knex) { + await knex.schema .alterTable("maintenance", function (table) { table.datetime("last_start_date"); }); + + // Perform migration for recurring-interval strategy + const recurringMaintenances = await knex("maintenance").where({ + strategy: "recurring-interval", + cron: "* * * * *" + }).select("id", "start_time"); + + // eslint-disable-next-line camelcase + const maintenanceUpdates = recurringMaintenances.map(async ({ start_time, id }) => { + // eslint-disable-next-line camelcase + const [ hourStr, minuteStr ] = start_time.split(":"); + const hour = parseInt(hourStr, 10); + const minute = parseInt(minuteStr, 10); + + const cron = `${minute} ${hour} * * *`; + + await knex("maintenance") + .where({ id }) + .update({ cron }); + }); + await Promise.all(maintenanceUpdates); }; exports.down = function (knex) { diff --git a/server/model/maintenance.js b/server/model/maintenance.js index 7ae88abd0..aa1fa0c14 100644 --- a/server/model/maintenance.js +++ b/server/model/maintenance.js @@ -219,8 +219,8 @@ class Maintenance extends BeanModel { log.debug("maintenance", "Run maintenance id: " + this.id); - // 1.21.2 migration and 2.0.0-beta.4 migration - if (!this.cron || this.strategy === "recurring-interval" && this.cron === "* * * * *") { + // 1.21.2 migration + if (!this.cron) { await this.generateCron(); if (!this.timezone) { this.timezone = "UTC"; @@ -281,7 +281,8 @@ class Maintenance extends BeanModel { } // If last start date is set, it means the maintenance has been started before - let lastStartDate = dayjs(this.lastStartDate); + let lastStartDate = dayjs(this.lastStartDate) + .subtract(1.1, "hour"); // Subtract 1.1 hour to avoid issues with timezone differences // Check if the interval is enough if (current.diff(lastStartDate, "day") < this.interval_day) { @@ -447,7 +448,7 @@ class Maintenance extends BeanModel { let array = this.start_time.split(":"); let hour = parseInt(array[0]); let minute = parseInt(array[1]); - this.cron = minute + " " + hour + " * * *"; + this.cron = `${minute} ${hour} * * *`; this.duration = this.calcDuration(); log.debug("maintenance", "Cron: " + this.cron); log.debug("maintenance", "Duration: " + this.duration);