mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-13 07:10:05 +00:00
fix(db): tryHardRead's replica leg doesn't fail over to the primary
MySQL's read() retries on the primary after a load-shed or transient replica error, so routing tryHardRead through it read the primary twice on that path. The replica leg now reads the replica alone.
This commit is contained in:
1 parent
a917a6243e
commit
250405df93
3 files changed
+37
-1
No files matched your search
@@ -119,6 +119,17 @@ export class AbstractDatabaseClient extends PuterClient {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The replica leg of `tryHardRead`. Override when `read()` falls back to
|
||||
* the primary by itself, or a replica failure reads the primary twice.
|
||||
*/
|
||||
protected replicaRead(
|
||||
query: string,
|
||||
params: unknown[],
|
||||
): Promise<Record<string, unknown>[]> {
|
||||
return this.read(query, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `read()`, but an empty or failed replica read falls back to the
|
||||
* primary, so replication lag can't hide a row that was just written. The
|
||||
@@ -134,7 +145,7 @@ export class AbstractDatabaseClient extends PuterClient {
|
||||
|
||||
const primary = settle(this.pread(query, params));
|
||||
try {
|
||||
const rows = await this.read(query, params);
|
||||
const rows = await this.replicaRead(query, params);
|
||||
if (rows.length > 0) return rows;
|
||||
} catch {
|
||||
// replica failed — fall through to primary
|
||||
|
||||
@@ -619,6 +619,21 @@ describe('MySQLDatabaseClient — tryHardRead', () => {
|
||||
{ from: 'primary' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('reads the primary once when the replica fails in a way read() fails over on', async () => {
|
||||
const replica = {
|
||||
execute: vi.fn().mockRejectedValue(codedError('dbBatchFailed')),
|
||||
};
|
||||
const primary = {
|
||||
execute: vi.fn().mockResolvedValue([[{ from: 'primary' }]]),
|
||||
};
|
||||
const client = makeClient({ replica, primary });
|
||||
|
||||
await expect(client.tryHardRead('SELECT 1')).resolves.toEqual([
|
||||
{ from: 'primary' },
|
||||
]);
|
||||
expect(primary.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MySQLDatabaseClient — migrations', () => {
|
||||
|
||||
@@ -205,6 +205,16 @@ export class MySQLDatabaseClient extends AbstractDatabaseClient {
|
||||
return this.configuration === Configuration.REPLICA;
|
||||
}
|
||||
|
||||
// No failover: `tryHardRead` already has a primary read in flight.
|
||||
protected override async replicaRead(
|
||||
query: string,
|
||||
params: unknown[],
|
||||
): Promise<Record<string, unknown>[]> {
|
||||
const result = await this.dbReplica.execute(query, params);
|
||||
if (!result) return [];
|
||||
return (result[0] as Record<string, unknown>[]) ?? [];
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Pool management
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user