Compare commits

...

3 Commits

Author SHA1 Message Date
Claude Bot
232db80457 fix(test): remove exitCode assertion since bunRun throws on failure
bunRun() already throws if the process exits non-zero, so it only
returns { stdout, stderr } on success. The exitCode property doesn't
exist in the return value.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-23 08:51:36 +00:00
Claude Bot
793fdf15ba fix: revert flushQueue() changes, keep only updateReferenceType fix
Address PR review feedback:

- Revert all flushQueue() catch {} replacements back to queue.advance()
  in handleAuth, caching_sha2 success, checkIfPreparedStatementIsDone,
  and handlePreparedStatement ERROR paths. These are called during
  readAndProcessData, and onData's defer already calls
  registerAutoFlusher() which handles flushing. Using flushQueue()
  directly would break batching by forcing immediate socket writes.

- The real fix is the updateReferenceType() call in enqueueRequest(),
  which re-refs the poll_ref when a new query is enqueued on an idle
  connection whose poll_ref was previously unrefed.

- Fix test fixture to support TLS by reading CA_PATH from env
- Pass CA_PATH in test invocation for MySQL TLS container
- Add exitCode assertion to sequential queries test

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-23 08:45:05 +00:00
Claude Bot
8188893044 fix(sql): prevent MySQL sequential queries from hanging on TCP connections
Two related bugs caused sequential `await sql.unsafe()` calls to hang
forever on remote MySQL connections (with real TCP latency):

1. `enqueueRequest()` did not call `updateReferenceType()`, so the
   poll_ref remained unrefed after a connection went idle. Without the
   poll ref, the event loop would not wait for the socket response.

2. Several code paths called `queue.advance()` without flushing the
   write buffer, so queued queries would never be sent to the server.
   Replace these with `flushQueue()` which calls both `advance()` and
   `flushData()`. The affected paths are: handleAuth OK, handleAuth
   caching_sha2 success, checkIfPreparedStatementIsDone, and
   handlePreparedStatement ERROR.

Closes #27362
Closes #26235
Closes #24130
Closes #27102

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-23 04:26:59 +00:00
3 changed files with 46 additions and 4 deletions

View File

@@ -155,6 +155,7 @@ pub fn enqueueRequest(this: *@This(), item: *JSMySQLQuery) void {
this.#connection.enqueueRequest(item);
this.resetConnectionTimeout();
this.registerAutoFlusher();
this.updateReferenceType();
}
pub fn close(this: *@This()) void {

View File

@@ -0,0 +1,34 @@
// Fixture for issue #27362: Sequential await sql.unsafe() calls should not hang.
// Without the fix, the process hangs on the 3rd-4th sequential call because the
// poll_ref is not re-refed when a new query is enqueued on an idle connection.
const tls = process.env.CA_PATH ? { ca: Bun.file(process.env.CA_PATH) } : undefined;
const sql = new Bun.SQL({
url: process.env.MYSQL_URL,
tls,
max: 1,
idleTimeout: 100,
maxLifetime: 100,
connectionTimeout: 100,
});
// Warmup / establish connection
await sql`SELECT 1`;
// Sequential queries - these would hang without the fix
const r1 = await sql.unsafe("SELECT 1 as v");
console.log("query1:", r1[0].v);
const r2 = await sql.unsafe("SELECT 2 as v");
console.log("query2:", r2[0].v);
const r3 = await sql.unsafe("SELECT 3 as v");
console.log("query3:", r3[0].v);
const r4 = await sql.unsafe("SELECT 4 as v");
console.log("query4:", r4[0].v);
const r5 = await sql.unsafe("SELECT 5 as v");
console.log("query5:", r5[0].v);
console.log("all queries completed");
// process should exit with code 0

View File

@@ -63,6 +63,15 @@ if (isDockerEnabled()) {
});
expect(stderr).toBe("");
});
test("sequential queries should not hang (#27362)", async () => {
const { stdout, stderr } = bunRun(path.join(import.meta.dir, "sql-mysql-sequential-fixture.ts"), {
...bunEnv,
MYSQL_URL: getOptions().url,
CA_PATH: image.name === "MySQL with TLS" ? path.join(import.meta.dir, "mysql-tls", "ssl", "ca.pem") : "",
});
expect(stderr).toBe("");
expect(stdout).toContain("all queries completed");
});
test("should return lastInsertRowid and affectedRows", async () => {
await using db = new SQL({ ...getOptions(), max: 1, idleTimeout: 5 });
using sql = await db.reserve();
@@ -483,9 +492,7 @@ if (isDockerEnabled()) {
test("Binary", async () => {
const random_name = ("t_" + Bun.randomUUIDv7("hex").replaceAll("-", "")).toLowerCase();
await sql`CREATE TEMPORARY TABLE ${sql(random_name)} (a binary(1), b varbinary(1), c blob)`;
const values = [
{ a: Buffer.from([1]), b: Buffer.from([2]), c: Buffer.from([3]) },
];
const values = [{ a: Buffer.from([1]), b: Buffer.from([2]), c: Buffer.from([3]) }];
await sql`INSERT INTO ${sql(random_name)} ${sql(values)}`;
const results = await sql`select * from ${sql(random_name)}`;
// return buffers
@@ -497,7 +504,7 @@ if (isDockerEnabled()) {
expect(results2[0].a).toEqual(Buffer.from([1]));
expect(results2[0].b).toEqual(Buffer.from([2]));
expect(results2[0].c).toEqual(Buffer.from([3]));
})
});
test("bulk insert nested sql()", async () => {
await using sql = new SQL({ ...getOptions(), max: 1 });