From cd45ae06edd21b997836cf6c4770199c6b649803 Mon Sep 17 00:00:00 2001 From: YayoiNanoka Date: Wed, 14 Oct 2026 10:25:53 +0800 Subject: [PATCH] fix(storage): initialize vacuum mode for concurrently created databases Use SQLite page_count instead of a pre-open file-existence check to detect an uninitialized database. A second opener must configure an empty file before schema creation rather than treating it as an existing database. Preserve initialized databases and configure lock waiting before inspecting the standalone runtime store. Cover both open paths with an existing empty file, preserve initialized NONE databases, and assert incremental vacuum in the multi-process open test. Generated-by: OpenAI Codex --- .../__tests__/operational-state-store.test.ts | 17 +++++++++++ .../sqlite-recovery-concurrency.test.ts | 1 + .../__tests__/sqlite-runtime-store.test.ts | 29 +++++++++++++++++++ .../storage/src/operational-state-store.ts | 2 +- packages/storage/src/sqlite-runtime-schema.ts | 17 +++++------ packages/storage/src/sqlite-runtime-store.ts | 7 +++-- 6 files changed, 59 insertions(+), 14 deletions(-) diff --git a/packages/storage/src/__tests__/operational-state-store.test.ts b/packages/storage/src/__tests__/operational-state-store.test.ts index 4c46073f295..9162405f83d 100644 --- a/packages/storage/src/__tests__/operational-state-store.test.ts +++ b/packages/storage/src/__tests__/operational-state-store.test.ts @@ -89,6 +89,23 @@ test('new runtime databases use incremental vacuum before operational schema cre } }); +test('an operational opener configures the empty file left by another connection', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-operational-empty-open-')); + const first = new DatabaseSync(join(root, 'runtime.sqlite')); + try { + const lease = acquireOperationalStateDatabase(root); + try { + assert.equal(lease.database.prepare('PRAGMA auto_vacuum').get()?.auto_vacuum, 2); + assert.equal(first.prepare('PRAGMA auto_vacuum').get()?.auto_vacuum, 2); + } finally { + lease.close(); + } + } finally { + first.close(); + await rm(root, { recursive: true, force: true }); + } +}); + test('workflow 12 upgrade adds wait storage atomically and preserves every released row and schema scope', async () => { const root = await mkdtemp(join(tmpdir(), 'maka-operational-workflow-12-')); const databasePath = join(root, 'runtime.sqlite'); diff --git a/packages/storage/src/__tests__/sqlite-recovery-concurrency.test.ts b/packages/storage/src/__tests__/sqlite-recovery-concurrency.test.ts index 35de68bff55..3af4c65b69c 100644 --- a/packages/storage/src/__tests__/sqlite-recovery-concurrency.test.ts +++ b/packages/storage/src/__tests__/sqlite-recovery-concurrency.test.ts @@ -236,6 +236,7 @@ describe('SQLite recovery authority multi-process races', () => { .user_version, SQLITE_RUNTIME_SCHEMA_VERSION, ); + assert.equal(database.prepare('PRAGMA auto_vacuum').get()?.auto_vacuum, 2); } finally { database.close(); } diff --git a/packages/storage/src/__tests__/sqlite-runtime-store.test.ts b/packages/storage/src/__tests__/sqlite-runtime-store.test.ts index e4b8452973e..8bdfdef42ce 100644 --- a/packages/storage/src/__tests__/sqlite-runtime-store.test.ts +++ b/packages/storage/src/__tests__/sqlite-runtime-store.test.ts @@ -127,6 +127,35 @@ describe('SqliteRuntimeStore', () => { }); }); + it('configures incremental auto-vacuum when another opener created the empty file', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-sqlite-runtime-empty-open-')); + const dbPath = join(root, 'runtime.sqlite'); + const first = new DatabaseSync(dbPath); + try { + const store = createSqliteRuntimeStore(dbPath); + store.close(); + assert.equal(first.prepare('PRAGMA auto_vacuum').get()?.auto_vacuum, 2); + } finally { + first.close(); + await rm(root, { recursive: true, force: true }); + } + }); + + it('preserves an initialized NONE database even when it has no tables', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-sqlite-runtime-empty-none-')); + const dbPath = join(root, 'runtime.sqlite'); + const first = new DatabaseSync(dbPath); + try { + first.exec('BEGIN IMMEDIATE; COMMIT'); + const store = createSqliteRuntimeStore(dbPath); + store.close(); + assert.equal(first.prepare('PRAGMA auto_vacuum').get()?.auto_vacuum, 0); + } finally { + first.close(); + await rm(root, { recursive: true, force: true }); + } + }); + it('reads a complete Session from one snapshot while another connection commits its terminal', async (t) => { await withStore(async (store, dbPath) => { const opening = invocationOpeningEvent(1); diff --git a/packages/storage/src/operational-state-store.ts b/packages/storage/src/operational-state-store.ts index a111215528d..ba657bd5b5b 100644 --- a/packages/storage/src/operational-state-store.ts +++ b/packages/storage/src/operational-state-store.ts @@ -226,7 +226,7 @@ class OperationalStateDatabaseOwner { configureSqliteRuntimeLockWait(this.database); this.database.exec('PRAGMA foreign_keys = ON'); if (options.schemaMigration !== 'require_current') { - configureSqliteRuntimeAutoVacuumForNewDatabase(this.database, databaseExisted); + configureSqliteRuntimeAutoVacuumForNewDatabase(this.database); } if (options.schemaMigration === 'require_current') { requireCurrentOperationalState(this.database); diff --git a/packages/storage/src/sqlite-runtime-schema.ts b/packages/storage/src/sqlite-runtime-schema.ts index 5591162eec6..e2ae018706e 100644 --- a/packages/storage/src/sqlite-runtime-schema.ts +++ b/packages/storage/src/sqlite-runtime-schema.ts @@ -954,16 +954,13 @@ export function configureSqliteRuntimeDatabase(db: DatabaseSync): void { db.exec('PRAGMA foreign_keys = ON'); } -/** Set the reclamation mode before schema creation on a newly created file only. */ -export function configureSqliteRuntimeAutoVacuumForNewDatabase( - db: DatabaseSync, - databaseExisted: boolean, -): void { - if (databaseExisted) return; - const existingSchema = db - .prepare("SELECT 1 AS present FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%' LIMIT 1") - .get() as { present?: unknown } | undefined; - if (existingSchema?.present === 1) return; +/** Set the reclamation mode before an uninitialized database gets its first page. */ +export function configureSqliteRuntimeAutoVacuumForNewDatabase(db: DatabaseSync): void { + // Another opener may have created the file without initializing its header. + // File existence cannot tell us whether it still needs this configuration. + // Preserve initialized databases, including those with no application tables. + const pages = db.prepare('PRAGMA page_count').get() as { page_count: number }; + if (pages.page_count !== 0) return; db.exec('PRAGMA auto_vacuum = INCREMENTAL'); const configured = db.prepare('PRAGMA auto_vacuum').get() as | { auto_vacuum?: unknown } diff --git a/packages/storage/src/sqlite-runtime-store.ts b/packages/storage/src/sqlite-runtime-store.ts index ceb3cf9b188..96df894ea83 100644 --- a/packages/storage/src/sqlite-runtime-store.ts +++ b/packages/storage/src/sqlite-runtime-store.ts @@ -36,7 +36,7 @@ export type { ImmutableRuntimePrefixProofReadBudget, } from './runtime-event-store-contract.js'; -import { existsSync, mkdirSync } from 'node:fs'; +import { mkdirSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname } from 'node:path'; import type { DatabaseSync, SQLInputValue } from 'node:sqlite'; @@ -134,6 +134,7 @@ import { import { configureSqliteRuntimeDatabase, configureSqliteRuntimeAutoVacuumForNewDatabase, + configureSqliteRuntimeLockWait, migrateSqliteRuntimeDatabase, readUserVersion, RUNTIME_RECOVERY_AUTHORITY_CAPABILITY, @@ -346,7 +347,6 @@ export class SqliteRuntimeStore return; } const DatabaseSync = loadDatabaseSync(); - const databaseExisted = path !== ':memory:' && existsSync(path); this.db = options.readOnly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path); @@ -360,7 +360,8 @@ export class SqliteRuntimeStore ); } } else { - configureSqliteRuntimeAutoVacuumForNewDatabase(this.db, databaseExisted); + configureSqliteRuntimeLockWait(this.db); + configureSqliteRuntimeAutoVacuumForNewDatabase(this.db); configureSqliteRuntimeDatabase(this.db); migrateSqliteRuntimeDatabase(this.db); }