mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
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>
137 lines
6.1 KiB
TypeScript
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);
|
|
});
|
|
});
|