Skip to content

Commit d243c41

Browse files
committed
fix(webapp): read the primary when a waitpoint token misses both read replicas
A token completed immediately after it was minted could miss on the read replicas and return a spurious 404, so the authoritative completion never ran. Fall back to the owning-store primary before giving up.
1 parent 409b057 commit d243c41

2 files changed

Lines changed: 81 additions & 2 deletions

File tree

apps/webapp/app/runEngine/concerns/resolveWaitpointThroughReadThrough.server.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { PrismaReplicaClient } from "~/db.server";
22
import {
33
$replica as defaultLegacyReplica,
4+
runOpsLegacyPrisma as defaultLegacyPrimary,
5+
runOpsNewPrisma as defaultNewPrimary,
46
runOpsNewReplica as defaultNewClient,
57
runOpsSplitReadEnabled as defaultSplitReadEnabled,
68
} from "~/db.server";
@@ -9,6 +11,8 @@ import { readThroughRun } from "~/v3/runOpsMigration/readThrough.server";
911
type ResolveWaitpointDeps = {
1012
newClient?: PrismaReplicaClient;
1113
legacyReplica?: PrismaReplicaClient;
14+
newPrimary?: PrismaReplicaClient;
15+
legacyPrimary?: PrismaReplicaClient;
1216
splitEnabled?: boolean;
1317
isPastRetention?: (id: string) => boolean;
1418
};
@@ -18,12 +22,16 @@ type ResolveWaitpointDeps = {
1822
export type ResolveWaitpointReadThroughDefaults = {
1923
newClient: PrismaReplicaClient;
2024
legacyReplica: PrismaReplicaClient;
25+
newPrimary: PrismaReplicaClient;
26+
legacyPrimary: PrismaReplicaClient;
2127
splitEnabled: boolean;
2228
};
2329

2430
const productionDefaults: ResolveWaitpointReadThroughDefaults = {
2531
newClient: defaultNewClient,
2632
legacyReplica: defaultLegacyReplica,
33+
newPrimary: defaultNewPrimary as unknown as PrismaReplicaClient,
34+
legacyPrimary: defaultLegacyPrimary as unknown as PrismaReplicaClient,
2735
splitEnabled: defaultSplitReadEnabled,
2836
};
2937

@@ -36,18 +44,42 @@ export async function resolveWaitpointThroughReadThrough<T>(opts: {
3644
}): Promise<T | null> {
3745
const defaults = opts.defaults ?? productionDefaults;
3846

47+
const splitEnabled = opts.deps?.splitEnabled ?? defaults.splitEnabled;
48+
3949
const result = await readThroughRun({
4050
runId: opts.waitpointId,
4151
environmentId: opts.environmentId,
4252
readNew: (client) => opts.read(client),
4353
readLegacy: (replica) => opts.read(replica),
4454
deps: {
45-
splitEnabled: opts.deps?.splitEnabled ?? defaults.splitEnabled,
55+
splitEnabled,
4656
newClient: opts.deps?.newClient ?? defaults.newClient,
4757
legacyReplica: opts.deps?.legacyReplica ?? defaults.legacyReplica,
4858
isPastRetention: opts.deps?.isPastRetention,
4959
},
5060
});
5161

52-
return result.source === "new" || result.source === "legacy-replica" ? result.value : null;
62+
if (result.source === "new" || result.source === "legacy-replica") {
63+
return result.value;
64+
}
65+
// past-retention is an intentional not-found: the token is gone, don't hit the primary.
66+
if (result.source === "past-retention") {
67+
return null;
68+
}
69+
70+
// Read-your-writes fallback: readThroughRun is replica-only, so a token completed immediately after
71+
// it was minted can miss on the replicas (not yet applied) and 404 a valid token - and the
72+
// authoritative completeWaitpoint never runs. Re-read from the owning-store PRIMARY (new then legacy)
73+
// before giving up. Bounded to this replica-miss; a genuinely-absent token still returns null.
74+
const fromNewPrimary = await opts.read(opts.deps?.newPrimary ?? defaults.newPrimary);
75+
if (fromNewPrimary != null) {
76+
return fromNewPrimary;
77+
}
78+
if (splitEnabled) {
79+
const fromLegacyPrimary = await opts.read(opts.deps?.legacyPrimary ?? defaults.legacyPrimary);
80+
if (fromLegacyPrimary != null) {
81+
return fromLegacyPrimary;
82+
}
83+
}
84+
return null;
5385
}

apps/webapp/test/resolveWaitpointThroughReadThrough.readthrough.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,48 @@ describe("resolveWaitpointThroughReadThrough (hetero PG14 legacy + dedicated run
151151
}
152152
);
153153

154+
// Read-your-writes: a token completed immediately after mint may not be on the replicas yet. The
155+
// replica-only read-through misses, and without a primary fallback we 404 a valid token (the
156+
// authoritative completeWaitpoint never runs). The primary fallback must find it.
157+
heteroRunOpsPostgresTest(
158+
"a token missing on both replicas is found on the primary (no spurious 404)",
159+
async ({ prisma17, prisma14 }) => {
160+
const id = generateLegacyCuid();
161+
const { project, environment } = await seedOrgProjectEnv(prisma14, "primary_fallback");
162+
const seeded = await seedWaitpoint(prisma14, id, {
163+
id: environment.id,
164+
projectId: project.id,
165+
});
166+
167+
// Both replicas miss (not yet replicated): point them at the dedicated DB, which lacks this
168+
// legacy cuid waitpoint. The LEGACY primary (control-plane writer, prisma14) has it.
169+
const newClient = recording(prisma17);
170+
const legacyReplica = recording(prisma17);
171+
const newPrimary = recording(prisma17);
172+
const legacyPrimary = recording(prisma14);
173+
174+
const result = await resolveWaitpointThroughReadThrough({
175+
waitpointId: id,
176+
environmentId: environment.id,
177+
read: read(id, environment.id),
178+
deps: {
179+
splitEnabled: true,
180+
newClient: newClient.handle,
181+
legacyReplica: legacyReplica.handle,
182+
newPrimary: newPrimary.handle,
183+
legacyPrimary: legacyPrimary.handle,
184+
},
185+
});
186+
187+
expect(result).not.toBeNull();
188+
expect(result!.id).toBe(seeded.id);
189+
// Replicas probed and missed; the fallback read the primary.
190+
expect(newClient.calls.length).toBe(1);
191+
expect(legacyReplica.calls.length).toBe(1);
192+
expect(legacyPrimary.calls.length).toBe(1);
193+
}
194+
);
195+
154196
heteroRunOpsPostgresTest(
155197
"bare caller (no deps) resolves a NEW-resident waitpoint via the safe run-ops defaults",
156198
async ({ prisma17, prisma14 }) => {
@@ -170,6 +212,9 @@ describe("resolveWaitpointThroughReadThrough (hetero PG14 legacy + dedicated run
170212
defaults: {
171213
newClient: prisma14 as unknown as PrismaReplicaClient,
172214
legacyReplica: prisma14 as unknown as PrismaReplicaClient,
215+
// Primaries also miss the NEW-resident waitpoint, so the miss stays a miss.
216+
newPrimary: prisma14 as unknown as PrismaReplicaClient,
217+
legacyPrimary: prisma14 as unknown as PrismaReplicaClient,
173218
splitEnabled: true,
174219
},
175220
});
@@ -183,6 +228,8 @@ describe("resolveWaitpointThroughReadThrough (hetero PG14 legacy + dedicated run
183228
defaults: {
184229
newClient: prisma17 as unknown as PrismaReplicaClient,
185230
legacyReplica: prisma14 as unknown as PrismaReplicaClient,
231+
newPrimary: prisma17 as unknown as PrismaReplicaClient,
232+
legacyPrimary: prisma14 as unknown as PrismaReplicaClient,
186233
splitEnabled: true,
187234
},
188235
});

0 commit comments

Comments
 (0)