Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,9 @@ const EnvironmentSchema = z
// claim TTL), how long a waiter blocks before timing out, and the
// waiter poll interval.
TRIGGER_MOLLIFIER_CLAIM_TTL_SECONDS: z.coerce.number().int().positive().default(30),
// Pipeline floor: the claim never shrinks below this even for a short customer key TTL, so it
// can't expire mid-pipeline and let a loser re-claim (cross-DB duplicate under the split).
TRIGGER_MOLLIFIER_CLAIM_MIN_TTL_SECONDS: z.coerce.number().int().positive().default(5),
TRIGGER_MOLLIFIER_CLAIM_WAIT_MS: z.coerce.number().int().positive().default(5_000),
TRIGGER_MOLLIFIER_CLAIM_POLL_MS: z.coerce.number().int().positive().default(25),

Expand Down
24 changes: 11 additions & 13 deletions apps/webapp/app/models/runtimeEnvironment.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,19 +274,17 @@ export async function findEnvironmentFromRun(
): Promise<EnvironmentFromRun | null> {
// Run-ops scalars (runTags/batchId/runtimeEnvironmentId) from the run store; the env half is
// resolved via the control-plane resolver so the run-ops DB can split without a cross-DB join.
const taskRun = await runStore.findRun(
{
id: runId,
},
{
select: {
runTags: true,
batchId: true,
runtimeEnvironmentId: true,
},
},
tx ?? $replica
);
const select = {
runTags: true,
batchId: true,
runtimeEnvironmentId: true,
} as const;
let taskRun = await runStore.findRun({ id: runId }, { select }, tx ?? $replica);
if (!taskRun) {
// Read-your-writes: a just-created run may not have replicated. Re-read the owning primary before
// treating it as absent, so runMetadataUpdated doesn't drop a live run's final metadata + publish.
taskRun = await runStore.findRun({ id: runId }, { select }, prisma);
}
if (!taskRun) {
return null;
}
Expand Down
53 changes: 30 additions & 23 deletions apps/webapp/app/presenters/v3/ApiWaitpointPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,29 +42,36 @@ export class ApiWaitpointPresenter extends BasePresenter {
return this.trace("call", async (span) => {
// The store routes by the waitpointId's residency (id shape) and reads the owning
// store's replica. waitpointId is pre-decoded from the friendlyId via WaitpointId.toId.
const waitpoint = await this.runStore.findWaitpoint({
where: {
id: waitpointId,
environmentId: environment.id,
},
select: {
id: true,
friendlyId: true,
type: true,
status: true,
idempotencyKey: true,
userProvidedIdempotencyKey: true,
idempotencyKeyExpiresAt: true,
inactiveIdempotencyKey: true,
output: true,
outputType: true,
outputIsError: true,
completedAfter: true,
completedAt: true,
createdAt: true,
tags: true,
},
});
const where = {
id: waitpointId,
environmentId: environment.id,
};
const select = {
id: true,
friendlyId: true,
type: true,
status: true,
idempotencyKey: true,
userProvidedIdempotencyKey: true,
idempotencyKeyExpiresAt: true,
inactiveIdempotencyKey: true,
output: true,
outputType: true,
outputIsError: true,
completedAfter: true,
completedAt: true,
createdAt: true,
tags: true,
} as const;

let waitpoint = await this.runStore.findWaitpoint({ where, select });

// Read-your-writes on a public GET: a just-minted token may not be on the owning store's
// replica yet, so a replica miss would 404 a live token. Re-read the owning primary before
// concluding it doesn't exist (mirrors the metadata GET loader + the complete/callback paths).
if (!waitpoint) {
waitpoint = await this.runStore.findWaitpointOnPrimary({ where, select });
}

if (!waitpoint) {
logger.error(`WaitpointPresenter: Waitpoint not found`, {
Expand Down
14 changes: 13 additions & 1 deletion apps/webapp/app/presenters/v3/BatchPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,25 @@ export class BatchPresenter extends BasePresenter {
// The BatchTaskRun (run-ops) is read through the run store, which routes by residency. The
// runtimeEnvironment (control-plane) is resolved separately because the cross-seam FK is
// dropped, so the batch row cannot single-SQL join to control-plane RuntimeEnvironment.
const batch = await this.runStore.findBatchTaskRunByFriendlyId(
let batch = await this.runStore.findBatchTaskRunByFriendlyId(
batchId,
environmentId,
{ include: BATCH_INCLUDE },
this._replica
);

// Read-your-writes: findBatchTaskRunByFriendlyId defaults to (and here reads) the replica, so a
// batch created within the replica's apply window returns null under lag. Re-read from the owning
// primary on a miss so a live batch's detail page never spuriously 404s ("Batch not found").
if (!batch) {
batch = await this.runStore.findBatchTaskRunByFriendlyId(
batchId,
environmentId,
{ include: BATCH_INCLUDE },
this._prisma
);
}

if (!batch) {
throw new Error("Batch not found");
}
Expand Down
5 changes: 5 additions & 0 deletions apps/webapp/app/routes/api.v1.batches.$batchId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export const loader = createLoaderApiRoute(
params: ParamsSchema,
allowJWT: true,
corsStrategy: "all",
// A just-created batch may not yet have replicated to the read replica this client-less
// findBatchTaskRunByFriendlyId lookup routes to; return a retryable 404 so the SDK retries through
// replica lag rather than stranding a live batch on a permanent 404 (mirrors the run-get routes,
// e.g. api.v3.runs.$runId).
shouldRetryNotFound: true,
findResource: (params, auth) => {
return runStore.findBatchTaskRunByFriendlyId(params.batchId, auth.environment.id, {
include: { errors: true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,21 +33,23 @@ const { action, loader } = createActionApiRoute(
},
async ({ authentication, body, params }) => {
try {
const run = await runStore.findRun(
{
friendlyId: params.runFriendlyId,
runtimeEnvironmentId: authentication.environment.id,
const where = {
friendlyId: params.runFriendlyId,
runtimeEnvironmentId: authentication.environment.id,
};
const args = {
select: {
id: true,
friendlyId: true,
realtimeStreamsVersion: true,
streamBasinName: true,
},
{
select: {
id: true,
friendlyId: true,
realtimeStreamsVersion: true,
streamBasinName: true,
},
},
$replica
);
};
// Replica lag can null out a live run; a spurious 404 fails the .wait() registration on a run
// that exists. Re-read the owning primary on a replica miss.
const run =
(await runStore.findRun(where, args, $replica)) ??
(await runStore.findRunOnPrimary(where, args));

if (!run) {
return json({ error: "Run not found" }, { status: 404 });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,22 @@ const { action, loader } = createActionApiRoute(
},
async ({ authentication, body, params }) => {
try {
const run = await runStore.findRun(
{
friendlyId: params.runFriendlyId,
runtimeEnvironmentId: authentication.environment.id,
const where = {
friendlyId: params.runFriendlyId,
runtimeEnvironmentId: authentication.environment.id,
};
const args = {
select: {
id: true,
friendlyId: true,
realtimeStreamsVersion: true,
},
{
select: {
id: true,
friendlyId: true,
realtimeStreamsVersion: true,
},
},
$replica
);
};
// Replica lag can null out a live run; a spurious 404 fails the .wait() registration on a run
// that exists. Re-read the owning primary on a replica miss.
const run =
(await runStore.findRun(where, args, $replica)) ??
(await runStore.findRunOnPrimary(where, args));

if (!run) {
return json({ error: "Run not found" }, { status: 404 });
Expand Down
13 changes: 13 additions & 0 deletions apps/webapp/app/routes/api.v1.runs.$runId.metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
);
}

// Read-your-writes: a run drained from the buffer to the primary but not yet replicated misses
// both the replica read and the buffer. Re-read the owning primary before 404ing.
const primaryRun = await runStore.findRunOnPrimary(
{ friendlyId: parsed.data.runId, runtimeEnvironmentId: env.id },
{ select: { metadata: true, metadataType: true } }
);
if (primaryRun) {
return json(
{ metadata: primaryRun.metadata, metadataType: primaryRun.metadataType },
{ status: 200 }
);
}

return json({ error: "Run not found" }, { status: 404 });
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,27 @@ const { action, loader } = createActionApiRoute(
// SDK exposes via `ctx.run.id`). Internally `Session.currentRunId`
// stores the TaskRun.id cuid, so resolve before handing to the
// optimistic-claim service.
const callingRun = await runStore.findRun(
let callingRun = await runStore.findRun(
{
friendlyId: body.callingRunId,
runtimeEnvironmentId: authentication.environment.id,
},
{ select: { id: true } },
$replica
);
if (!callingRun) {
// Replica lag: `callingRunId` is the agent's own live run (it is executing this request), so it
// exists on the owning primary even when the read replica has not caught up. Re-read the primary
// before 404ing — otherwise a lagging replica turns a legitimate handoff into a spurious
// "callingRunId not found in this environment".
callingRun = await runStore.findRunOnPrimary(
{
friendlyId: body.callingRunId,
runtimeEnvironmentId: authentication.environment.id,
},
{ select: { id: true } }
);
}
if (!callingRun) {
return json({ error: "callingRunId not found in this environment" }, { status: 404 });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,22 @@ export async function action({ request, params }: ActionFunctionArgs) {
// Resolve wherever the waitpoint resides. The store routes by the waitpoint id's residency
// (id-shape) and probes both run-ops DBs, so a token on either store resolves; the env is
// resolved below from the row via the control-plane resolver.
const waitpoint = await runStore.findWaitpoint({
let waitpoint = await runStore.findWaitpoint({
where: {
id: waitpointId,
},
select: { id: true, status: true, environmentId: true },
});

if (!waitpoint) {
// Read-your-writes: a token whose callback fires right after mint may not have replicated
// yet. Re-read the owning primary before 404ing (mirrors complete.ts's primary fallback).
waitpoint = await runStore.findWaitpointOnPrimary({
where: { id: waitpointId },
select: { id: true, status: true, environmentId: true },
});
}

if (!waitpoint) {
return json({ error: "Waitpoint not found" }, { status: 404 });
}
Expand Down
5 changes: 5 additions & 0 deletions apps/webapp/app/routes/api.v2.batches.$batchId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export const loader = createLoaderApiRoute(
params: ParamsSchema,
allowJWT: true,
corsStrategy: "all",
// A just-created batch may not yet have replicated to the read replica this client-less
// findBatchTaskRunByFriendlyId lookup routes to; return a retryable 404 so the SDK retries through
// replica lag rather than stranding a live batch on a permanent 404 (mirrors the run-get routes,
// e.g. api.v3.runs.$runId).
shouldRetryNotFound: true,
findResource: (params, auth) => {
return runStore.findBatchTaskRunByFriendlyId(params.batchId, auth.environment.id, {
include: { errors: true },
Expand Down
12 changes: 8 additions & 4 deletions apps/webapp/app/routes/realtime.v1.batches.$batchId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { z } from "zod";
import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server";
import { resolveRealtimeStreamClient } from "~/services/realtime/resolveRealtimeStreamClient.server";
import { anyResource, createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server";
import { runStore } from "~/v3/runStore.server";
import { resolveBatchTaskRunForRealtime } from "~/v3/realtime/resolveBatchForRealtime.server";

const ParamsSchema = z.object({
batchId: z.string(),
Expand All @@ -13,9 +13,13 @@ export const loader = createLoaderApiRoute(
params: ParamsSchema,
allowJWT: true,
corsStrategy: "all",
findResource: (params, auth) => {
return runStore.findBatchTaskRunByFriendlyId(params.batchId, auth.environment.id);
},
// A just-created batch may not yet have replicated to the read replica the client-less lookup uses.
// shouldRetryNotFound stamps a retryable 404 for the zodfetch GET; the realtime resolver ALSO
// re-reads the owning primary on a replica miss, so the Electric ShapeStream consumer (which ignores
// x-should-retry) doesn't strand a live batch on a permanent 404. Mirrors the run-get routes.
shouldRetryNotFound: true,
Comment thread
claude[bot] marked this conversation as resolved.
findResource: (params, auth) =>
resolveBatchTaskRunForRealtime(params.batchId, auth.environment.id),
authorization: {
action: "read",
// See sibling note in api.v1.batches.$batchId.ts — `{type: "runs"}`
Expand Down
28 changes: 15 additions & 13 deletions apps/webapp/app/routes/realtime.v1.runs.$runId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,24 @@ export const loader = createLoaderApiRoute(
allowJWT: true,
corsStrategy: "all",
findResource: async (params, authentication) => {
return runStore.findRun(
{
friendlyId: params.runId,
runtimeEnvironmentId: authentication.environment.id,
},
{
include: {
batch: {
select: {
friendlyId: true,
},
const where = {
friendlyId: params.runId,
runtimeEnvironmentId: authentication.environment.id,
};
const args = {
include: {
batch: {
select: {
friendlyId: true,
},
},
},
$replica
);
};
// Replica lag can null out a run that already exists on the owning primary. A spurious 404
// here permanently fails the client's realtime subscription (the SSE client treats 404 as
// "stream gone" — nonRetryableStatuses). Re-read the primary on a replica miss.
const run = await runStore.findRun(where, args, $replica);
return run ?? runStore.findRunOnPrimary(where, args);
},
authorization: {
action: "read",
Expand Down
Loading