mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
fix(apply-migrations): unblock PGLite chain (closes #1100)
`gbrain apply-migrations --yes` was wedging on the v0.11.0 (Minions) schema phase for PGLite installs. Two compounding bugs: 1. `apply-migrations` pre-flight schema-version warning connects to PGLite to read config.version, then disconnects. The brief lock hold races with downstream subprocess spawns that try to re-acquire it; the 30s lock timeout fires before the parent fully releases. Pre-flight is a *warning*; on PGLite it adds no information the orchestrators don't already handle. Skip the probe for PGLite. 2. v0.11.0 phase A spawned `gbrain init --migrate-only` as an execSync subprocess to apply schema migrations. PGLite is single-writer; the subprocess inherits HOME and tries to lock the same DB. On Postgres this works (concurrent connections OK); on PGLite it deadlocks. Route in-process for PGLite — create + connect + initSchema + disconnect directly, skipping the subprocess hop. Postgres keeps the legacy execSync path. Verified: fresh PGLite install now walks the full migration chain through v0.32.2 (Facts SoR) and lands "All migrations up to date" on re-run. Closes #1100.
This commit is contained in:
@@ -364,17 +364,27 @@ export async function runApplyMigrations(args: string[]): Promise<void> {
|
||||
const { createEngine } = await import('../core/engine-factory.ts');
|
||||
const cfg = lc();
|
||||
if (cfg) {
|
||||
const eng = await createEngine(toEngineConfig(cfg));
|
||||
await eng.connect(toEngineConfig(cfg));
|
||||
const verStr = await eng.getConfig('version');
|
||||
const schemaVer = parseInt(verStr || '1', 10);
|
||||
await eng.disconnect();
|
||||
if (schemaVer < LATEST_VERSION) {
|
||||
console.warn(
|
||||
`\n⚠️ Schema version ${schemaVer} is behind latest ${LATEST_VERSION}.\n` +
|
||||
` Schema migrations run automatically on next connectEngine() / initSchema().\n` +
|
||||
` To run them now: gbrain init --migrate-only\n`,
|
||||
);
|
||||
// v0.36.x #1100: skip the pre-flight warning on PGLite. The probe
|
||||
// briefly holds the single-writer lock; if a downstream orchestrator
|
||||
// phase spawns `gbrain init --migrate-only` as a subprocess (the
|
||||
// legacy v0.11.0 phase A path), the child can race the parent's
|
||||
// lock release and hit a 30s timeout. The orchestrators handle
|
||||
// schema lifecycle internally on PGLite (phase A routes in-process),
|
||||
// so the warning here adds no information for PGLite users.
|
||||
const skipPreflight = cfg.engine === 'pglite';
|
||||
if (!skipPreflight) {
|
||||
const eng = await createEngine(toEngineConfig(cfg));
|
||||
await eng.connect(toEngineConfig(cfg));
|
||||
const verStr = await eng.getConfig('version');
|
||||
const schemaVer = parseInt(verStr || '1', 10);
|
||||
await eng.disconnect();
|
||||
if (schemaVer < LATEST_VERSION) {
|
||||
console.warn(
|
||||
`\n⚠️ Schema version ${schemaVer} is behind latest ${LATEST_VERSION}.\n` +
|
||||
` Schema migrations run automatically on next connectEngine() / initSchema().\n` +
|
||||
` To run them now: gbrain init --migrate-only\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -58,9 +58,30 @@ export interface PendingHostWorkEntry {
|
||||
// Phase A — Schema
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
function phaseASchema(opts: OrchestratorOpts): OrchestratorPhaseResult {
|
||||
async function phaseASchema(opts: OrchestratorOpts): Promise<OrchestratorPhaseResult> {
|
||||
if (opts.dryRun) return { name: 'schema', status: 'skipped', detail: 'dry-run' };
|
||||
try {
|
||||
// v0.36.x #1100: route PGLite through an in-process schema apply rather
|
||||
// than `execSync('gbrain init --migrate-only')`. The subprocess inherits
|
||||
// HOME and tries to acquire the same file lock the parent process is
|
||||
// holding (or briefly released and the on-disk artifact has not finished
|
||||
// settling), which deadlocks until the 30s lock timeout fires. The
|
||||
// structural fix is to not spawn a subprocess for work the parent can
|
||||
// do directly — Postgres tolerates concurrent connections, so the
|
||||
// legacy execSync path stays for Postgres callers.
|
||||
const { loadConfig, toEngineConfig } = await import('../../core/config.ts');
|
||||
const cfg = loadConfig();
|
||||
if (cfg?.engine === 'pglite') {
|
||||
const { createEngine } = await import('../../core/engine-factory.ts');
|
||||
const eng = await createEngine(toEngineConfig(cfg));
|
||||
try {
|
||||
await eng.connect(toEngineConfig(cfg));
|
||||
await eng.initSchema();
|
||||
} finally {
|
||||
try { await eng.disconnect(); } catch { /* best-effort */ }
|
||||
}
|
||||
return { name: 'schema', status: 'complete' };
|
||||
}
|
||||
execSync('gbrain init --migrate-only' + childGlobalFlags(), { stdio: 'inherit', timeout: 60_000, env: process.env });
|
||||
return { name: 'schema', status: 'complete' };
|
||||
} catch (e) {
|
||||
@@ -413,7 +434,7 @@ function phaseFInstall(opts: OrchestratorOpts): OrchestratorPhaseResult {
|
||||
async function orchestrator(opts: OrchestratorOpts): Promise<OrchestratorResult> {
|
||||
const phases: OrchestratorPhaseResult[] = [];
|
||||
|
||||
const a = phaseASchema(opts);
|
||||
const a = await phaseASchema(opts);
|
||||
phases.push(a);
|
||||
if (a.status === 'failed') {
|
||||
console.error(`Phase A (schema) failed: ${a.detail}. Aborting; re-run after fixing.`);
|
||||
|
||||
Reference in New Issue
Block a user