Skip to content
Open
Show file tree
Hide file tree
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
14 changes: 10 additions & 4 deletions src/js/internal/sql/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,10 @@ abstract class BaseSQLAdapter<PooledConnection extends BasePooledConnection, Con

constructor(connectionInfo: Bun.SQL.__internal.DefinedPostgresOrMySQLOptions) {
this.connectionInfo = connectionInfo;
// Slots are filled one at a time in connect()'s pool-start loop, and
// createPooledConnection can synchronously run user code (for example a
// function-valued `password`) that re-enters methods scanning this array,
// so every scan must tolerate unassigned holes.
this.connections = new Array(connectionInfo.max);
}

Expand Down Expand Up @@ -1128,7 +1132,7 @@ abstract class BaseSQLAdapter<PooledConnection extends BasePooledConnection, Con
const pollSize = this.connections.length;
for (let i = 0; i < pollSize; i++) {
const connection = this.connections[i];
if (connection.state === PooledConnectionState.connected) {
if (connection?.state === PooledConnectionState.connected) {
return true;
}
}
Expand All @@ -1143,7 +1147,7 @@ abstract class BaseSQLAdapter<PooledConnection extends BasePooledConnection, Con
const pollSize = this.connections.length;
for (let i = 0; i < pollSize; i++) {
const connection = this.connections[i];
if (connection.state === PooledConnectionState.connected) {
if (connection?.state === PooledConnectionState.connected) {
connection.connection?.flush();
}
}
Expand All @@ -1169,7 +1173,7 @@ abstract class BaseSQLAdapter<PooledConnection extends BasePooledConnection, Con
const pollSize = this.connections.length;
for (let i = 0; i < pollSize; i++) {
const connection = this.connections[i];
switch (connection.state) {
switch (connection?.state) {
case PooledConnectionState.pending:
case PooledConnectionState.connected: {
// cancelRetry only returns true while a connect retry is parked
Expand Down Expand Up @@ -1272,7 +1276,9 @@ abstract class BaseSQLAdapter<PooledConnection extends BasePooledConnection, Con
for (let i = 0; i < pollSize; i++) {
const connection = this.connections[i];
// we need a new connection and we have some connections that can retry
if (connection.state === PooledConnectionState.closed) {
// (an unassigned hole is a connection still being created, so it
// lands in the "pending" branch below)
if (connection?.state === PooledConnectionState.closed) {
if (connection.retry()) {
// lets wait for connection to be released
if (!retry_in_progress) {
Expand Down
47 changes: 47 additions & 0 deletions test/js/sql/sql-close-pending-connection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,50 @@ for (const [name, scheme, closedCode] of drivers) {
}
});
}

// https://github.com/oven-sh/bun/issues/32198
//
// The pool's connection array is allocated as `new Array(max)` and filled one
// slot at a time when the pool starts. A function-valued `password` option
// runs synchronously during that fill, so pool methods re-entered from it
// used to dereference unassigned slots and throw a raw TypeError.
test("pool scans tolerate unassigned connection slots during pool start", async () => {
const { port, server, sockets } = await neverAnsweringServer();
let passwordCalls = 0;
const errors: unknown[] = [];
const sql = new SQL({
adapter: "postgres",
hostname: "127.0.0.1",
port,
username: "u",
database: "d",
max: 2,
connectionTimeout: 0,
password: () => {
passwordCalls++;
try {
sql.flush();
} catch (e) {
errors.push(e);
}
try {
sql.connect().catch(() => {});
} catch (e) {
errors.push(e);
}
return "";
},
});
try {
sql.connect().catch(() => {});
// the pool-start fill loop runs synchronously inside connect(), invoking
// password() once per pool slot
expect(passwordCalls).toBe(2);
expect(errors).toEqual([]);
} finally {
// force an immediate close even with waiters queued
await sql.close({ timeout: "0" });
for (const socket of sockets) socket.destroy();
server.close();
}
});
Loading