Files
gbrain/test/postgres-execute-raw-direct.test.ts
553515c82c fix(cycle): enforce the extract-atoms drain deadline end to end (#2750)
The drain window was only checked BETWEEN batches: one batch (sequential
LLM calls per page) plus lock acquire/release and the backlog count ran
unbounded, so window=120s runs were observed overrunning to 282.5s.

The drain now derives a real-time deadline signal (window timeout + the
Minion job.signal) and threads it through everything the loop awaits:

- extract-atoms-drain: signal-driven loop; hung count/batch/lock-acquire
  are cancelled at the deadline; no post-window final count (remaining
  reports null instead of overrunning); external worker abort rethrows
  after the lock is released.
- extract-atoms phase: abortSignal forwarded to every gateway chat call,
  discovery/count/idempotency queries, and putPage writes, plus a
  cooperative between-item check (the bound on PGLite, whose query abort
  only abandons the waiter — the default engine keeps a working drain).
  Billable usage is recorded before the post-chat abort check; receipt +
  rollup bookkeeping runs on a fresh 5s grace signal so committed atoms
  never lose their cost trail; deadline-truncated runs don't count as
  completed rounds (deadline_aborted detail).
- db-lock: tryAcquireDbLock/withRefreshingLock accept a deadline signal;
  release routes through the direct session pool with a fresh grace
  timeout; deadline-bound callers skip the unbounded same-host takeover
  path (honest busy result; TTL stays the backstop).
- engines: putPage accepts an optional signal (Postgres cancels the
  in-flight statement; PGLite pre-checks); executeRawDirect short-circuits
  an already-fired signal before pool routing and bounds a stalled
  direct-pool acquisition.

Takeover of PR #2752: keeps its signal-threading design, but retains the
putPage write path (so the chunker_version default from #2988 applies —
the PR's bulk INSERT bypassed it), drops the PGLite hard refusal in favor
of cooperative abort, and reverts the unsanctioned transcript-suppression
scope change (_transcripts: []).

Fixes #2750

Co-authored-by: panda850819 <panda850819@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:41:34 -07:00

137 lines
6.1 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import { PostgresEngine } from '../src/core/postgres-engine.ts';
/**
* executeRawDirect routing decision (PR #1816 lock hot-path fix).
*
* The point of executeRawDirect is to send the Minion lock heartbeat
* (claim/renewLock) to the DIRECT session-mode pool (port 5432) instead of the
* transaction pooler (6543) that reaps connections mid-hold. That routing
* branch only fires against a real Supabase dual-pool, which CI doesn't have —
* so the decision itself (which connection gets the statement) went untested.
*
* This exercises the pure routing logic with a stubbed ConnectionManager and
* fake Sql handles, covering all three shapes:
*
* ┌─────────────────────────────┬──────────────┬───────────────┐
* │ engine shape │ dual-pool │ conn chosen │
* ├─────────────────────────────┼──────────────┼───────────────┤
* │ worker, not in tx │ active │ ddl() direct │
* │ tx clone (_sql = tx conn) │ active │ this.sql (tx) │ ← atomicity
* │ worker, not in tx │ inactive │ this.sql read │
* └─────────────────────────────┴──────────────┴───────────────┘
*/
type FakeSql = { unsafe: (sql: string, params?: unknown[]) => Promise<unknown[]> };
/** A fake postgres.js handle whose unsafe() tags rows with its label. */
function fakeSql(label: string): FakeSql {
return {
unsafe: async () => [{ via: label }],
};
}
/**
* Build a PostgresEngine with its connection internals stubbed so
* executeRawDirect can be driven without a live database.
*
* - readConn → the read pool (what `this.sql` returns when _sql === readPool)
* - directConn → what connectionManager.ddl() resolves to
* - sqlOverride → forces the `get sql()` getter (used to model a tx clone whose
* `this.sql` is the tx connection, distinct from peekReadPool()).
*/
function makeEngine(opts: {
dualPoolActive: boolean;
readConn: FakeSql;
directConn: FakeSql;
// When set, models a tx clone: _sql is the tx conn (!== peekReadPool()).
txConn?: FakeSql;
}): PostgresEngine {
const engine = new PostgresEngine();
const e = engine as unknown as Record<string, unknown>;
// _sql: tx conn for a clone, otherwise the read pool itself (the worker case
// where connect() does setReadPool(this._sql)).
e._sql = opts.txConn ?? opts.readConn;
// The `get sql()` getter on the prototype returns _sql when set, so we don't
// need to override it — _sql already drives it. For a tx clone _sql is txConn,
// so this.sql === txConn, exactly as Object.defineProperty does in transaction().
e.connectionManager = {
isDualPoolActive: () => opts.dualPoolActive,
peekReadPool: () => opts.readConn,
ddl: async () => opts.directConn,
};
return engine;
}
describe('PostgresEngine.executeRawDirect — routing decision (PR #1816)', () => {
test('dual-pool active + not in tx → routes to direct (ddl) pool', async () => {
const readConn = fakeSql('read');
const directConn = fakeSql('direct');
const engine = makeEngine({ dualPoolActive: true, readConn, directConn });
const rows = await engine.executeRawDirect<{ via: string }>('UPDATE minion_jobs SET x=1');
expect(rows[0].via).toBe('direct');
});
test('inside a transaction → honors the tx connection (never reroutes off it)', async () => {
const readConn = fakeSql('read');
const directConn = fakeSql('direct');
const txConn = fakeSql('tx');
// dual-pool is active, but _sql (tx) !== peekReadPool() (read) → inTransaction.
const engine = makeEngine({ dualPoolActive: true, readConn, directConn, txConn });
const rows = await engine.executeRawDirect<{ via: string }>('UPDATE minion_jobs SET x=1');
expect(rows[0].via).toBe('tx');
});
test('dual-pool inactive → falls back to the read pool', async () => {
const readConn = fakeSql('read');
const directConn = fakeSql('direct');
const engine = makeEngine({ dualPoolActive: false, readConn, directConn });
const rows = await engine.executeRawDirect<{ via: string }>('UPDATE minion_jobs SET x=1');
expect(rows[0].via).toBe('read');
});
test('already-aborted signal short-circuits with AbortError before routing the query', async () => {
let unsafeCalls = 0;
let ddlCalls = 0;
const readConn: FakeSql = { unsafe: async () => { unsafeCalls++; return []; } };
const directConn = fakeSql('direct');
const engine = makeEngine({ dualPoolActive: true, readConn, directConn });
const e = engine as unknown as { connectionManager: { ddl: () => Promise<FakeSql> } };
e.connectionManager.ddl = async () => { ddlCalls++; return directConn; };
const ac = new AbortController();
ac.abort();
await expect(
engine.executeRawDirect('UPDATE minion_jobs SET x=1', [], { signal: ac.signal }),
).rejects.toThrow(/abort/i);
// #2750: short-circuits BEFORE pool routing — no ddl(), no unsafe().
expect(ddlCalls).toBe(0);
expect(unsafeCalls).toBe(0);
});
test('#2750: signal bounds a stalled direct-pool acquisition before unsafe starts', async () => {
let unsafeCalls = 0;
const readConn: FakeSql = { unsafe: async () => { unsafeCalls++; return []; } };
const directConn = fakeSql('direct');
const engine = makeEngine({ dualPoolActive: true, readConn, directConn });
const e = engine as unknown as { connectionManager: { ddl: () => Promise<FakeSql> } };
e.connectionManager.ddl = () => new Promise<FakeSql>(() => {}); // pooler exhausted: never resolves
const started = Date.now();
await expect(engine.executeRawDirect(
'DELETE FROM gbrain_cycle_locks',
[],
{ signal: AbortSignal.timeout(10) },
)).rejects.toThrow(/abort/i);
expect(Date.now() - started).toBeLessThan(1_000);
expect(unsafeCalls).toBe(0);
});
});