Skip to content
Merged
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
19 changes: 18 additions & 1 deletion lib/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,26 @@ const pool = mysql.createPool({
charset: "utf8mb4",
});

// MySQL closes a connection after wait_timeout seconds without a query (60 on
// prod), and the pool only finds out when the next query on it fails with
// errno 4031 ("disconnected by the server because of inactivity"). The
// server closed the connection before that query arrived, so the query never
// ran and one retry on a fresh connection is safe, writes included. Any other
// error, a connection lost mid-query among them, is passed on unchanged.
const SERVER_CLOSED_IDLE = 4031;

export async function retryIfIdleClosed(run) {
try {
return await run();
} catch (err) {
if (err?.errno !== SERVER_CLOSED_IDLE) throw err;
return run();
}
}

export default {
query: function (query, params) {
return pool.query(query, params)
return retryIfIdleClosed(() => pool.query(query, params))
.then((result) => result[0]); // [rows, fields]
},
// The refresh-* bins call this to close the pool so the process can exit
Expand Down
41 changes: 41 additions & 0 deletions test/db-retry.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { retryIfIdleClosed } from '../lib/db.js';

const idleClosed = Object.assign(new Error('disconnected by the server'), { errno: 4031 });

test('a query on a connection MySQL closed for idling runs once more', async () => {
let calls = 0;
const rows = await retryIfIdleClosed(async () => {
calls++;
if (calls === 1) throw idleClosed;
return 'rows';
});
assert.equal(rows, 'rows');
assert.equal(calls, 2);
});

test('any other error is not retried', async () => {
let calls = 0;
const lost = Object.assign(new Error('lost'), { code: 'PROTOCOL_CONNECTION_LOST' });
await assert.rejects(
retryIfIdleClosed(async () => {
calls++;
throw lost;
}),
lost
);
assert.equal(calls, 1);
});

test('it retries only once', async () => {
let calls = 0;
await assert.rejects(
retryIfIdleClosed(async () => {
calls++;
throw idleClosed;
}),
idleClosed
);
assert.equal(calls, 2);
});
Loading