mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* fix(postgres): module-singleton ownership — borrower disconnect no longer nulls the cycle's connection (#1404/#1471/#1619) gbrain dream on Postgres failed every DB phase with "No database connection: connect() has not been called": a short-lived borrower probe engine (lint/doctor config-lift, no poolSize) called db.disconnect() in its own disconnect(), nulling the shared module singleton the long-lived cycle owner was still using. The module `sql` is only ever nulled by db.disconnect() (postgres.js auto-reconnects its own pool), so the failure was always a borrower-disconnect, never an idle-pooler drop. Fix: db.connect() returns whether THIS call created the singleton (atomic — no await between the null-check and the sql=postgres() assignment), PostgresEngine stores it as _ownsModuleSingleton, and disconnect() only calls db.disconnect() when it owns the connection. Borrowers no-op. Hardening: db.disconnect() snapshots +nulls sql before awaiting end(); reconnect() shares one in-flight _reconnectPromise. Tests: new postgres-engine-singleton-ownership.test.ts; expanded DB-gated e2e matrix (owner/borrower, creation-not-role, symmetric CLI-exit, owner-reconnect- with-live-borrower); module-style getter asymmetry; #1570 shared-recovery regression updated to assert the fixed contract. Co-Authored-By: nullhex-io <noreply@github.com> Co-Authored-By: joelwp <noreply@github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.42.15.0) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: nullhex-io <noreply@github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68 lines
2.9 KiB
TypeScript
68 lines
2.9 KiB
TypeScript
/**
|
|
* issue #1678 — the `PostgresEngine.sql` getter must not fall through to the
|
|
* never-connected module singleton when an INSTANCE pool's _sql went null
|
|
* (mid-process disconnect, or a reaped pooler socket). Pre-fix it threw the
|
|
* misleading "connect() has not been called"; post-fix it throws a tailored
|
|
* RETRYABLE error so withRetry+reconnect rebuilds the pool and recovers.
|
|
*
|
|
* Pure: pokes the private fields and reads the synchronous getter; no real DB.
|
|
*/
|
|
|
|
import { describe, it, expect } from 'bun:test';
|
|
import { PostgresEngine } from '../src/core/postgres-engine.ts';
|
|
import { isRetryableConnError } from '../src/core/retry-matcher.ts';
|
|
|
|
describe('PostgresEngine.sql getter self-heal (issue #1678)', () => {
|
|
it('instance-pool + null _sql throws a RETRYABLE error naming the reaped pool', () => {
|
|
const e = new PostgresEngine();
|
|
(e as unknown as { _connectionStyle: string })._connectionStyle = 'instance';
|
|
(e as unknown as { _sql: unknown })._sql = null;
|
|
|
|
let thrown: unknown;
|
|
try {
|
|
// accessing the getter triggers the throw
|
|
void e.sql;
|
|
} catch (err) {
|
|
thrown = err;
|
|
}
|
|
expect(thrown).toBeDefined();
|
|
// Must be classified retryable so the lock/batch retry paths reconnect.
|
|
expect(isRetryableConnError(thrown)).toBe(true);
|
|
// Must NOT be the misleading legacy message.
|
|
const msg = (thrown as Error).message;
|
|
expect(msg).toContain('instance connection pool');
|
|
expect(msg).not.toContain('connect() has not been called');
|
|
});
|
|
|
|
it('a live instance _sql is returned directly (no throw)', () => {
|
|
const e = new PostgresEngine();
|
|
const fakeSql = { tag: 'live-pool' };
|
|
(e as unknown as { _sql: unknown })._sql = fakeSql;
|
|
expect(e.sql as unknown).toBe(fakeSql);
|
|
});
|
|
|
|
// #1471: the getter self-heal is INTENTIONALLY gated to instance style. A
|
|
// module-style engine with a null _sql must fall through to the loud legacy
|
|
// db.getConnection() error, NOT the instance "reaped pool" self-heal. Post
|
|
// ownership-fix, the module singleton never goes null via a borrower
|
|
// disconnect, so a null module singleton signals a genuine bug we want loud
|
|
// (never connected, or a real owner-side teardown) rather than papered over.
|
|
it('module-style + null _sql falls through to the loud legacy error, not the instance self-heal', () => {
|
|
const e = new PostgresEngine();
|
|
(e as unknown as { _connectionStyle: string })._connectionStyle = 'module';
|
|
(e as unknown as { _sql: unknown })._sql = null;
|
|
|
|
let thrown: unknown;
|
|
try {
|
|
void e.sql; // delegates to db.getConnection(), which throws (no module connect)
|
|
} catch (err) {
|
|
thrown = err;
|
|
}
|
|
expect(thrown).toBeDefined();
|
|
const msg = (thrown as Error).message;
|
|
// The intentional asymmetry: module style keeps the legacy loud message.
|
|
expect(msg).toContain('No database connection');
|
|
expect(msg).not.toContain('instance connection pool');
|
|
});
|
|
});
|