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:
2026-10-16 10:20:38 +07:00
parent ecbbb4dd8f
commit e07993c536
21 changed files with 1323 additions and 58 deletions
+35 -1
View File
@@ -141,6 +141,14 @@ migrateLegacyDatabase(DB_PATH);
const db = new Database(DB_PATH);
// Freed pages go back to the OS only with auto_vacuum. On a NEW database this
// takes effect at once — it must come before `journal_mode = WAL`, which
// writes the header; an existing one is converted further down, by a VACUUM,
// only while it is small (`convertAutoVacuum`), else by Settings → Data →
// Compact. `maintainDatabase` then runs a bounded `incremental_vacuum`.
if (db.pragma("auto_vacuum", { simple: true }) === 0) {
db.pragma("auto_vacuum = INCREMENTAL");
}
db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");
db.pragma("busy_timeout = 5000");
@@ -1866,6 +1874,28 @@ for (const col of ["display_name", "email", "avatar_at"]) {
}
}
// An existing database made before auto_vacuum: the setting is stored only by
// a VACUUM, which rewrites the whole file under an exclusive lock. Done here,
// at startup and under the migration lock, only for a file up to
// AUTO_VACUUM_CONVERT_MAX_BYTES (well under a second); a bigger one is
// converted by the manual Compact (server/lib/db-retention.js). Best effort: a
// second process holding a read open makes it fail, and the next start retries.
const AUTO_VACUUM_CONVERT_MAX_BYTES = 32 * 1024 * 1024;
function convertAutoVacuum() {
try {
if (db.pragma("auto_vacuum", { simple: true }) !== 0) return false;
const bytes =
db.pragma("page_count", { simple: true }) * db.pragma("page_size", { simple: true });
if (bytes > AUTO_VACUUM_CONVERT_MAX_BYTES) return false;
db.pragma("auto_vacuum = INCREMENTAL");
db.exec("VACUUM");
return db.pragma("auto_vacuum", { simple: true }) === 2;
} catch {
return false;
}
}
convertAutoVacuum();
migrationLock.exec("COMMIT");
migrationLock.close();
@@ -2535,7 +2565,9 @@ const stmts = {
* on a reader — the other dashboard's), after which the next write truncates
* the WAL to `journal_size_limit`; and `PRAGMA optimize`, which refreshes the
* planner's statistics only where they went stale. Never VACUUM: it rewrites
* the whole file under an exclusive lock.
* the whole file under an exclusive lock. The opt-in retention and the
* bounded `incremental_vacuum` run after this, from
* `server/lib/db-retention.js`'s `runScheduledMaintenance`.
*/
function maintainDatabase() {
const out = {};
@@ -2561,4 +2593,6 @@ module.exports = {
applyIntroPricing,
topUpDefaultPricing,
maintainDatabase,
convertAutoVacuum,
AUTO_VACUUM_CONVERT_MAX_BYTES,
};