Skip to content

Commit cfe73e3

Browse files
committed
fix(webapp,run-engine): read-your-writes primary fallback for replica-lag run-store reads
Run-store reads that gate a mutation or feed a public GET/realtime response were routed to the lagging read replica, so a just-written run/waitpoint/batch could spuriously miss under replica lag. On a replica miss re-read the owning primary (findRun/findWaitpoint/findBatchTaskRunByFriendlyId -> *OnPrimary / this.$.prisma / this._prisma); read the owning primary outright on the in-flow runAttemptSystem paths; and make the SDK batch GET 404 retryable. Sites: findEnvironmentFromRun, ApiWaitpointPresenter, BatchPresenter, session end-and-continue, api/realtime batch GET routes, realtime run/stream/session routes, sync trace loader, dashboardAgent run-commit, batchTriggerV3 dependent- attempt guard, runAttemptSystem resolveTaskRunContext + attemptFailed(forceRequeue).
1 parent cb8b981 commit cfe73e3

22 files changed

Lines changed: 356 additions & 265 deletions

apps/webapp/app/models/runtimeEnvironment.server.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -274,19 +274,17 @@ export async function findEnvironmentFromRun(
274274
): Promise<EnvironmentFromRun | null> {
275275
// Run-ops scalars (runTags/batchId/runtimeEnvironmentId) from the run store; the env half is
276276
// resolved via the control-plane resolver so the run-ops DB can split without a cross-DB join.
277-
const taskRun = await runStore.findRun(
278-
{
279-
id: runId,
280-
},
281-
{
282-
select: {
283-
runTags: true,
284-
batchId: true,
285-
runtimeEnvironmentId: true,
286-
},
287-
},
288-
tx ?? $replica
289-
);
277+
const select = {
278+
runTags: true,
279+
batchId: true,
280+
runtimeEnvironmentId: true,
281+
} as const;
282+
let taskRun = await runStore.findRun({ id: runId }, { select }, tx ?? $replica);
283+
if (!taskRun) {
284+
// Read-your-writes: a just-created run may not have replicated. Re-read the owning primary before
285+
// treating it as absent, so runMetadataUpdated doesn't drop a live run's final metadata + publish.
286+
taskRun = await runStore.findRun({ id: runId }, { select }, prisma);
287+
}
290288
if (!taskRun) {
291289
return null;
292290
}

apps/webapp/app/presenters/v3/ApiWaitpointPresenter.server.ts

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -42,29 +42,36 @@ export class ApiWaitpointPresenter extends BasePresenter {
4242
return this.trace("call", async (span) => {
4343
// The store routes by the waitpointId's residency (id shape) and reads the owning
4444
// store's replica. waitpointId is pre-decoded from the friendlyId via WaitpointId.toId.
45-
const waitpoint = await this.runStore.findWaitpoint({
46-
where: {
47-
id: waitpointId,
48-
environmentId: environment.id,
49-
},
50-
select: {
51-
id: true,
52-
friendlyId: true,
53-
type: true,
54-
status: true,
55-
idempotencyKey: true,
56-
userProvidedIdempotencyKey: true,
57-
idempotencyKeyExpiresAt: true,
58-
inactiveIdempotencyKey: true,
59-
output: true,
60-
outputType: true,
61-
outputIsError: true,
62-
completedAfter: true,
63-
completedAt: true,
64-
createdAt: true,
65-
tags: true,
66-
},
67-
});
45+
const where = {
46+
id: waitpointId,
47+
environmentId: environment.id,
48+
};
49+
const select = {
50+
id: true,
51+
friendlyId: true,
52+
type: true,
53+
status: true,
54+
idempotencyKey: true,
55+
userProvidedIdempotencyKey: true,
56+
idempotencyKeyExpiresAt: true,
57+
inactiveIdempotencyKey: true,
58+
output: true,
59+
outputType: true,
60+
outputIsError: true,
61+
completedAfter: true,
62+
completedAt: true,
63+
createdAt: true,
64+
tags: true,
65+
} as const;
66+
67+
let waitpoint = await this.runStore.findWaitpoint({ where, select });
68+
69+
// Read-your-writes on a public GET: a just-minted token may not be on the owning store's
70+
// replica yet, so a replica miss would 404 a live token. Re-read the owning primary before
71+
// concluding it doesn't exist (mirrors the metadata GET loader + the complete/callback paths).
72+
if (!waitpoint) {
73+
waitpoint = await this.runStore.findWaitpointOnPrimary({ where, select });
74+
}
6875

6976
if (!waitpoint) {
7077
logger.error(`WaitpointPresenter: Waitpoint not found`, {

apps/webapp/app/presenters/v3/BatchPresenter.server.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,25 @@ export class BatchPresenter extends BasePresenter {
4747
// The BatchTaskRun (run-ops) is read through the run store, which routes by residency. The
4848
// runtimeEnvironment (control-plane) is resolved separately because the cross-seam FK is
4949
// dropped, so the batch row cannot single-SQL join to control-plane RuntimeEnvironment.
50-
const batch = await this.runStore.findBatchTaskRunByFriendlyId(
50+
let batch = await this.runStore.findBatchTaskRunByFriendlyId(
5151
batchId,
5252
environmentId,
5353
{ include: BATCH_INCLUDE },
5454
this._replica
5555
);
5656

57+
// Read-your-writes: findBatchTaskRunByFriendlyId defaults to (and here reads) the replica, so a
58+
// batch created within the replica's apply window returns null under lag. Re-read from the owning
59+
// primary on a miss so a live batch's detail page never spuriously 404s ("Batch not found").
60+
if (!batch) {
61+
batch = await this.runStore.findBatchTaskRunByFriendlyId(
62+
batchId,
63+
environmentId,
64+
{ include: BATCH_INCLUDE },
65+
this._prisma
66+
);
67+
}
68+
5769
if (!batch) {
5870
throw new Error("Batch not found");
5971
}

apps/webapp/app/routes/api.v1.batches.$batchId.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ export const loader = createLoaderApiRoute(
1212
params: ParamsSchema,
1313
allowJWT: true,
1414
corsStrategy: "all",
15+
// A just-created batch may not yet have replicated to the read replica this client-less
16+
// findBatchTaskRunByFriendlyId lookup routes to; return a retryable 404 so the SDK retries through
17+
// replica lag rather than stranding a live batch on a permanent 404 (mirrors the run-get routes,
18+
// e.g. api.v3.runs.$runId).
19+
shouldRetryNotFound: true,
1520
findResource: (params, auth) => {
1621
return runStore.findBatchTaskRunByFriendlyId(params.batchId, auth.environment.id, {
1722
include: { errors: true },

apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,23 @@ const { action, loader } = createActionApiRoute(
3333
},
3434
async ({ authentication, body, params }) => {
3535
try {
36-
const run = await runStore.findRun(
37-
{
38-
friendlyId: params.runFriendlyId,
39-
runtimeEnvironmentId: authentication.environment.id,
36+
const where = {
37+
friendlyId: params.runFriendlyId,
38+
runtimeEnvironmentId: authentication.environment.id,
39+
};
40+
const args = {
41+
select: {
42+
id: true,
43+
friendlyId: true,
44+
realtimeStreamsVersion: true,
45+
streamBasinName: true,
4046
},
41-
{
42-
select: {
43-
id: true,
44-
friendlyId: true,
45-
realtimeStreamsVersion: true,
46-
streamBasinName: true,
47-
},
48-
},
49-
$replica
50-
);
47+
};
48+
// Replica lag can null out a live run; a spurious 404 fails the .wait() registration on a run
49+
// that exists. Re-read the owning primary on a replica miss.
50+
const run =
51+
(await runStore.findRun(where, args, $replica)) ??
52+
(await runStore.findRunOnPrimary(where, args));
5153

5254
if (!run) {
5355
return json({ error: "Run not found" }, { status: 404 });

apps/webapp/app/routes/api.v1.runs.$runFriendlyId.session-streams.wait.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,22 @@ const { action, loader } = createActionApiRoute(
3939
},
4040
async ({ authentication, body, params }) => {
4141
try {
42-
const run = await runStore.findRun(
43-
{
44-
friendlyId: params.runFriendlyId,
45-
runtimeEnvironmentId: authentication.environment.id,
42+
const where = {
43+
friendlyId: params.runFriendlyId,
44+
runtimeEnvironmentId: authentication.environment.id,
45+
};
46+
const args = {
47+
select: {
48+
id: true,
49+
friendlyId: true,
50+
realtimeStreamsVersion: true,
4651
},
47-
{
48-
select: {
49-
id: true,
50-
friendlyId: true,
51-
realtimeStreamsVersion: true,
52-
},
53-
},
54-
$replica
55-
);
52+
};
53+
// Replica lag can null out a live run; a spurious 404 fails the .wait() registration on a run
54+
// that exists. Re-read the owning primary on a replica miss.
55+
const run =
56+
(await runStore.findRun(where, args, $replica)) ??
57+
(await runStore.findRunOnPrimary(where, args));
5658

5759
if (!run) {
5860
return json({ error: "Run not found" }, { status: 404 });

apps/webapp/app/routes/api.v1.sessions.$session.end-and-continue.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,27 @@ const { action, loader } = createActionApiRoute(
7575
// SDK exposes via `ctx.run.id`). Internally `Session.currentRunId`
7676
// stores the TaskRun.id cuid, so resolve before handing to the
7777
// optimistic-claim service.
78-
const callingRun = await runStore.findRun(
78+
let callingRun = await runStore.findRun(
7979
{
8080
friendlyId: body.callingRunId,
8181
runtimeEnvironmentId: authentication.environment.id,
8282
},
8383
{ select: { id: true } },
8484
$replica
8585
);
86+
if (!callingRun) {
87+
// Replica lag: `callingRunId` is the agent's own live run (it is executing this request), so it
88+
// exists on the owning primary even when the read replica has not caught up. Re-read the primary
89+
// before 404ing — otherwise a lagging replica turns a legitimate handoff into a spurious
90+
// "callingRunId not found in this environment".
91+
callingRun = await runStore.findRunOnPrimary(
92+
{
93+
friendlyId: body.callingRunId,
94+
runtimeEnvironmentId: authentication.environment.id,
95+
},
96+
{ select: { id: true } }
97+
);
98+
}
8699
if (!callingRun) {
87100
return json({ error: "callingRunId not found in this environment" }, { status: 404 });
88101
}

apps/webapp/app/routes/api.v2.batches.$batchId.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ export const loader = createLoaderApiRoute(
1212
params: ParamsSchema,
1313
allowJWT: true,
1414
corsStrategy: "all",
15+
// A just-created batch may not yet have replicated to the read replica this client-less
16+
// findBatchTaskRunByFriendlyId lookup routes to; return a retryable 404 so the SDK retries through
17+
// replica lag rather than stranding a live batch on a permanent 404 (mirrors the run-get routes,
18+
// e.g. api.v3.runs.$runId).
19+
shouldRetryNotFound: true,
1520
findResource: (params, auth) => {
1621
return runStore.findBatchTaskRunByFriendlyId(params.batchId, auth.environment.id, {
1722
include: { errors: true },

apps/webapp/app/routes/realtime.v1.batches.$batchId.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export const loader = createLoaderApiRoute(
1313
params: ParamsSchema,
1414
allowJWT: true,
1515
corsStrategy: "all",
16+
// A just-created batch may not yet have replicated to the read replica this client-less
17+
// findBatchTaskRunByFriendlyId lookup routes to; return a retryable 404 so the SDK retries through
18+
// replica lag rather than stranding a live batch on a permanent 404 (mirrors the run-get routes,
19+
// e.g. api.v3.runs.$runId).
20+
shouldRetryNotFound: true,
1621
findResource: (params, auth) => {
1722
return runStore.findBatchTaskRunByFriendlyId(params.batchId, auth.environment.id);
1823
},

apps/webapp/app/routes/realtime.v1.runs.$runId.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,24 @@ export const loader = createLoaderApiRoute(
1515
allowJWT: true,
1616
corsStrategy: "all",
1717
findResource: async (params, authentication) => {
18-
return runStore.findRun(
19-
{
20-
friendlyId: params.runId,
21-
runtimeEnvironmentId: authentication.environment.id,
22-
},
23-
{
24-
include: {
25-
batch: {
26-
select: {
27-
friendlyId: true,
28-
},
18+
const where = {
19+
friendlyId: params.runId,
20+
runtimeEnvironmentId: authentication.environment.id,
21+
};
22+
const args = {
23+
include: {
24+
batch: {
25+
select: {
26+
friendlyId: true,
2927
},
3028
},
3129
},
32-
$replica
33-
);
30+
};
31+
// Replica lag can null out a run that already exists on the owning primary. A spurious 404
32+
// here permanently fails the client's realtime subscription (the SSE client treats 404 as
33+
// "stream gone" — nonRetryableStatuses). Re-read the primary on a replica miss.
34+
const run = await runStore.findRun(where, args, $replica);
35+
return run ?? runStore.findRunOnPrimary(where, args);
3436
},
3537
authorization: {
3638
action: "read",

0 commit comments

Comments
 (0)