Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 50 additions & 9 deletions packages/drivers/driver-sql/src/sql-driver-connect-bound.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,26 @@
// wait is 30s per query on the request path. SqlDriver now bounds it by default,
// while leaving a host's own choice alone.

import { describe, it, expect, afterEach } from 'vitest';
import { describe, it, expect, afterEach, vi } from 'vitest';
import net from 'node:net';
import { SqlDriver } from './sql-driver.js';

/** A listener that accepts sockets and then says nothing, ever. */
/**
* A listener that accepts sockets and then says nothing, ever. `accepted`
* settles once the first socket is held — the moment the connection attempt
* is a TCP connection that will never see a handshake.
*/
function blackHole() {
const held: net.Socket[] = [];
const server = net.createServer((sock) => { sock.on('error', () => {}); held.push(sock); });
let firstAccepted!: () => void;
const accepted = new Promise<void>((resolve) => { firstAccepted = resolve; });
const server = net.createServer((sock) => {
sock.on('error', () => {});
held.push(sock);
firstAccepted();
});
return {
accepted,
listen: () => new Promise<number>((resolve) => {
server.listen(0, '127.0.0.1', () => resolve((server.address() as net.AddressInfo).port));
}),
Expand Down Expand Up @@ -199,19 +210,46 @@ describe('SqlDriver — connection-attempt bound (framework#3769)', () => {
it('actually bounds a query against a black-holing endpoint', async () => {
const bh = blackHole();
const port = await bh.listen();
// The endpoint is real: a loopback listener that holds the accepted socket
// and never answers, so only the driver's own bound can end the attempt.
// The CLOCK is faked (setTimeout/clearTimeout only — `net` I/O and the
// microtask queue stay real), because the default bound is a 10s wall-clock
// timer and waiting it out was the whole 10s this file used to cost. Fake
// time still proves the bound: pg arms `connectionTimeoutMillis` and tarn
// arms `createTimeoutMillis` on the faked `setTimeout`, so whichever
// fires — or none, for an unbounded connect — shows in how far the fake
// clock has to move before the query settles.
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] });
try {
// The default is 10s; unbounded would be knex/tarn's 30s.
const d = make({
client: 'pg',
connection: `postgres://u:p@127.0.0.1:${port}/d`,
pool: { min: 0, max: 2 },
});
const started = Date.now();
const err = await (d as any).knex.raw('SELECT 1').then(
() => { throw new Error('query resolved but should have failed'); },
(e: Error) => e,
let settled = undefined as { ok: true } | { ok: false; error: Error } | undefined;
(d as any).knex.raw('SELECT 1').then(
() => { settled = { ok: true }; },
(error: Error) => { settled = { ok: false, error }; },
);
const elapsed = Date.now() - started;

// Real I/O: do not move the clock until the server holds the socket, so
// the attempt being timed is a connection that was accepted and then
// went silent — not one that never started.
await bh.accepted;

// Move the fake clock in steps until the query settles, never past 20s.
// Each step also lets a real macrotask run, so the socket teardown and
// the rejection it causes land before the next step is judged.
let elapsed = 0;
while (settled === undefined && elapsed < 20_000) {
await vi.advanceTimersByTimeAsync(500);
await new Promise<void>((resolve) => setImmediate(resolve));
elapsed += 500;
}
expect(settled, 'still pending after 20s of fake time — the connect is unbounded').toBeDefined();
expect(settled?.ok, 'query resolved but should have failed').toBe(false);
const err = (settled as { ok: false; error: Error }).error;

// The pg-level timeout fires first (10s), so the message names the
// connection attempt rather than blaming pool sizing. Guarding against
Expand All @@ -222,9 +260,12 @@ describe('SqlDriver — connection-attempt bound (framework#3769)', () => {
expect(elapsed).toBeGreaterThan(8_000);
expect(elapsed).toBeLessThan(20_000);
} finally {
// Real timers back BEFORE afterEach disconnects the driver and the
// listener closes: teardown must not depend on a clock nobody advances.
vi.useRealTimers();
bh.close();
}
}, 40_000);
}, 15_000);

it('does not disturb a working sqlite connection', async () => {
const d = make({ client: 'better-sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true });
Expand Down
Loading