diff --git a/server/database.js b/server/database.js index 714c51ba..75f5f25c 100644 --- a/server/database.js +++ b/server/database.js @@ -9,6 +9,7 @@ const mysql = require("mysql2/promise"); const { Settings } = require("./settings"); const { UptimeCalculator } = require("./uptime-calculator"); const dayjs = require("dayjs"); +const { SimpleMigrationServer } = require("./utils/simple-migration-server"); /** * Database & App Data Folder @@ -382,9 +383,11 @@ class Database { /** * Patch the database + * @param {number} port Start the migration server for aggregate tables on this port if provided + * @param {string} hostname Start the migration server for aggregate tables on this hostname if provided * @returns {Promise} */ - static async patch() { + static async patch(port = undefined, hostname = undefined) { // Still need to keep this for old versions of Uptime Kuma if (Database.dbConfig.type === "sqlite") { await this.patchSqlite(); @@ -409,7 +412,7 @@ class Database { await R.exec("PRAGMA foreign_keys = ON"); } - await this.migrateAggregateTable(); + await this.migrateAggregateTable(port, hostname); } catch (e) { // Allow missing patch files for downgrade or testing pr. @@ -735,9 +738,11 @@ class Database { * Normally, it should be in transaction, but UptimeCalculator wasn't designed to be in transaction before that. * I don't want to heavily modify the UptimeCalculator, so it is not in transaction. * Run `npm run reset-migrate-aggregate-table-state` to reset, in case the migration is interrupted. + * @param {number} port Start the migration server on this port if provided + * @param {string} hostname Start the migration server on this hostname if provided * @returns {Promise} */ - static async migrateAggregateTable() { + static async migrateAggregateTable(port, hostname = undefined) { log.debug("db", "Enter Migrate Aggregate Table function"); // Add a setting for 2.0.0-dev users to skip this migration @@ -758,6 +763,18 @@ class Database { throw new Error("Aggregate table migration is already in progress"); } + /** + * Start migration server for displaying the migration status + * @type {SimpleMigrationServer} + */ + let migrationServer; + let msg; + + if (port) { + migrationServer = new SimpleMigrationServer(); + await migrationServer.start(port, hostname); + } + await Settings.set("migrateAggregateTableState", "migrating"); log.info("db", "Migrating Aggregate Table"); @@ -777,6 +794,7 @@ class Database { let count = countResult.count; if (count > 0) { log.warn("db", `Aggregate table ${table} is not empty, migration will not be started (Maybe you were using 2.0.0-dev?)`); + await migrationServer?.stop(); return; } } @@ -811,7 +829,9 @@ class Database { `, [ monitor.monitor_id, date.date ]); if (heartbeats.length > 0) { - log.info("db", `[DON'T STOP] Migrating monitor data ${monitor.monitor_id} - ${date.date} [${progressPercent.toFixed(2)}%][${i}/${monitors.length}]`); + msg = `[DON'T STOP] Migrating monitor data ${monitor.monitor_id} - ${date.date} [${progressPercent.toFixed(2)}%][${i}/${monitors.length}]`; + log.info("db", msg); + migrationServer?.update(msg); } for (let heartbeat of heartbeats) { @@ -829,9 +849,13 @@ class Database { i++; } - await Database.clearHeartbeatData(true); + msg = "Clearing non-important heartbeats"; + log.info("db", msg); + migrationServer?.update(msg); + await Database.clearHeartbeatData(true); await Settings.set("migrateAggregateTableState", "migrated"); + await migrationServer?.stop(); if (monitors.length > 0) { log.info("db", "Aggregate Table Migration Completed"); diff --git a/server/server.js b/server/server.js index 7c46fa89..ec5ad49f 100644 --- a/server/server.js +++ b/server/server.js @@ -1716,7 +1716,7 @@ async function initDatabase(testMode = false) { log.info("server", "Connected to the database"); // Patch the database - await Database.patch(); + await Database.patch(port, hostname); let jwtSecretBean = await R.findOne("setting", " `key` = ? ", [ "jwtSecret", diff --git a/server/utils/simple-migration-server.js b/server/utils/simple-migration-server.js new file mode 100644 index 00000000..680f8df2 --- /dev/null +++ b/server/utils/simple-migration-server.js @@ -0,0 +1,84 @@ +const express = require("express"); +const http = require("node:http"); +const { log } = require("../../src/util"); + +/** + * SimpleMigrationServer + * For displaying the migration status of the server + * Also, it is used to let Docker healthcheck know the status of the server, as the main server is not started yet, healthcheck will think the server is down incorrectly. + */ +class SimpleMigrationServer { + /** + * Express app instance + * @type {?Express} + */ + app; + + /** + * Server instance + * @type {?Server} + */ + server; + + /** + * Response object + * @type {?Response} + */ + response; + + /** + * Start the server + * @param {number} port Port + * @param {string} hostname Hostname + * @returns {Promise} + */ + start(port, hostname) { + this.app = express(); + this.server = http.createServer(this.app); + + this.app.get("/", (req, res) => { + res.set("Content-Type", "text/plain"); + res.write("Migration is in progress, listening message...\n"); + if (this.response) { + this.response.write("Disconnected\n"); + this.response.end(); + } + this.response = res; + // never ending response + }); + + return new Promise((resolve) => { + this.server.listen(port, hostname, () => { + if (hostname) { + log.info("migration", `Migration server is running on http://${hostname}:${port}`); + } else { + log.info("migration", `Migration server is running on http://localhost:${port}`); + } + resolve(); + }); + }); + } + + /** + * Update the message + * @param {string} msg Message to update + * @returns {void} + */ + update(msg) { + this.response?.write(msg + "\n"); + } + + /** + * Stop the server + * @returns {Promise} + */ + async stop() { + this.response?.write("Finished, please refresh this page.\n"); + this.response?.end(); + await this.server?.close(); + } +} + +module.exports = { + SimpleMigrationServer, +};