mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-18 21:47:20 +02:00
Merge pull request #2234 from time-attack/time-attack/issue-2194-pglite-lock
fix(gbrain): distinguish PGLite locks from malformed config
This commit is contained in:
@@ -18,7 +18,7 @@
|
|||||||
* "gstack_brain_sync_mode": "off"|"artifacts-only"|"full",
|
* "gstack_brain_sync_mode": "off"|"artifacts-only"|"full",
|
||||||
* "gstack_brain_git": true|false,
|
* "gstack_brain_git": true|false,
|
||||||
* "gstack_artifacts_remote": "https://..." | "",
|
* "gstack_artifacts_remote": "https://..." | "",
|
||||||
* "gbrain_local_status": "ok"|"no-cli"|"missing-config"|"broken-config"|"broken-db"|"timeout",
|
* "gbrain_local_status": "ok"|"no-cli"|"missing-config"|"broken-config"|"broken-db"|"engine-locked"|"timeout",
|
||||||
* "gbrain_pooler_mode": "transaction"|"session"|null
|
* "gbrain_pooler_mode": "transaction"|"session"|null
|
||||||
* }
|
* }
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -717,6 +717,7 @@ function dreamMarkerPid(): number | null {
|
|||||||
* missing-config → "no local engine; run /setup-gbrain to add local PGLite"
|
* missing-config → "no local engine; run /setup-gbrain to add local PGLite"
|
||||||
* broken-config → "config file at ~/.gbrain/config.json is malformed; see /setup-gbrain Step 1.5"
|
* broken-config → "config file at ~/.gbrain/config.json is malformed; see /setup-gbrain Step 1.5"
|
||||||
* broken-db → "config points at unreachable DB; see /setup-gbrain Step 1.5"
|
* broken-db → "config points at unreachable DB; see /setup-gbrain Step 1.5"
|
||||||
|
* engine-locked → PGLite is busy; stop its holder or sync outside the live session
|
||||||
* timeout → kept for Record totality; stages PROCEED on timeout (#1964)
|
* timeout → kept for Record totality; stages PROCEED on timeout (#1964)
|
||||||
* via the gate's warnProbeTimeout path, never this skip.
|
* via the gate's warnProbeTimeout path, never this skip.
|
||||||
*/
|
*/
|
||||||
@@ -733,6 +734,8 @@ function skipStageForLocalStatus(
|
|||||||
"config at ~/.gbrain/config.json is malformed; see /setup-gbrain Step 1.5",
|
"config at ~/.gbrain/config.json is malformed; see /setup-gbrain Step 1.5",
|
||||||
"broken-db":
|
"broken-db":
|
||||||
"config points at unreachable DB; see /setup-gbrain Step 1.5",
|
"config points at unreachable DB; see /setup-gbrain Step 1.5",
|
||||||
|
"engine-locked":
|
||||||
|
"PGLite is busy (often held by gbrain serve); stop the holding process or run /sync-gbrain outside the live Claude session, then retry",
|
||||||
"timeout":
|
"timeout":
|
||||||
"engine probe timed out; raise GSTACK_GBRAIN_PROBE_TIMEOUT_MS if your pooler is slow",
|
"engine probe timed out; raise GSTACK_GBRAIN_PROBE_TIMEOUT_MS if your pooler is slow",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
* Broken-config → config exists but `gbrain sources list` fails with config parse error
|
* Broken-config → config exists but `gbrain sources list` fails with config parse error
|
||||||
* (or any non-recognized error — defensive default per codex #8).
|
* (or any non-recognized error — defensive default per codex #8).
|
||||||
* Broken-db → config exists, DB unreachable per stderr classification.
|
* Broken-db → config exists, DB unreachable per stderr classification.
|
||||||
|
* Engine-locked → PGLite probe hit gbrain's own connect timeout, usually
|
||||||
|
* because another `gbrain serve` process owns the embedded DB.
|
||||||
* Timeout → probe exceeded GSTACK_GBRAIN_PROBE_TIMEOUT_MS (default 15s) with no
|
* Timeout → probe exceeded GSTACK_GBRAIN_PROBE_TIMEOUT_MS (default 15s) with no
|
||||||
* recognized error — engine is likely healthy but slow (e.g. a cold
|
* recognized error — engine is likely healthy but slow (e.g. a cold
|
||||||
* pooler connection, #1964). Consumers treat this as usable.
|
* pooler connection, #1964). Consumers treat this as usable.
|
||||||
@@ -47,6 +49,7 @@ export type LocalEngineStatus =
|
|||||||
| "missing-config"
|
| "missing-config"
|
||||||
| "broken-config"
|
| "broken-config"
|
||||||
| "broken-db"
|
| "broken-db"
|
||||||
|
| "engine-locked"
|
||||||
| "timeout";
|
| "timeout";
|
||||||
|
|
||||||
export interface ClassifyOptions {
|
export interface ClassifyOptions {
|
||||||
@@ -118,6 +121,15 @@ function gbrainConfigPath(env?: NodeJS.ProcessEnv): string {
|
|||||||
return join(gbrainHome, "config.json");
|
return join(gbrainHome, "config.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function configuredEngine(env?: NodeJS.ProcessEnv): "pglite" | "postgres" | null {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(readFileSync(gbrainConfigPath(env), "utf-8")) as { engine?: string };
|
||||||
|
return parsed.engine === "pglite" || parsed.engine === "postgres" ? parsed.engine : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function hashPath(p: string): string {
|
function hashPath(p: string): string {
|
||||||
return createHash("sha256").update(p).digest("hex").slice(0, 16);
|
return createHash("sha256").update(p).digest("hex").slice(0, 16);
|
||||||
}
|
}
|
||||||
@@ -281,6 +293,7 @@ function freshClassify(env?: NodeJS.ProcessEnv): LocalEngineStatus {
|
|||||||
stderr?: Buffer | string;
|
stderr?: Buffer | string;
|
||||||
killed?: boolean;
|
killed?: boolean;
|
||||||
signal?: NodeJS.Signals | null;
|
signal?: NodeJS.Signals | null;
|
||||||
|
status?: number | null;
|
||||||
};
|
};
|
||||||
const stderr = (e.stderr ? e.stderr.toString() : "") || "";
|
const stderr = (e.stderr ? e.stderr.toString() : "") || "";
|
||||||
|
|
||||||
@@ -292,6 +305,14 @@ function freshClassify(env?: NodeJS.ProcessEnv): LocalEngineStatus {
|
|||||||
if (stderr.includes("Cannot connect to database")) return "broken-db";
|
if (stderr.includes("Cannot connect to database")) return "broken-db";
|
||||||
if (stderr.includes("config.json")) return "broken-config";
|
if (stderr.includes("config.json")) return "broken-config";
|
||||||
|
|
||||||
|
// PGLite is single-process. A long-lived `gbrain serve` can own the
|
||||||
|
// embedded database, causing the CLI to finish with its own exit 124 and
|
||||||
|
// "connect timed out" message. This is neither our watchdog timeout nor
|
||||||
|
// evidence that the valid config is malformed (#2194).
|
||||||
|
if (stderr.includes("connect timed out") || e.status === 124) {
|
||||||
|
return configuredEngine(env) === "pglite" ? "engine-locked" : "broken-db";
|
||||||
|
}
|
||||||
|
|
||||||
// Probe killed by the timeout with no recognized error: the engine is
|
// Probe killed by the timeout with no recognized error: the engine is
|
||||||
// most likely healthy but slow (cold pooler connections measured at
|
// most likely healthy but slow (cold pooler connections measured at
|
||||||
// 6.9-10.7s in #1964). Don't tell the user their config is malformed.
|
// 6.9-10.7s in #1964). Don't tell the user their config is malformed.
|
||||||
|
|||||||
@@ -820,7 +820,7 @@ Capture the JSON output. It contains: `gbrain_on_path`, `gbrain_version`,
|
|||||||
`gbrain_config_exists`, `gbrain_engine`, `gbrain_doctor_ok`, `gbrain_mcp_mode`,
|
`gbrain_config_exists`, `gbrain_engine`, `gbrain_doctor_ok`, `gbrain_mcp_mode`,
|
||||||
`gstack_brain_sync_mode`, `gstack_brain_git`, `gstack_artifacts_remote`, and
|
`gstack_brain_sync_mode`, `gstack_brain_git`, `gstack_artifacts_remote`, and
|
||||||
the v1.34.0.0+ `gbrain_local_status` field (one of: `ok`, `no-cli`,
|
the v1.34.0.0+ `gbrain_local_status` field (one of: `ok`, `no-cli`,
|
||||||
`missing-config`, `broken-config`, `broken-db`, `timeout`). Treat `timeout`
|
`missing-config`, `broken-config`, `broken-db`, `engine-locked`, `timeout`). Treat `timeout`
|
||||||
like `ok` (slow-but-healthy engine, #1964) — it never triggers Step 1.5
|
like `ok` (slow-but-healthy engine, #1964) — it never triggers Step 1.5
|
||||||
remediation.
|
remediation.
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ Capture the JSON output. It contains: `gbrain_on_path`, `gbrain_version`,
|
|||||||
`gbrain_config_exists`, `gbrain_engine`, `gbrain_doctor_ok`, `gbrain_mcp_mode`,
|
`gbrain_config_exists`, `gbrain_engine`, `gbrain_doctor_ok`, `gbrain_mcp_mode`,
|
||||||
`gstack_brain_sync_mode`, `gstack_brain_git`, `gstack_artifacts_remote`, and
|
`gstack_brain_sync_mode`, `gstack_brain_git`, `gstack_artifacts_remote`, and
|
||||||
the v1.34.0.0+ `gbrain_local_status` field (one of: `ok`, `no-cli`,
|
the v1.34.0.0+ `gbrain_local_status` field (one of: `ok`, `no-cli`,
|
||||||
`missing-config`, `broken-config`, `broken-db`, `timeout`). Treat `timeout`
|
`missing-config`, `broken-config`, `broken-db`, `engine-locked`, `timeout`). Treat `timeout`
|
||||||
like `ok` (slow-but-healthy engine, #1964) — it never triggers Step 1.5
|
like `ok` (slow-but-healthy engine, #1964) — it never triggers Step 1.5
|
||||||
remediation.
|
remediation.
|
||||||
|
|
||||||
|
|||||||
@@ -893,6 +893,10 @@ BEFORE invoking the orchestrator:
|
|||||||
slow (cold pooler connection, #1964). Tell the user in one line: "Engine
|
slow (cold pooler connection, #1964). Tell the user in one line: "Engine
|
||||||
probe timed out (>15s) — proceeding; raise `GSTACK_GBRAIN_PROBE_TIMEOUT_MS`
|
probe timed out (>15s) — proceeding; raise `GSTACK_GBRAIN_PROBE_TIMEOUT_MS`
|
||||||
if your pooler is slow." Do NOT treat this as a broken config.
|
if your pooler is slow." Do NOT treat this as a broken config.
|
||||||
|
- **`engine-locked`**: STOP. "The local PGLite database is busy, usually
|
||||||
|
because `gbrain serve` from a live Claude session owns it. Stop that process
|
||||||
|
or run `/sync-gbrain` outside the live session, then retry. This identifies
|
||||||
|
the conflict but does not remove PGLite's single-process limit."
|
||||||
- **`no-cli`**: STOP. "Local gbrain CLI not installed. Run `/setup-gbrain`
|
- **`no-cli`**: STOP. "Local gbrain CLI not installed. Run `/setup-gbrain`
|
||||||
first."
|
first."
|
||||||
- **`missing-config`** AND `gbrain_mcp_mode == "remote-http"`: tell the user
|
- **`missing-config`** AND `gbrain_mcp_mode == "remote-http"`: tell the user
|
||||||
|
|||||||
@@ -139,6 +139,10 @@ BEFORE invoking the orchestrator:
|
|||||||
slow (cold pooler connection, #1964). Tell the user in one line: "Engine
|
slow (cold pooler connection, #1964). Tell the user in one line: "Engine
|
||||||
probe timed out (>15s) — proceeding; raise `GSTACK_GBRAIN_PROBE_TIMEOUT_MS`
|
probe timed out (>15s) — proceeding; raise `GSTACK_GBRAIN_PROBE_TIMEOUT_MS`
|
||||||
if your pooler is slow." Do NOT treat this as a broken config.
|
if your pooler is slow." Do NOT treat this as a broken config.
|
||||||
|
- **`engine-locked`**: STOP. "The local PGLite database is busy, usually
|
||||||
|
because `gbrain serve` from a live Claude session owns it. Stop that process
|
||||||
|
or run `/sync-gbrain` outside the live session, then retry. This identifies
|
||||||
|
the conflict but does not remove PGLite's single-process limit."
|
||||||
- **`no-cli`**: STOP. "Local gbrain CLI not installed. Run `/setup-gbrain`
|
- **`no-cli`**: STOP. "Local gbrain CLI not installed. Run `/setup-gbrain`
|
||||||
first."
|
first."
|
||||||
- **`missing-config`** AND `gbrain_mcp_mode == "remote-http"`: tell the user
|
- **`missing-config`** AND `gbrain_mcp_mode == "remote-http"`: tell the user
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
* on PATH that emits canned exit codes + stderr matching the patterns the
|
* on PATH that emits canned exit codes + stderr matching the patterns the
|
||||||
* classifier looks for.
|
* classifier looks for.
|
||||||
*
|
*
|
||||||
* Six status cases:
|
* Seven status cases:
|
||||||
* 1. no-cli — gbrain absent from PATH
|
* 1. no-cli — gbrain absent from PATH
|
||||||
* 2. missing-config — gbrain present, config.json absent (honors GBRAIN_HOME)
|
* 2. missing-config — gbrain present, config.json absent (honors GBRAIN_HOME)
|
||||||
* 3. broken-config — gbrain present, config exists, stderr contains "config.json"
|
* 3. broken-config — gbrain present, config exists, stderr contains "config.json"
|
||||||
* 4. broken-db — gbrain present, config exists, stderr contains "Cannot connect to database"
|
* 4. broken-db — gbrain present, config exists, stderr contains "Cannot connect to database"
|
||||||
* 5. timeout — probe exceeds GSTACK_GBRAIN_PROBE_TIMEOUT_MS with no recognized error (#1964)
|
* 5. timeout — probe exceeds GSTACK_GBRAIN_PROBE_TIMEOUT_MS with no recognized error (#1964)
|
||||||
* 6. ok — gbrain present, config exists, sources list returns valid JSON
|
* 6. engine-locked — PGLite CLI exits 124 because another process owns the DB (#2194)
|
||||||
|
* 7. ok — gbrain present, config exists, sources list returns valid JSON
|
||||||
*
|
*
|
||||||
* Plus cache behavior: hit, TTL expiry, invariant invalidation (HOME change,
|
* Plus cache behavior: hit, TTL expiry, invariant invalidation (HOME change,
|
||||||
* probe-timeout change), --no-cache bypass. Timeout tests keep runtime sane by
|
* probe-timeout change), --no-cache bypass. Timeout tests keep runtime sane by
|
||||||
@@ -61,7 +62,7 @@ interface FakeEnv {
|
|||||||
*/
|
*/
|
||||||
function makeEnv(opts: {
|
function makeEnv(opts: {
|
||||||
withGbrain?: boolean;
|
withGbrain?: boolean;
|
||||||
gbrainBehavior?: "ok" | "broken-db" | "broken-config" | "throws" | "slow";
|
gbrainBehavior?: "ok" | "broken-db" | "broken-config" | "engine-locked" | "throws" | "slow";
|
||||||
withConfig?: boolean;
|
withConfig?: boolean;
|
||||||
}): FakeEnv {
|
}): FakeEnv {
|
||||||
const tmp = mkdtempSync(join(tmpdir(), "gbrain-local-status-test-"));
|
const tmp = mkdtempSync(join(tmpdir(), "gbrain-local-status-test-"));
|
||||||
@@ -102,7 +103,7 @@ function makeEnv(opts: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function makeFakeGbrainScript(
|
function makeFakeGbrainScript(
|
||||||
behavior: "ok" | "broken-db" | "broken-config" | "throws" | "slow",
|
behavior: "ok" | "broken-db" | "broken-config" | "engine-locked" | "throws" | "slow",
|
||||||
): string {
|
): string {
|
||||||
// "slow": healthy engine on a cold pooler connection (#1964) — sleeps past
|
// "slow": healthy engine on a cold pooler connection (#1964) — sleeps past
|
||||||
// the (test-lowered) probe timeout, then would answer fine.
|
// the (test-lowered) probe timeout, then would answer fine.
|
||||||
@@ -125,10 +126,12 @@ exit 0
|
|||||||
? 'echo "Cannot connect to database: . Fix: Check your connection URL in ~/.gbrain/config.json" >&2'
|
? 'echo "Cannot connect to database: . Fix: Check your connection URL in ~/.gbrain/config.json" >&2'
|
||||||
: behavior === "broken-config"
|
: behavior === "broken-config"
|
||||||
? 'echo "Error: malformed config.json at ~/.gbrain/config.json" >&2'
|
? 'echo "Error: malformed config.json at ~/.gbrain/config.json" >&2'
|
||||||
|
: behavior === "engine-locked"
|
||||||
|
? 'echo "gbrain sources: connect timed out (default 10000ms; pass --timeout=Ns to override)." >&2'
|
||||||
: behavior === "throws"
|
: behavior === "throws"
|
||||||
? 'echo "unexpected gbrain failure" >&2'
|
? 'echo "unexpected gbrain failure" >&2'
|
||||||
: "";
|
: "";
|
||||||
const exitCode = behavior === "ok" ? 0 : 1;
|
const exitCode = behavior === "ok" ? 0 : behavior === "engine-locked" ? 124 : 1;
|
||||||
return `#!/bin/sh
|
return `#!/bin/sh
|
||||||
if [ "$1" = "--version" ]; then
|
if [ "$1" = "--version" ]; then
|
||||||
echo "gbrain 0.33.1.0"
|
echo "gbrain 0.33.1.0"
|
||||||
@@ -225,6 +228,19 @@ describe("lib/gbrain-local-status — status classification", () => {
|
|||||||
expect(localEngineStatus({ noCache: true })).toBe("broken-config");
|
expect(localEngineStatus({ noCache: true })).toBe("broken-config");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns 'engine-locked' when PGLite exits 124 with its own connect timeout (#2194)", () => {
|
||||||
|
env = makeEnv({ withGbrain: true, gbrainBehavior: "engine-locked", withConfig: true });
|
||||||
|
restoreEnv = applyEnv(env);
|
||||||
|
expect(localEngineStatus({ noCache: true })).toBe("engine-locked");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("classifies a non-PGLite connect timeout as unreachable DB, not malformed config", () => {
|
||||||
|
env = makeEnv({ withGbrain: true, gbrainBehavior: "engine-locked", withConfig: true });
|
||||||
|
restoreEnv = applyEnv(env);
|
||||||
|
writeFileSync(env.configPath, JSON.stringify({ engine: "postgres", database_url: "postgres://fake" }));
|
||||||
|
expect(localEngineStatus({ noCache: true })).toBe("broken-db");
|
||||||
|
});
|
||||||
|
|
||||||
it("returns 'ok' when sources list succeeds", () => {
|
it("returns 'ok' when sources list succeeds", () => {
|
||||||
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
|
env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
|
||||||
restoreEnv = applyEnv(env);
|
restoreEnv = applyEnv(env);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ interface FakeEnv {
|
|||||||
*/
|
*/
|
||||||
function makeEnv(opts: {
|
function makeEnv(opts: {
|
||||||
withGbrain: boolean;
|
withGbrain: boolean;
|
||||||
gbrainBehavior?: "ok" | "broken-db" | "broken-config" | "slow";
|
gbrainBehavior?: "ok" | "broken-db" | "broken-config" | "engine-locked" | "slow";
|
||||||
withConfig: boolean;
|
withConfig: boolean;
|
||||||
}): FakeEnv {
|
}): FakeEnv {
|
||||||
const tmp = mkdtempSync(join(tmpdir(), "gbrain-sync-skip-"));
|
const tmp = mkdtempSync(join(tmpdir(), "gbrain-sync-skip-"));
|
||||||
@@ -75,6 +75,9 @@ function makeEnv(opts: {
|
|||||||
: behavior === "ok"
|
: behavior === "ok"
|
||||||
? ` echo '{"sources":[]}'
|
? ` echo '{"sources":[]}'
|
||||||
exit 0`
|
exit 0`
|
||||||
|
: behavior === "engine-locked"
|
||||||
|
? ` echo "gbrain sources: connect timed out (default 10000ms; pass --timeout=Ns to override)." >&2
|
||||||
|
exit 124`
|
||||||
: ` ${
|
: ` ${
|
||||||
behavior === "broken-db"
|
behavior === "broken-db"
|
||||||
? 'echo "Cannot connect to database: . Fix: Check your connection URL in ~/.gbrain/config.json" >&2'
|
? 'echo "Cannot connect to database: . Fix: Check your connection URL in ~/.gbrain/config.json" >&2'
|
||||||
@@ -207,6 +210,20 @@ describe("gstack-gbrain-sync — split-engine SKIP (plan D12)", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("SKIPs with actionable guidance when PGLite is held by gbrain serve (#2194)", () => {
|
||||||
|
const env = makeEnv({ withGbrain: true, gbrainBehavior: "engine-locked", withConfig: true });
|
||||||
|
try {
|
||||||
|
const r = runOrchestrator(env, ["--code-only"]);
|
||||||
|
const out = r.stdout + r.stderr;
|
||||||
|
expect(out).toContain("local engine engine-locked");
|
||||||
|
expect(out).toContain("gbrain serve");
|
||||||
|
expect(out).toContain("outside the live Claude session");
|
||||||
|
expect(out).not.toContain("config.json is malformed");
|
||||||
|
} finally {
|
||||||
|
env.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("SKIPs code stage when gbrain CLI is missing (no-cli)", () => {
|
it("SKIPs code stage when gbrain CLI is missing (no-cli)", () => {
|
||||||
const env = makeEnv({ withGbrain: false, withConfig: false });
|
const env = makeEnv({ withGbrain: false, withConfig: false });
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user