mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-04 04:53:39 +00:00
perf(database): defer primary fallback for user lookups (#4359)
This commit is contained in:
1 parent
91554a137e
commit
2dfd93687f
4 files changed
+36
-10
No files matched your search
@@ -108,6 +108,25 @@ describe('AbstractDatabaseClient — unimplemented surface', () => {
|
||||
});
|
||||
|
||||
describe('AbstractDatabaseClient — replica-aware reads', () => {
|
||||
it('avoids a primary read when the replica has rows in sequential mode', async () => {
|
||||
const client = new FixtureDatabaseClient([{ id: 1 }], [{ id: 2 }]);
|
||||
expect(
|
||||
await client.tryHardRead('SELECT 1', [], { race: false }),
|
||||
).toEqual([{ id: 1 }]);
|
||||
expect(client.preads).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([[], new Error('replica unavailable')])(
|
||||
'falls back once in sequential mode',
|
||||
async (replica) => {
|
||||
const client = new FixtureDatabaseClient(replica, [{ id: 2 }]);
|
||||
expect(
|
||||
await client.tryHardRead('SELECT 1', [], { race: false }),
|
||||
).toEqual([{ id: 2 }]);
|
||||
expect(client.preads).toEqual(['SELECT 1']);
|
||||
},
|
||||
);
|
||||
|
||||
it('returns replica rows without waiting on the primary', async () => {
|
||||
const client = new FixtureDatabaseClient([{ id: 1 }], [{ id: 2 }]);
|
||||
await expect(client.tryHardRead('SELECT 1')).resolves.toEqual([
|
||||
|
||||
@@ -138,25 +138,27 @@ export class AbstractDatabaseClient extends PuterClient {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* primary read starts alongside the replica one rather than after it.
|
||||
* Without a replica this is a single `read()`.
|
||||
* primary, so replication lag can't hide a row that was just written. By
|
||||
* default the primary read starts alongside the replica one; `race: false`
|
||||
* reads the primary only after the replica returns empty or fails. Without
|
||||
* a replica this is a single `read()`.
|
||||
*/
|
||||
@Span('db.tryHardRead', (query: string) => ({ 'db.statement': query }))
|
||||
async tryHardRead(
|
||||
query: string,
|
||||
params: unknown[] = [],
|
||||
{ race = true }: { race?: boolean } = {},
|
||||
): Promise<Record<string, unknown>[]> {
|
||||
if (!this.hasReadReplica()) return this.read(query, params);
|
||||
|
||||
const primary = settle(this.pread(query, params));
|
||||
const primary = race ? settle(this.pread(query, params)) : null;
|
||||
try {
|
||||
const rows = await this.replicaRead(query, params);
|
||||
if (rows.length > 0) return rows;
|
||||
} catch {
|
||||
// replica failed — fall through to primary
|
||||
}
|
||||
const result = await primary;
|
||||
const result = await (primary ?? settle(this.pread(query, params)));
|
||||
if ('error' in result) throw result.error;
|
||||
return result.value;
|
||||
}
|
||||
|
||||
@@ -217,7 +217,8 @@ export class MySQLDatabaseClient extends AbstractDatabaseClient {
|
||||
return this.configuration === Configuration.REPLICA;
|
||||
}
|
||||
|
||||
// No failover: `tryHardRead` already has a primary read in flight.
|
||||
// No failover: `tryHardRead` falls back to the primary on an empty result
|
||||
// or error (raced alongside by default, after the replica with `race: false`).
|
||||
protected override async replicaRead(
|
||||
query: string,
|
||||
params: unknown[],
|
||||
|
||||
@@ -316,7 +316,9 @@ export class UserStore extends PuterStore {
|
||||
const rows = (
|
||||
primary
|
||||
? await this.clients.db.pread(sql, chunk)
|
||||
: await this.clients.db.tryHardRead(sql, chunk)
|
||||
: await this.clients.db.tryHardRead(sql, chunk, {
|
||||
race: false,
|
||||
})
|
||||
) as Array<Record<string, unknown>>;
|
||||
for (const row of rows) users.push(this.#normalizeRow(row));
|
||||
}
|
||||
@@ -430,8 +432,8 @@ export class UserStore extends PuterStore {
|
||||
|
||||
// Replication-aware read: on `force` (or a tombstoned key), go
|
||||
// straight to the primary (`pread`) to bypass replica lag for hot
|
||||
// reads (e.g., immediately after a signup). Otherwise `tryHardRead`
|
||||
// parallels primary + replica and prefers whichever returns rows.
|
||||
// reads (e.g., immediately after a signup). Otherwise the primary is
|
||||
// only read when the replica returns no match or fails.
|
||||
// `id`, `uuid` and `username` are UNIQUE, so at most one row matches and
|
||||
// the optimizer drops the ordering. `email` is not — multiple rows may
|
||||
// legitimately hold the same address while unconfirmed, so without an
|
||||
@@ -447,7 +449,9 @@ export class UserStore extends PuterStore {
|
||||
async (primary) => {
|
||||
const rows = primary
|
||||
? await this.clients.db.pread(sql, [value])
|
||||
: await this.clients.db.tryHardRead(sql, [value]);
|
||||
: await this.clients.db.tryHardRead(sql, [value], {
|
||||
race: false,
|
||||
});
|
||||
const row = rows[0];
|
||||
if (!row) return null;
|
||||
if (
|
||||
|
||||
Reference in new issue
Block a user