diff --git a/src/backend/clients/database/DatabaseClient.ts b/src/backend/clients/database/DatabaseClient.ts index 15072816d..5da056acf 100644 --- a/src/backend/clients/database/DatabaseClient.ts +++ b/src/backend/clients/database/DatabaseClient.ts @@ -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[]> { + 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 diff --git a/src/backend/clients/database/MySQLDatabaseClient.test.ts b/src/backend/clients/database/MySQLDatabaseClient.test.ts index 0a243115e..ae2608121 100644 --- a/src/backend/clients/database/MySQLDatabaseClient.test.ts +++ b/src/backend/clients/database/MySQLDatabaseClient.test.ts @@ -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', () => { diff --git a/src/backend/clients/database/MySQLDatabaseClient.ts b/src/backend/clients/database/MySQLDatabaseClient.ts index cc7ddd0ca..6a5b9d934 100644 --- a/src/backend/clients/database/MySQLDatabaseClient.ts +++ b/src/backend/clients/database/MySQLDatabaseClient.ts @@ -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[]> { + const result = await this.dbReplica.execute(query, params); + if (!result) return []; + return (result[0] as Record[]) ?? []; + } + // ------------------------------------------------------------------ // Pool management // ------------------------------------------------------------------