Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions packages/runtime/src/__tests__/owned-child-process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,10 @@ process.send = (message, ...rest) => {
// The command also starts a descendant in its own session, outside the
// supervisor's process group: only the supervisor's tree walk reaches it.
`const { spawn } = require('node:child_process');
spawn(process.execPath, ['-e', ${JSON.stringify(`setTimeout(() => require('node:fs').writeFileSync(${JSON.stringify(escapedLate)}, 'late'), 1000); setInterval(() => {}, 1000);`)}], { detached: true, stdio: 'ignore' }).unref();
setTimeout(() => require('node:fs').writeFileSync(${JSON.stringify(late)}, 'late'), 1000);
spawn(process.execPath, ['-e', ${JSON.stringify(`setTimeout(() => require('node:fs').writeFileSync(${JSON.stringify(escapedLate)}, 'late'), 5000); setInterval(() => {}, 1000);`)}], { detached: true, stdio: 'ignore' }).unref();
// Leave enough time for an overloaded CI worker to deliver the
// supervisor's fault and complete its tree walk before this fires.
setTimeout(() => require('node:fs').writeFileSync(${JSON.stringify(late)}, 'late'), 5000);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep the termination assertion later than the delayed writes

Both late and escapedLate are now written after 5,000 ms, but the assertions below still run after await delay(1200). On a promptly observed supervisor fault, a surviving command or detached descendant therefore passes both ENOENT assertions before its timer fires; cleanup then deletes the directory. A controlled surviving descendant passed this exact absence predicate at 1.2 seconds and wrote its marker after 5 seconds. Please wait beyond the marker deadline or verify both processes' death through deterministic readiness/liveness checks, and demonstrate that an intentionally surviving descendant makes the test fail. This is a regression in the test's ability to detect leaked children, not a claim that the production termination implementation currently leaks them.

setInterval(() => {}, 1000);`,
],
cwd: directory,
Expand All @@ -294,7 +296,8 @@ process.send = (message, ...rest) => {
}
}
supervisor.kill('SIGKILL');
await rm(directory, { recursive: true, force: true });
// Windows can retain a handle briefly after the detached descendant exits.
await rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
});

Expand Down
17 changes: 17 additions & 0 deletions packages/storage/src/__tests__/operational-state-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,23 @@ test('shares one operational database and produces an online backup', async () =
}
});

test('new runtime databases use incremental vacuum before operational schema creation', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-operational-incremental-vacuum-'));
try {
const lease = acquireOperationalStateDatabase(root);
try {
assert.equal(
(lease.database.prepare('PRAGMA auto_vacuum').get() as { auto_vacuum: number }).auto_vacuum,
2,
);
} finally {
lease.close();
}
} finally {
await rm(root, { recursive: true, force: true });
}
});

test('atomically reapplies current owner schema without republishing its registry', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-operational-current-convergence-'));
const databasePath = join(root, 'runtime.sqlite');
Expand Down
42 changes: 42 additions & 0 deletions packages/storage/src/__tests__/sqlite-runtime-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,48 @@ const PREFIX_PROOF_TEST_BUDGET = {
};

describe('SqliteRuntimeStore', () => {
it('leaves an existing NONE auto-vacuum database unchanged', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-sqlite-runtime-existing-none-'));
const dbPath = join(root, 'runtime.sqlite');
const existing = new DatabaseSync(dbPath);
existing.exec('CREATE TABLE existing_data (value TEXT NOT NULL)');
existing.close();
try {
const store = createSqliteRuntimeStore(dbPath);
store.close();
const inspected = new DatabaseSync(dbPath, { readOnly: true });
try {
assert.equal(
(inspected.prepare('PRAGMA auto_vacuum').get() as { auto_vacuum: number }).auto_vacuum,
0,
);
assert.ok(
inspected
.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'existing_data'")
.get(),
);
} finally {
inspected.close();
}
} finally {
await rm(root, { recursive: true, force: true });
}
});

it('creates runtime databases with incremental auto-vacuum', async () => {
await withStore(async (_store, dbPath) => {
const inspected = new DatabaseSync(dbPath, { readOnly: true });
try {
assert.equal(
(inspected.prepare('PRAGMA auto_vacuum').get() as { auto_vacuum: number }).auto_vacuum,
2,
);
} finally {
inspected.close();
}
});
});

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);
Expand Down
7 changes: 6 additions & 1 deletion packages/storage/src/operational-state-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { createRequire } from 'node:module';
import type { DatabaseSync } from 'node:sqlite';
import {
configureSqliteRuntimeDatabase,
configureSqliteRuntimeAutoVacuumForNewDatabase,
configureSqliteRuntimeLockWait,
migrateSqliteRuntimeDatabase,
readUserVersion,
Expand Down Expand Up @@ -208,7 +209,8 @@ class OperationalStateDatabaseOwner {
readonly databasePath: string,
options: OperationalStateDatabaseOptions,
) {
if (options.schemaMigration === 'require_current' && !existsSync(databasePath)) {
const databaseExisted = existsSync(databasePath);
if (options.schemaMigration === 'require_current' && !databaseExisted) {
throw new OperationalStateMigrationBlockedError(
new Error('Operational state has not been initialized by its Runtime Host'),
'requires_host_migration',
Expand All @@ -223,6 +225,9 @@ class OperationalStateDatabaseOwner {
try {
configureSqliteRuntimeLockWait(this.database);
this.database.exec('PRAGMA foreign_keys = ON');
if (options.schemaMigration !== 'require_current') {
configureSqliteRuntimeAutoVacuumForNewDatabase(this.database, databaseExisted);
}
if (options.schemaMigration === 'require_current') {
requireCurrentOperationalState(this.database);
} else {
Expand Down
19 changes: 19 additions & 0 deletions packages/storage/src/sqlite-runtime-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,25 @@ 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;
db.exec('PRAGMA auto_vacuum = INCREMENTAL');
const configured = db.prepare('PRAGMA auto_vacuum').get() as
| { auto_vacuum?: unknown }
| undefined;
if (configured?.auto_vacuum !== 2) {
throw new Error('New SQLite runtime database did not enable incremental auto-vacuum');
}
}

/** Configure connection-local lock waiting without changing persistent database state. */
export function configureSqliteRuntimeLockWait(db: DatabaseSync): void {
db.exec(`PRAGMA busy_timeout = ${SQLITE_INITIALIZATION_BUSY_TIMEOUT_MS}`);
Expand Down
5 changes: 4 additions & 1 deletion packages/storage/src/sqlite-runtime-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export type {
ImmutableRuntimePrefixProofReadBudget,
} from './runtime-event-store-contract.js';

import { mkdirSync } from 'node:fs';
import { existsSync, mkdirSync } from 'node:fs';
import { createRequire } from 'node:module';
import { dirname } from 'node:path';
import type { DatabaseSync, SQLInputValue } from 'node:sqlite';
Expand Down Expand Up @@ -133,6 +133,7 @@ import {
} from '@maka/core/tool-recovery-bundle';
import {
configureSqliteRuntimeDatabase,
configureSqliteRuntimeAutoVacuumForNewDatabase,
migrateSqliteRuntimeDatabase,
readUserVersion,
RUNTIME_RECOVERY_AUTHORITY_CAPABILITY,
Expand Down Expand Up @@ -345,6 +346,7 @@ 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);
Expand All @@ -358,6 +360,7 @@ export class SqliteRuntimeStore
);
}
} else {
configureSqliteRuntimeAutoVacuumForNewDatabase(this.db, databaseExisted);
configureSqliteRuntimeDatabase(this.db);
migrateSqliteRuntimeDatabase(this.db);
}
Expand Down