84f3e1b39c
## What Routes the webapp write path through the run-ops split seam: trigger/batch minting, idempotency-key resolution, and the run-lifecycle services now determine residency and dispatch writes to the correct store. - **Trigger & batch** (`runEngine/services/triggerTask.server.ts`, `batchTrigger.server.ts`, `createBatch.server.ts`, `streamBatchItems.server.ts`, `v3/services/batchTriggerV3.server.ts`): mint ids with the run-ops-aware minting and route creation/streaming through the store; batch children inherit the parent's residency. - **Idempotency** (`runEngine/concerns/idempotencyKeys.server.ts` + new `idempotencyResidency.server.ts`): idempotency-key lookup/dedup is residency-aware so a keyed retrigger resolves against the store that owns the original run. - **Run lifecycle services** (`createCheckpoint`, `createTaskRunAttempt`, `enqueueDelayedRun`, `expireEnqueuedRun`, `finalizeTaskRun`, `resumeBatchRun`, `cancelDevSessionRuns`, `executeTasksWaitingForDeploy`, `triggerFailedTask`): resolve their target run through the store rather than a fixed client. - **Reads that fan out from writes** (`runsRepository` + `clickhouseRunsRepository`, `BulkActionV2` + batch read-through, realtime `sessions`/`runReader`, alerts `deliverAlert`/`performTaskRunAlerts`): route through the read-through resolver. - `9535ae63d` — resolves the parent run through an injectable run store in `TriggerFailedTaskService`. - `bf8f7c881` — drops the "known-migrated" concept from write-path and read repos; residency is id-shape only. - `515b897ea` — self-defaults `resolveWaitpointThroughReadThrough` to the safe run-ops clients. ## Why PR6 of the run-ops split stack. This is the write-path counterpart to the read foundation in the previous PRs: with it in place, both reads and writes route through the seam. Additive when the split is disabled (id-shape resolution collapses to the control-plane client); behavior-changing on the minting, idempotency, and lifecycle paths when enabled. ## Tests Large new/expanded vitest suite under `apps/webapp/test/` and colocated service tests: trigger-task and batch-trigger store routing, residency inheritance, idempotency dedup residency + legacy-authority, bulk-action read routing, cancel-dev-session routing, alerts store routing, runs-repository read-through, realtime session/run-reader read-through and stream-registration routing, and the waitpoint read-through default. Testcontainers-backed; no mocks. ## Notes Draft, **stacked on #4117** (`runops/pr05-webapp-foundation`). Review that first; this diff is against it. Server-change / changeset note to be added at stack-assembly time. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
122 lines
3.4 KiB
TypeScript
122 lines
3.4 KiB
TypeScript
import { parseNaturalLanguageDuration } from "@trigger.dev/core/v3/isomorphic";
|
|
import { logger } from "~/services/logger.server";
|
|
import { workerQueue } from "~/services/worker.server";
|
|
import { commonWorker } from "../commonWorker.server";
|
|
import { BaseService } from "./baseService.server";
|
|
import { enqueueRun } from "./enqueueRun.server";
|
|
import { ExpireEnqueuedRunService } from "./expireEnqueuedRun.server";
|
|
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
|
|
import { isV3Disabled } from "../engineDeprecation.server";
|
|
|
|
export class EnqueueDelayedRunService extends BaseService {
|
|
public static async enqueue(runId: string, runAt?: Date) {
|
|
await commonWorker.enqueue({
|
|
job: "v3.enqueueDelayedRun",
|
|
payload: { runId },
|
|
availableAt: runAt,
|
|
id: `v3.enqueueDelayed:${runId}`,
|
|
});
|
|
}
|
|
|
|
public static async reschedule(runId: string, runAt?: Date) {
|
|
// We have to do this for now because it's possible that the workerQueue
|
|
// was used when the run was first delayed, and EnqueueDelayedRunService.reschedule
|
|
// is called from RescheduleTaskRunService, which allows the runAt to be changed
|
|
// so if we don't dequeue the old job, we might end up with multiple jobs
|
|
await workerQueue.dequeue(`v3.enqueueDelayedRun.${runId}`);
|
|
|
|
await commonWorker.enqueue({
|
|
job: "v3.enqueueDelayedRun",
|
|
payload: { runId },
|
|
availableAt: runAt,
|
|
id: `v3.enqueueDelayed:${runId}`,
|
|
});
|
|
}
|
|
|
|
public async call(runId: string) {
|
|
const run = await this.runStore.findRun(
|
|
{
|
|
id: runId,
|
|
},
|
|
{
|
|
include: {
|
|
dependency: {
|
|
include: {
|
|
dependentBatchRun: {
|
|
include: {
|
|
dependentTaskAttempt: {
|
|
include: {
|
|
taskRun: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
dependentAttempt: {
|
|
include: {
|
|
taskRun: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
this._prisma
|
|
);
|
|
|
|
if (!run) {
|
|
logger.debug("Could not find delayed run to enqueue", {
|
|
runId,
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
// v3 (engine V1) shutdown: don't enqueue delayed V1 runs into MarQS. v4 is unaffected.
|
|
if (isV3Disabled() && run.engine === "V1") {
|
|
logger.debug("[EnqueueDelayedRunService] Skipping enqueue for shut-down v3 run", { runId });
|
|
return;
|
|
}
|
|
|
|
const env = await controlPlaneResolver.resolveAuthenticatedEnv(run.runtimeEnvironmentId);
|
|
|
|
if (!env) {
|
|
logger.debug("EnqueueDelayedRunService: environment not found", { runId });
|
|
return;
|
|
}
|
|
|
|
if (run.status !== "DELAYED") {
|
|
logger.debug("Delayed run cannot be enqueued because it's not in DELAYED status", {
|
|
run,
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
await this._prisma.taskRun.update({
|
|
where: {
|
|
id: run.id,
|
|
},
|
|
data: {
|
|
status: "PENDING",
|
|
queuedAt: new Date(),
|
|
},
|
|
});
|
|
|
|
if (run.ttl) {
|
|
const expireAt = parseNaturalLanguageDuration(run.ttl);
|
|
|
|
if (expireAt) {
|
|
await ExpireEnqueuedRunService.enqueue(run.id, expireAt);
|
|
}
|
|
}
|
|
|
|
await enqueueRun({
|
|
env,
|
|
run: run,
|
|
dependentRun:
|
|
run.dependency?.dependentAttempt?.taskRun ??
|
|
run.dependency?.dependentBatchRun?.dependentTaskAttempt?.taskRun,
|
|
});
|
|
}
|
|
}
|