mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
fix(db): keep SQLite temp files in memory so trips can be deleted (#2518)
Deleting a trip failed with SQLITE_IOERR_GETTEMPPATH when the container runs with a read-only root filesystem and no tmpfs on /tmp. The cascade behind DELETE FROM trips keeps a statement journal, SQLite moves it to a temp file once it passes 64 KiB, and it looks for a directory for that file in SQLITE_TMPDIR, TMPDIR, /var/tmp, /usr/tmp, /tmp and the working directory. None of them is writable there. Installs on the shipped compose file have a tmpfs on /tmp and never hit it. Every connection the server opens now comes from openDatabase() in src/db/connection.ts, which sets temp_store = MEMORY: the main database (also when it is reopened after a restore), each plugin's own database and the uploaded backup checked before a restore. reset-admin.js and the key rotation script cannot import src/ in the image, so they set the pragma themselves. A test pins that nothing else in src/ constructs a handle, and one deletes a full trip on the real schema and watches for temp files.
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import Database from 'better-sqlite3';
|
||||
|
||||
/**
|
||||
* Opens a SQLite connection. Every handle the server opens comes from here (the
|
||||
* main database, each plugin's own database, a backup being checked before a
|
||||
* restore), so what has to hold for all of them is set in one place.
|
||||
*
|
||||
* Temporary storage stays in memory. Statement journals, sort runs and transient
|
||||
* indices move to a temp file once they outgrow a small in-memory buffer, and
|
||||
* SQLite looks for a directory to hold that file in SQLITE_TMPDIR, TMPDIR,
|
||||
* /var/tmp, /usr/tmp, /tmp and the working directory. A container with a
|
||||
* read-only root filesystem and no tmpfs on /tmp has none of them writable, so
|
||||
* the first statement that needs a temp file fails with SQLITE_IOERR_GETTEMPPATH.
|
||||
* Deleting a trip is where that showed: the cascade keeps a statement journal
|
||||
* larger than the 64 KiB SQLite holds before it spills to disk (#2518).
|
||||
*
|
||||
* The data directory is the only place TREK can count on being able to write,
|
||||
* and SQLite has no supported per-connection way to put temp files there, so
|
||||
* they stay in RAM. The shipped compose file already had them there, its /tmp
|
||||
* being a tmpfs. VACUUM INTO, the only VACUUM TREK runs, writes straight into
|
||||
* its target file and builds no temporary database.
|
||||
*/
|
||||
export function openDatabase(filename: string, options?: Database.Options): Database.Database {
|
||||
const db = new Database(filename, options);
|
||||
db.pragma('temp_store = MEMORY');
|
||||
return db;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import type Database from 'better-sqlite3';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { readEnv } from '../app-config';
|
||||
import { openDatabase } from './connection';
|
||||
import { applyDurabilityPragmas } from './durability';
|
||||
import { createTables } from './schema';
|
||||
import { runMigrations } from './migrations';
|
||||
@@ -39,7 +40,7 @@ function initDb(): void {
|
||||
_db = null;
|
||||
}
|
||||
|
||||
_db = new Database(dbPath);
|
||||
_db = openDatabase(dbPath);
|
||||
// Ahead of the journal switch now: changing journal_mode needs an exclusive
|
||||
// lock, which a sibling process (reset-admin, the rotation script) may hold.
|
||||
_db.exec('PRAGMA busy_timeout = 5000');
|
||||
|
||||
@@ -4,7 +4,8 @@ import path from 'path';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import { readEnv } from '../../app-config';
|
||||
import fs from 'fs';
|
||||
import Database from 'better-sqlite3';
|
||||
import type Database from 'better-sqlite3';
|
||||
import { openDatabase } from '../../db/connection';
|
||||
import { db, closeDb, reinitialize } from '../../db/database';
|
||||
import { VALID_INTERVALS } from './auto-backup.settings';
|
||||
import { invalidatePermissionsCache } from '../permissions/permissions-cache';
|
||||
@@ -436,7 +437,7 @@ export async function restoreFromZip(storage: StorageService, zipPath: string):
|
||||
|
||||
let uploadedDb: InstanceType<typeof Database> | null = null;
|
||||
try {
|
||||
uploadedDb = new Database(extractedDb, { readonly: true });
|
||||
uploadedDb = openDatabase(extractedDb, { readonly: true });
|
||||
|
||||
const integrityResult = uploadedDb.prepare('PRAGMA integrity_check').get() as { integrity_check: string };
|
||||
if (integrityResult.integrity_check !== 'ok') {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import Database from 'better-sqlite3';
|
||||
import type { Database as Db } from 'better-sqlite3';
|
||||
import { openDatabase } from '../../../db/connection';
|
||||
import { pluginDataDir, pluginDbFile, pluginsDataRoot } from '../paths';
|
||||
|
||||
/**
|
||||
@@ -63,7 +63,7 @@ export class PluginDataDb {
|
||||
constructor(pluginId: string) {
|
||||
this.pluginId = pluginId;
|
||||
fs.mkdirSync(pluginDataDir(pluginId), { recursive: true });
|
||||
this.db = new Database(pluginDbFile(pluginId));
|
||||
this.db = openDatabase(pluginDbFile(pluginId));
|
||||
this.db.pragma('journal_mode = WAL');
|
||||
this.db.pragma('foreign_keys = ON');
|
||||
openDbs.add(this);
|
||||
|
||||
Reference in New Issue
Block a user