Files
triggerdotdev--trigger.dev/apps/webapp/app/v3/services/createTaskRunAttempt.server.ts
devin-ai-integration[bot] 4b28080ed4 feat: add isReplay to run context (#3454)
## Summary

Adds `isReplay` boolean to the run context (`ctx.run.isReplay`),
following the same pattern as the existing `isTest`. The value is
derived from the existing `replayedFromTaskRunFriendlyId` database
field, so no schema migration is needed.

##  Checklist

- [x] I have followed every step in the [contributing
guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md)
- [x] The PR title follows the convention.
- [x] I ran and tested the code works

---

## Testing

- Verified `@trigger.dev/core` builds successfully
- Verified `webapp` typechecks successfully
- All new fields use `default(false)` for backwards compatibility

---

## Changelog

- Added `isReplay` to `TaskRun` and `V3TaskRun` schemas in `common.ts`
- Added `RUN_IS_REPLAY` semantic attribute and wired it in `taskContext`
- Propagated `isReplay` through the dequeue system, run attempt system,
and all execution context construction paths (V1 + V2)
- Added `isReplay` to `DequeuedMessage` and
`TaskRunExecutionLazyAttemptPayload` schemas
- Added patch changeset for `@trigger.dev/core`
- Updated docs: added `isReplay` to context reference, added "Detecting
replays" section to replaying page

---

💯

Link to Devin session:
https://app.devin.ai/sessions/1d6f1b3cc39a4623b72d05bf00f2d70c

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: nick <55853254+nicktrn@users.noreply.github.com>
2026-04-28 11:57:44 +02:00

287 lines
8.8 KiB
TypeScript

import { parsePacket, V3TaskRunExecution } from "@trigger.dev/core/v3";
import { TaskRun, TaskRunAttempt } from "@trigger.dev/database";
import { MAX_TASK_RUN_ATTEMPTS } from "~/consts";
import { $transaction, prisma, PrismaClientOrTransaction } from "~/db.server";
import { findQueueInEnvironment } from "~/models/taskQueue.server";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { reportInvocationUsage } from "~/services/platform.v3.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { machinePresetFromConfig, machinePresetFromRun } from "../machinePresets.server";
import { FINAL_RUN_STATUSES } from "../taskStatus";
import { BaseService, ServiceValidationError } from "./baseService.server";
import { CrashTaskRunService } from "./crashTaskRun.server";
import { ExpireEnqueuedRunService } from "./expireEnqueuedRun.server";
export class CreateTaskRunAttemptService extends BaseService {
public async call({
runId,
authenticatedEnv,
setToExecuting = true,
startAtZero = false,
}: {
runId: string;
authenticatedEnv?: AuthenticatedEnvironment;
setToExecuting?: boolean;
startAtZero?: boolean;
}): Promise<{
execution: V3TaskRunExecution;
run: TaskRun;
attempt: TaskRunAttempt;
}> {
const environment =
authenticatedEnv ?? (await getAuthenticatedEnvironmentFromRun(runId, this._prisma));
if (!environment) {
throw new ServiceValidationError("Environment not found", 404);
}
const isFriendlyId = runId.startsWith("run_");
return await this.traceWithEnv("call()", environment, async (span) => {
if (isFriendlyId) {
span.setAttribute("taskRunFriendlyId", runId);
} else {
span.setAttribute("taskRunId", runId);
}
const taskRun = await this._prisma.taskRun.findFirst({
where: {
id: !isFriendlyId ? runId : undefined,
friendlyId: isFriendlyId ? runId : undefined,
runtimeEnvironmentId: environment.id,
},
include: {
attempts: {
take: 1,
orderBy: {
number: "desc",
},
},
lockedBy: {
include: {
worker: {
select: {
id: true,
version: true,
sdkVersion: true,
cliVersion: true,
supportsLazyAttempts: true,
},
},
},
},
batchItems: {
include: {
batchTaskRun: {
select: {
friendlyId: true,
},
},
},
},
},
});
logger.debug("Creating a task run attempt", { taskRun });
if (!taskRun) {
throw new ServiceValidationError("Task run not found", 404);
}
span.setAttribute("taskRunId", taskRun.id);
span.setAttribute("taskRunFriendlyId", taskRun.friendlyId);
span.setAttribute("taskRunStatus", taskRun.status);
if (taskRun.status === "CANCELED") {
throw new ServiceValidationError("Task run is cancelled", 400);
}
// If the run is finalized, it's pointless to create another attempt
if (FINAL_RUN_STATUSES.includes(taskRun.status)) {
throw new ServiceValidationError("Task run is already finished", 400);
}
const lockedBy = taskRun.lockedBy;
if (!lockedBy) {
throw new ServiceValidationError("Task run is not locked", 400);
}
const queue = await findQueueInEnvironment(
taskRun.queue,
environment.id,
lockedBy.id,
lockedBy
);
if (!queue) {
throw new ServiceValidationError("Queue not found", 404);
}
const nextAttemptNumber = taskRun.attempts[0]
? taskRun.attempts[0].number + 1
: startAtZero
? 0
: 1;
if (nextAttemptNumber > MAX_TASK_RUN_ATTEMPTS) {
const service = new CrashTaskRunService(this._prisma);
await service.call(taskRun.id, {
reason: lockedBy.worker.supportsLazyAttempts
? "Max attempts reached."
: "Max attempts reached. Please upgrade your CLI and SDK.",
});
throw new ServiceValidationError("Max attempts reached", 400);
}
const taskRunAttempt = await $transaction(this._prisma, "create attempt", async (tx) => {
const taskRunAttempt = await tx.taskRunAttempt.create({
data: {
number: nextAttemptNumber,
friendlyId: generateFriendlyId("attempt"),
taskRunId: taskRun.id,
startedAt: new Date(),
backgroundWorkerId: lockedBy.worker.id,
backgroundWorkerTaskId: lockedBy.id,
status: setToExecuting ? "EXECUTING" : "PENDING",
queueId: queue.id,
runtimeEnvironmentId: environment.id,
},
});
await tx.taskRun.update({
where: {
id: taskRun.id,
},
data: {
status: setToExecuting ? "EXECUTING" : undefined,
executedAt: taskRun.executedAt ?? new Date(),
attemptNumber: nextAttemptNumber,
},
});
if (taskRun.ttl) {
await ExpireEnqueuedRunService.ack(taskRun.id, tx);
}
return taskRunAttempt;
});
if (!taskRunAttempt) {
logger.error("Failed to create task run attempt", { runId: taskRun.id, nextAttemptNumber });
throw new ServiceValidationError("Failed to create task run attempt", 500);
}
if (taskRunAttempt.number === 1 && taskRun.baseCostInCents > 0) {
await reportInvocationUsage(environment.organizationId, taskRun.baseCostInCents, {
runId: taskRun.id,
});
}
const machinePreset =
machinePresetFromRun(taskRun) ?? machinePresetFromConfig(lockedBy.machineConfig ?? {});
const metadata = await parsePacket({
data: taskRun.metadata ?? undefined,
dataType: taskRun.metadataType,
});
const execution: V3TaskRunExecution = {
task: {
id: lockedBy.slug,
filePath: lockedBy.filePath,
exportName: lockedBy.exportName ?? "@deprecated",
},
attempt: {
id: taskRunAttempt.friendlyId,
number: taskRunAttempt.number,
startedAt: taskRunAttempt.startedAt ?? taskRunAttempt.createdAt,
backgroundWorkerId: lockedBy.worker.id,
backgroundWorkerTaskId: lockedBy.id,
status: "EXECUTING" as const,
},
run: {
id: taskRun.friendlyId,
payload: taskRun.payload,
payloadType: taskRun.payloadType,
context: taskRun.context,
createdAt: taskRun.createdAt,
tags: taskRun.runTags ?? [],
isTest: taskRun.isTest,
isReplay: !!taskRun.replayedFromTaskRunFriendlyId,
idempotencyKey: taskRun.idempotencyKey ?? undefined,
startedAt: taskRun.startedAt ?? taskRun.createdAt,
durationMs: taskRun.usageDurationMs,
costInCents: taskRun.costInCents,
baseCostInCents: taskRun.baseCostInCents,
maxAttempts: taskRun.maxAttempts ?? undefined,
version: lockedBy.worker.version,
metadata,
maxDuration: taskRun.maxDurationInSeconds ?? undefined,
},
queue: {
id: queue.friendlyId,
name: queue.name,
},
environment: {
id: environment.id,
slug: environment.slug,
type: environment.type,
},
organization: {
id: environment.organization.id,
slug: environment.organization.slug,
name: environment.organization.title,
},
project: {
id: environment.project.id,
ref: environment.project.externalRef,
slug: environment.project.slug,
name: environment.project.name,
},
batch:
taskRun.batchItems[0] && taskRun.batchItems[0].batchTaskRun
? { id: taskRun.batchItems[0].batchTaskRun.friendlyId }
: undefined,
machine: machinePreset,
};
return {
execution,
run: taskRun,
attempt: taskRunAttempt,
};
});
}
}
async function getAuthenticatedEnvironmentFromRun(
friendlyId: string,
prismaClient?: PrismaClientOrTransaction
) {
const isFriendlyId = friendlyId.startsWith("run_");
const taskRun = await (prismaClient ?? prisma).taskRun.findFirst({
where: {
id: !isFriendlyId ? friendlyId : undefined,
friendlyId: isFriendlyId ? friendlyId : undefined,
},
include: {
runtimeEnvironment: {
include: {
organization: true,
project: true,
},
},
},
});
if (!taskRun) {
return;
}
return taskRun?.runtimeEnvironment;
}