perf: hourly update check only with a browser open; opt-in retention, incremental auto-vacuum and Compact
- update-scheduler: default interval 1 h (DASHBOARD_UPDATE_CHECK_INTERVAL_MS and its 60 s floor kept); a scheduled tick with no /ws client is skipped, and the next browser to connect gets one catch-up check when the last one is stale (websocket.js onConnection). Manual check unchanged. - db-retention.js: shared session purge (used by POST /api/settings/cleanup too), opt-in AGENTHUB_RETENTION_DAYS applied by the hourly maintenance in 25-session transactions with yields (max 5000/run), bounded incremental_vacuum when the freelist is large, and a manual Compact (VACUUM in a worker, converting to auto_vacuum=INCREMENTAL). - db.js: new databases are INCREMENTAL; an existing one up to 32 MB is converted at startup. - GET/PUT /api/settings/retention, POST /api/settings/compact (confirm: true); Settings → Data → Session cleanup gets the Automatic cleanup control and Compact now (en + vi).
This commit is contained in:
+78
-10
@@ -1,5 +1,11 @@
|
||||
/**
|
||||
* @file Periodic git upstream check and WebSocket broadcast when update availability changes.
|
||||
* @file Periodic release/upstream check and WebSocket broadcast when update
|
||||
* availability changes. Hourly by default (`DASHBOARD_UPDATE_CHECK_INTERVAL_MS`,
|
||||
* floor 60 s); a tick with no browser connected is skipped — a checkout install
|
||||
* runs `git fetch` on every check, and nobody would see the answer — and the
|
||||
* first browser to connect after a skipped or stale check gets one promptly.
|
||||
* The manual "Check now" (`POST /api/updates/check`, `agenthub update-check`)
|
||||
* is not routed through here and always runs.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -13,11 +19,39 @@ function isUpdateCheckDisabled() {
|
||||
function intervalMs() {
|
||||
const n = Number.parseInt(process.env.DASHBOARD_UPDATE_CHECK_INTERVAL_MS || "", 10);
|
||||
if (Number.isFinite(n) && n >= 60_000) return n;
|
||||
return 5 * 60 * 1000;
|
||||
return 60 * 60 * 1000;
|
||||
}
|
||||
|
||||
function startUpdateScheduler({ broadcast }) {
|
||||
/** Delay before the catch-up check a reconnecting browser triggers. */
|
||||
const CONNECT_CHECK_DELAY_MS = 3_000;
|
||||
|
||||
/**
|
||||
* @param {object} opts
|
||||
* @param {Function} opts.broadcast
|
||||
* @param {() => number} [opts.getConnectionCount] browsers connected now
|
||||
* (default: server/websocket.js's); 0 skips a scheduled tick.
|
||||
* @param {(fn: Function) => Function} [opts.onConnection] subscribe to new
|
||||
* browser connections; returns an unsubscribe.
|
||||
* @param {Function} [opts.getStatus] the check itself (tests inject it).
|
||||
* @param {number} [opts.initialDelayMs] first tick after start (default 8 s).
|
||||
*/
|
||||
function startUpdateScheduler({
|
||||
broadcast,
|
||||
getConnectionCount,
|
||||
onConnection,
|
||||
getStatus = getUpdatesStatus,
|
||||
initialDelayMs = 8_000,
|
||||
}) {
|
||||
if (isUpdateCheckDisabled()) return { stop: () => {} };
|
||||
if (!getConnectionCount || !onConnection) {
|
||||
const ws = require("./websocket");
|
||||
getConnectionCount = getConnectionCount || ws.getConnectionCount;
|
||||
onConnection = onConnection || ws.onConnection;
|
||||
}
|
||||
const every = intervalMs();
|
||||
let lastCheckAt = 0;
|
||||
let inFlight = false;
|
||||
let connectTimer = null;
|
||||
|
||||
let lastFingerprint = "";
|
||||
let lastHadUpdate = false;
|
||||
@@ -25,10 +59,24 @@ function startUpdateScheduler({ broadcast }) {
|
||||
let initialTimer = null;
|
||||
let intervalTimer = null;
|
||||
|
||||
async function tick() {
|
||||
// A scheduled tick: skipped while no browser is connected.
|
||||
function scheduledTick() {
|
||||
if (stopped) return;
|
||||
let clients = 1;
|
||||
try {
|
||||
const status = await getUpdatesStatus();
|
||||
clients = getConnectionCount();
|
||||
} catch {
|
||||
/* unknown: check as before */
|
||||
}
|
||||
if (clients > 0) tick();
|
||||
}
|
||||
|
||||
async function tick() {
|
||||
if (stopped || inFlight) return;
|
||||
inFlight = true;
|
||||
lastCheckAt = Date.now();
|
||||
try {
|
||||
const status = await getStatus();
|
||||
// Fingerprint must change whenever anything user-visible changes,
|
||||
// including the *shape* of manual_command (e.g. user switches branch
|
||||
// or adds an upstream remote — same remote_sha / commits_behind, but
|
||||
@@ -72,15 +120,33 @@ function startUpdateScheduler({ broadcast }) {
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal — never block the server on update checks
|
||||
} finally {
|
||||
inFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
initialTimer = setTimeout(() => {
|
||||
tick();
|
||||
}, 8_000);
|
||||
// A browser came back after the scheduled checks were skipped (or the last
|
||||
// one is older than the interval): check once, shortly, so the banner is
|
||||
// current without waiting up to an hour.
|
||||
let unsubscribe = null;
|
||||
try {
|
||||
unsubscribe = onConnection(() => {
|
||||
if (stopped || connectTimer || inFlight) return;
|
||||
if (Date.now() - lastCheckAt < every) return;
|
||||
connectTimer = setTimeout(() => {
|
||||
connectTimer = null;
|
||||
tick();
|
||||
}, CONNECT_CHECK_DELAY_MS);
|
||||
if (typeof connectTimer.unref === "function") connectTimer.unref();
|
||||
});
|
||||
} catch {
|
||||
/* no WebSocket server: scheduled ticks only */
|
||||
}
|
||||
|
||||
initialTimer = setTimeout(scheduledTick, initialDelayMs);
|
||||
if (typeof initialTimer.unref === "function") initialTimer.unref();
|
||||
|
||||
intervalTimer = setInterval(tick, intervalMs());
|
||||
intervalTimer = setInterval(scheduledTick, every);
|
||||
if (typeof intervalTimer.unref === "function") intervalTimer.unref();
|
||||
|
||||
return {
|
||||
@@ -88,8 +154,10 @@ function startUpdateScheduler({ broadcast }) {
|
||||
stopped = true;
|
||||
if (initialTimer) clearTimeout(initialTimer);
|
||||
if (intervalTimer) clearInterval(intervalTimer);
|
||||
if (connectTimer) clearTimeout(connectTimer);
|
||||
if (typeof unsubscribe === "function") unsubscribe();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { startUpdateScheduler };
|
||||
module.exports = { startUpdateScheduler, intervalMs };
|
||||
|
||||
Reference in New Issue
Block a user