feat: run.ctx tidying and additions (#2322)

* Cleanup context and execution creation, cache stuff, add parent and root task run ids

* more efficient by using friendly IDs instead of doing joins

* metadata.root/parent now reference current run when run has no root/parent

* Adding changeset

* try to make test less flaky

* Clean imports

* Another attempt to fix the flaky test

* Fix usage by still passing durationMs and costInCents to the execution, just not the run.ctx
This commit is contained in:
Eric Allam
2025-07-30 13:47:12 +01:00
committed by GitHub
parent 5ea6605ec6
commit 14dcc76f93
38 changed files with 1478 additions and 433 deletions
+18
View File
@@ -0,0 +1,18 @@
---
"@trigger.dev/sdk": patch
---
Added and cleaned up the run ctx param:
- New optional properties `ctx.run.parentTaskRunId` and `ctx.run.rootTaskRunId` reference the current run's root/parent ID.
- Removed deprecated properties from `ctx`
- Added a new `ctx.deployment` object that contains information about the deployment associated with the run.
We also update `metadata.root` and `metadata.parent` to work even when the run is a "root" run (meaning it doesn't have a parent or a root associated run). This now works:
```ts
metadata.root.set("foo", "bar");
metadata.parent.set("baz", 1);
metadata.current().foo // "bar"
metadata.current().baz // 1
```
@@ -1,22 +1,29 @@
import {
type MachinePresetName,
MachinePreset,
prettyPrintPacket,
SemanticInternalAttributes,
TaskRunContext,
TaskRunError,
V3TaskRunContext,
} from "@trigger.dev/core/v3";
import { getMaxDuration } from "@trigger.dev/core/v3/isomorphic";
import { AttemptId, getMaxDuration } from "@trigger.dev/core/v3/isomorphic";
import { RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
import { logger } from "~/services/logger.server";
import { eventRepository, rehydrateAttribute } from "~/v3/eventRepository.server";
import { machinePresetFromName, machinePresetFromRun } from "~/v3/machinePresets.server";
import { machinePresetFromRun } from "~/v3/machinePresets.server";
import { getTaskEventStoreTableForRun, type TaskEventStoreTable } from "~/v3/taskEventStore.server";
import { isFailedRunStatus, isFinalRunStatus } from "~/v3/taskStatus";
import { BasePresenter } from "./basePresenter.server";
import { WaitpointPresenter } from "./WaitpointPresenter.server";
import { engine } from "~/v3/runEngine.server";
type Result = Awaited<ReturnType<SpanPresenter["call"]>>;
export type Span = NonNullable<NonNullable<Result>["span"]>;
export type SpanRun = NonNullable<NonNullable<Result>["run"]>;
type FindRunResult = NonNullable<
Awaited<ReturnType<InstanceType<typeof SpanPresenter>["findRun"]>>
>;
type GetSpanResult = NonNullable<Awaited<ReturnType<(typeof eventRepository)["getSpan"]>>>;
export class SpanPresenter extends BasePresenter {
public async call({
@@ -60,7 +67,7 @@ export class SpanPresenter extends BasePresenter {
const eventStore = getTaskEventStoreTableForRun(parentRun);
const run = await this.#getRun({
const run = await this.getRun({
eventStore,
traceId,
spanId,
@@ -95,7 +102,7 @@ export class SpanPresenter extends BasePresenter {
};
}
async #getRun({
async getRun({
eventStore,
traceId,
spanId,
@@ -120,6 +127,146 @@ export class SpanPresenter extends BasePresenter {
return;
}
const run = await this.findRun({ span, spanId });
if (!run) {
return;
}
const isFinished = isFinalRunStatus(run.status);
const output = !isFinished
? undefined
: run.outputType === "application/store"
? `/resources/packets/${run.runtimeEnvironment.id}/${run.output}`
: typeof run.output !== "undefined" && run.output !== null
? await prettyPrintPacket(run.output, run.outputType ?? undefined)
: undefined;
const payload =
run.payloadType === "application/store"
? `/resources/packets/${run.runtimeEnvironment.id}/${run.payload}`
: typeof run.payload !== "undefined" && run.payload !== null
? await prettyPrintPacket(run.payload, run.payloadType ?? undefined)
: undefined;
let error: TaskRunError | undefined = undefined;
if (run?.error) {
const result = TaskRunError.safeParse(run.error);
if (result.success) {
error = result.data;
} else {
error = {
type: "CUSTOM_ERROR",
raw: JSON.stringify(run.error),
};
}
}
const metadata = run.metadata
? await prettyPrintPacket(run.metadata, run.metadataType, {
filteredKeys: ["$$streams", "$$streamsVersion", "$$streamsBaseUrl"],
})
: undefined;
const machine = run.machinePreset ? machinePresetFromRun(run) : undefined;
const context = await this.#getTaskRunContext({ run, machine: machine ?? undefined });
return {
id: run.id,
friendlyId: run.friendlyId,
status: run.status,
statusReason: run.statusReason ?? undefined,
createdAt: run.createdAt,
startedAt: run.startedAt,
executedAt: run.executedAt,
updatedAt: run.updatedAt,
delayUntil: run.delayUntil,
expiredAt: run.expiredAt,
completedAt: run.completedAt,
logsDeletedAt: run.logsDeletedAt,
ttl: run.ttl,
taskIdentifier: run.taskIdentifier,
version: run.lockedToVersion?.version,
sdkVersion: run.lockedToVersion?.sdkVersion,
runtime: run.lockedToVersion?.runtime,
runtimeVersion: run.lockedToVersion?.runtimeVersion,
isTest: run.isTest,
replayedFromTaskRunFriendlyId: run.replayedFromTaskRunFriendlyId,
environmentId: run.runtimeEnvironment.id,
idempotencyKey: run.idempotencyKey,
idempotencyKeyExpiresAt: run.idempotencyKeyExpiresAt,
schedule: await this.resolveSchedule(run.scheduleId ?? undefined),
queue: {
name: run.queue,
isCustomQueue: !run.queue.startsWith("task/"),
concurrencyKey: run.concurrencyKey,
},
tags: run.runTags,
baseCostInCents: run.baseCostInCents,
costInCents: run.costInCents,
totalCostInCents: run.costInCents + run.baseCostInCents,
usageDurationMs: run.usageDurationMs,
isFinished,
isRunning: RUNNING_STATUSES.includes(run.status),
isError: isFailedRunStatus(run.status),
payload,
payloadType: run.payloadType,
output,
outputType: run?.outputType ?? "application/json",
error,
relationships: {
root: run.rootTaskRun
? {
...run.rootTaskRun,
isParent: run.parentTaskRun?.friendlyId === run.rootTaskRun.friendlyId,
}
: undefined,
parent: run.parentTaskRun ?? undefined,
},
context: JSON.stringify(context, null, 2),
metadata,
maxDurationInSeconds: getMaxDuration(run.maxDurationInSeconds),
batch: run.batch ? { friendlyId: run.batch.friendlyId } : undefined,
engine: run.engine,
workerQueue: run.workerQueue,
spanId: run.spanId,
isCached: !!span.originalRun,
machinePreset: machine?.name,
};
}
async resolveSchedule(scheduleId?: string) {
if (!scheduleId) {
return;
}
const schedule = await this._replica.taskSchedule.findFirst({
where: {
id: scheduleId,
},
select: {
friendlyId: true,
generatorExpression: true,
timezone: true,
generatorDescription: true,
},
});
if (!schedule) {
return;
}
return {
friendlyId: schedule.friendlyId,
generatorExpression: schedule.generatorExpression,
description: schedule.generatorDescription,
timezone: schedule.timezone,
};
}
async findRun({ span, spanId }: { span: GetSpanResult; spanId: string }) {
const run = await this._replica.taskRun.findFirst({
select: {
id: true,
@@ -132,11 +279,7 @@ export class SpanPresenter extends BasePresenter {
isTest: true,
maxDurationInSeconds: true,
taskEventStore: true,
tags: {
select: {
name: true,
},
},
runTags: true,
machinePreset: true,
lockedToVersion: {
select: {
@@ -219,6 +362,18 @@ export class SpanPresenter extends BasePresenter {
},
},
replayedFromTaskRunFriendlyId: true,
attempts: {
take: 1,
orderBy: {
createdAt: "desc",
},
select: {
number: true,
status: true,
createdAt: true,
friendlyId: true,
},
},
},
where: span.originalRun
? {
@@ -229,180 +384,7 @@ export class SpanPresenter extends BasePresenter {
},
});
if (!run) {
return;
}
const isFinished = isFinalRunStatus(run.status);
const output = !isFinished
? undefined
: run.outputType === "application/store"
? `/resources/packets/${run.runtimeEnvironment.id}/${run.output}`
: typeof run.output !== "undefined" && run.output !== null
? await prettyPrintPacket(run.output, run.outputType ?? undefined)
: undefined;
const payload =
run.payloadType === "application/store"
? `/resources/packets/${run.runtimeEnvironment.id}/${run.payload}`
: typeof run.payload !== "undefined" && run.payload !== null
? await prettyPrintPacket(run.payload, run.payloadType ?? undefined)
: undefined;
let error: TaskRunError | undefined = undefined;
if (run?.error) {
const result = TaskRunError.safeParse(run.error);
if (result.success) {
error = result.data;
} else {
error = {
type: "CUSTOM_ERROR",
raw: JSON.stringify(run.error),
};
}
}
const metadata = run.metadata
? await prettyPrintPacket(run.metadata, run.metadataType, {
filteredKeys: ["$$streams", "$$streamsVersion", "$$streamsBaseUrl"],
})
: undefined;
const machine = run.machinePreset ? machinePresetFromRun(run) : undefined;
const context = {
task: {
id: run.taskIdentifier,
filePath: run.lockedBy?.filePath,
},
run: {
id: run.friendlyId,
createdAt: run.createdAt,
tags: run.tags.map((tag) => tag.name),
isTest: run.isTest,
idempotencyKey: run.idempotencyKey ?? undefined,
startedAt: run.startedAt ?? run.createdAt,
durationMs: run.usageDurationMs,
costInCents: run.costInCents,
baseCostInCents: run.baseCostInCents,
maxAttempts: run.maxAttempts ?? undefined,
version: run.lockedToVersion?.version,
maxDuration: run.maxDurationInSeconds ?? undefined,
},
queue: {
name: run.queue,
},
environment: {
id: run.runtimeEnvironment.id,
slug: run.runtimeEnvironment.slug,
type: run.runtimeEnvironment.type,
},
organization: {
id: run.project.organization.id,
slug: run.project.organization.slug,
name: run.project.organization.title,
},
project: {
id: run.project.id,
ref: run.project.externalRef,
slug: run.project.slug,
name: run.project.name,
},
machine,
};
return {
id: run.id,
friendlyId: run.friendlyId,
status: run.status,
statusReason: run.statusReason ?? undefined,
createdAt: run.createdAt,
startedAt: run.startedAt,
executedAt: run.executedAt,
updatedAt: run.updatedAt,
delayUntil: run.delayUntil,
expiredAt: run.expiredAt,
completedAt: run.completedAt,
logsDeletedAt: run.logsDeletedAt,
ttl: run.ttl,
taskIdentifier: run.taskIdentifier,
version: run.lockedToVersion?.version,
sdkVersion: run.lockedToVersion?.sdkVersion,
runtime: run.lockedToVersion?.runtime,
runtimeVersion: run.lockedToVersion?.runtimeVersion,
isTest: run.isTest,
replayedFromTaskRunFriendlyId: run.replayedFromTaskRunFriendlyId,
environmentId: run.runtimeEnvironment.id,
idempotencyKey: run.idempotencyKey,
idempotencyKeyExpiresAt: run.idempotencyKeyExpiresAt,
schedule: await this.resolveSchedule(run.scheduleId ?? undefined),
queue: {
name: run.queue,
isCustomQueue: !run.queue.startsWith("task/"),
concurrencyKey: run.concurrencyKey,
},
tags: run.tags.map((tag) => tag.name),
baseCostInCents: run.baseCostInCents,
costInCents: run.costInCents,
totalCostInCents: run.costInCents + run.baseCostInCents,
usageDurationMs: run.usageDurationMs,
isFinished,
isRunning: RUNNING_STATUSES.includes(run.status),
isError: isFailedRunStatus(run.status),
payload,
payloadType: run.payloadType,
output,
outputType: run?.outputType ?? "application/json",
error,
relationships: {
root: run.rootTaskRun
? {
...run.rootTaskRun,
isParent: run.parentTaskRun?.friendlyId === run.rootTaskRun.friendlyId,
}
: undefined,
parent: run.parentTaskRun ?? undefined,
},
context: JSON.stringify(context, null, 2),
metadata,
maxDurationInSeconds: getMaxDuration(run.maxDurationInSeconds),
batch: run.batch ? { friendlyId: run.batch.friendlyId } : undefined,
engine: run.engine,
workerQueue: run.workerQueue,
spanId: run.spanId,
isCached: !!span.originalRun,
machinePreset: machine?.name,
};
}
async resolveSchedule(scheduleId?: string) {
if (!scheduleId) {
return;
}
const schedule = await this._replica.taskSchedule.findFirst({
where: {
id: scheduleId,
},
select: {
friendlyId: true,
generatorExpression: true,
timezone: true,
generatorDescription: true,
},
});
if (!schedule) {
return;
}
return {
friendlyId: schedule.friendlyId,
generatorExpression: schedule.generatorExpression,
description: schedule.generatorDescription,
timezone: schedule.timezone,
};
return run;
}
async #getSpan({
@@ -513,4 +495,83 @@ export class SpanPresenter extends BasePresenter {
return { ...data, entity: null };
}
}
async #getTaskRunContext({ run, machine }: { run: FindRunResult; machine?: MachinePreset }) {
if (run.engine === "V1") {
return this.#getV3TaskRunContext({ run, machine });
} else {
return this.#getV4TaskRunContext({ run });
}
}
async #getV3TaskRunContext({
run,
machine,
}: {
run: FindRunResult;
machine?: MachinePreset;
}): Promise<V3TaskRunContext> {
const attempt = run.attempts[0];
const context = {
attempt: attempt
? {
id: attempt.friendlyId,
number: attempt.number,
status: attempt.status,
startedAt: attempt.createdAt,
}
: {
id: AttemptId.generate().friendlyId,
number: 1,
status: "PENDING" as const,
startedAt: run.updatedAt,
},
task: {
id: run.taskIdentifier,
filePath: run.lockedBy?.filePath ?? "",
},
run: {
id: run.friendlyId,
createdAt: run.createdAt,
tags: run.runTags,
isTest: run.isTest,
idempotencyKey: run.idempotencyKey ?? undefined,
startedAt: run.startedAt ?? run.createdAt,
durationMs: run.usageDurationMs,
costInCents: run.costInCents,
baseCostInCents: run.baseCostInCents,
maxAttempts: run.maxAttempts ?? undefined,
version: run.lockedToVersion?.version,
maxDuration: run.maxDurationInSeconds ?? undefined,
},
queue: {
name: run.queue,
id: run.queue,
},
environment: {
id: run.runtimeEnvironment.id,
slug: run.runtimeEnvironment.slug,
type: run.runtimeEnvironment.type,
},
organization: {
id: run.project.organization.id,
slug: run.project.organization.slug,
name: run.project.organization.title,
},
project: {
id: run.project.id,
ref: run.project.externalRef,
slug: run.project.slug,
name: run.project.name,
},
machine,
} satisfies V3TaskRunContext;
return context;
}
async #getV4TaskRunContext({ run }: { run: FindRunResult }): Promise<TaskRunContext> {
return engine.resolveTaskRunContext(run.id);
}
}
@@ -76,6 +76,30 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
lockedBy: {
select: {
filePath: true,
worker: {
select: {
deployment: {
select: {
friendlyId: true,
shortCode: true,
version: true,
runtime: true,
runtimeVersion: true,
git: true,
},
},
},
},
},
},
parentTaskRun: {
select: {
friendlyId: true,
},
},
rootTaskRun: {
select: {
friendlyId: true,
},
},
},
@@ -163,6 +187,8 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
baseCostInCents: run.baseCostInCents,
maxAttempts: run.maxAttempts ?? undefined,
version: run.lockedToVersion?.version,
parentTaskRunId: run.parentTaskRun?.friendlyId ?? undefined,
rootTaskRunId: run.rootTaskRun?.friendlyId ?? undefined,
},
queue: {
name: run.queue,
@@ -184,6 +210,16 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
name: run.project.name,
},
machine: run.machinePreset ? machinePresetFromRun(run) : undefined,
deployment: run.lockedBy?.worker.deployment
? {
id: run.lockedBy.worker.deployment.friendlyId,
shortCode: run.lockedBy.worker.deployment.shortCode,
version: run.lockedBy.worker.deployment.version,
runtime: run.lockedBy.worker.deployment.runtime,
runtimeVersion: run.lockedBy.worker.deployment.runtimeVersion,
git: run.lockedBy.worker.deployment.git,
}
: undefined,
};
return typedjson({
+2 -1
View File
@@ -4,6 +4,7 @@ import {
TaskRunExecution,
TaskRunExecutionRetry,
TaskRunFailedExecutionResult,
V3TaskRunExecution,
} from "@trigger.dev/core/v3";
import type { Prisma, TaskRun } from "@trigger.dev/database";
import * as semver from "semver";
@@ -129,7 +130,7 @@ export class FailedTaskRunRetryHelper extends BaseService {
async #getRetriableAttemptExecution(
run: TaskRunWithAttempts,
completion: TaskRunFailedExecutionResult
): Promise<TaskRunExecution | undefined> {
): Promise<V3TaskRunExecution | undefined> {
let attempt = run.attempts[0];
// We need to create an attempt if:
@@ -1,6 +1,6 @@
import { Context, ROOT_CONTEXT, Span, SpanKind, context, trace } from "@opentelemetry/api";
import {
TaskRunExecution,
V3TaskRunExecution,
TaskRunExecutionLazyAttemptPayload,
TaskRunExecutionResult,
TaskRunFailedExecutionResult,
@@ -138,7 +138,7 @@ export class DevQueueConsumer {
public async taskAttemptCompleted(
workerId: string,
completion: TaskRunExecutionResult,
execution: TaskRunExecution
execution: V3TaskRunExecution
) {
if (completion.ok) {
this._taskSuccesses++;
@@ -11,8 +11,8 @@ import {
import {
AckCallbackResult,
MachinePreset,
ProdTaskRunExecution,
ProdTaskRunExecutionPayload,
V3ProdTaskRunExecution,
V3ProdTaskRunExecutionPayload,
TaskRunError,
TaskRunErrorCodes,
TaskRunExecution,
@@ -1679,7 +1679,7 @@ class SharedQueueTasks {
private async _executionFromAttempt(
attempt: AttemptForExecution,
machinePreset?: MachinePreset
): Promise<ProdTaskRunExecution> {
): Promise<V3ProdTaskRunExecution> {
const { backgroundWorkerTask, taskRun, queue } = attempt;
if (!machinePreset) {
@@ -1693,7 +1693,7 @@ class SharedQueueTasks {
dataType: taskRun.metadataType,
});
const execution: ProdTaskRunExecution = {
const execution: V3ProdTaskRunExecution = {
task: {
id: backgroundWorkerTask.slug,
filePath: backgroundWorkerTask.filePath,
@@ -1784,7 +1784,7 @@ class SharedQueueTasks {
setToExecuting?: boolean;
isRetrying?: boolean;
skipStatusChecks?: boolean;
}): Promise<ProdTaskRunExecutionPayload | undefined> {
}): Promise<V3ProdTaskRunExecutionPayload | undefined> {
const attempt = await prisma.taskRunAttempt.findFirst({
where: {
id,
@@ -1874,7 +1874,7 @@ class SharedQueueTasks {
machinePreset
);
const payload: ProdTaskRunExecutionPayload = {
const payload: V3ProdTaskRunExecutionPayload = {
execution,
traceContext: taskRun.traceContext as Record<string, unknown>,
environment: variables.reduce((acc: Record<string, string>, curr) => {
@@ -1888,7 +1888,7 @@ class SharedQueueTasks {
async getResumePayload(attemptId: string): Promise<
| {
execution: ProdTaskRunExecution;
execution: V3ProdTaskRunExecution;
completion: TaskRunExecutionResult;
}
| undefined
@@ -1927,7 +1927,7 @@ class SharedQueueTasks {
async getResumePayloads(attemptIds: string[]): Promise<
Array<{
execution: ProdTaskRunExecution;
execution: V3ProdTaskRunExecution;
completion: TaskRunExecutionResult;
}>
> {
@@ -1985,7 +1985,7 @@ class SharedQueueTasks {
id: string,
setToExecuting?: boolean,
isRetrying?: boolean
): Promise<ProdTaskRunExecutionPayload | undefined> {
): Promise<V3ProdTaskRunExecutionPayload | undefined> {
const run = await prisma.taskRun.findFirst({
where: {
id,
@@ -2,13 +2,13 @@ import { Attributes } from "@opentelemetry/api";
import {
MachinePresetName,
TaskRunContext,
TaskRunError,
TaskRunErrorCodes,
TaskRunExecution,
TaskRunExecutionResult,
TaskRunExecutionRetry,
TaskRunFailedExecutionResult,
TaskRunSuccessfulExecutionResult,
V3TaskRunExecution,
flattenAttributes,
isOOMRunError,
sanitizeError,
@@ -60,7 +60,7 @@ export class CompleteAttemptService extends BaseService {
checkpoint,
}: {
completion: TaskRunExecutionResult;
execution: TaskRunExecution;
execution: V3TaskRunExecution;
env?: AuthenticatedEnvironment;
checkpoint?: CheckpointData;
}): Promise<"COMPLETED" | "RETRIED"> {
@@ -196,7 +196,7 @@ export class CompleteAttemptService extends BaseService {
checkpoint,
}: {
completion: TaskRunFailedExecutionResult;
execution: TaskRunExecution;
execution: V3TaskRunExecution;
taskRunAttempt: NonNullable<FoundAttempt>;
env?: AuthenticatedEnvironment;
checkpoint?: CheckpointData;
@@ -559,7 +559,7 @@ export class CompleteAttemptService extends BaseService {
forceRequeue = false,
oomMachine,
}: {
execution: TaskRunExecution;
execution: V3TaskRunExecution;
executionRetry: TaskRunExecutionRetry;
executionRetryInferred: boolean;
taskRunAttempt: NonNullable<FoundAttempt>;
@@ -648,7 +648,7 @@ export class CompleteAttemptService extends BaseService {
executionRetryInferred,
checkpoint,
}: {
execution: TaskRunExecution;
execution: V3TaskRunExecution;
taskRunAttempt: NonNullable<FoundAttempt>;
executionRetry: TaskRunExecutionRetry;
executionRetryInferred: boolean;
@@ -1,4 +1,4 @@
import { parsePacket, TaskRunExecution } from "@trigger.dev/core/v3";
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";
@@ -25,7 +25,7 @@ export class CreateTaskRunAttemptService extends BaseService {
setToExecuting?: boolean;
startAtZero?: boolean;
}): Promise<{
execution: TaskRunExecution;
execution: V3TaskRunExecution;
run: TaskRun;
attempt: TaskRunAttempt;
}> {
@@ -189,7 +189,7 @@ export class CreateTaskRunAttemptService extends BaseService {
dataType: taskRun.metadataType,
});
const execution: TaskRunExecution = {
const execution: V3TaskRunExecution = {
task: {
id: lockedBy.slug,
filePath: lockedBy.filePath,
+3
View File
@@ -0,0 +1,3 @@
# Redis
This is a simple package that is used to return a valid Redis client and provides an error callback. It will log and swallow errors if they're not handled.
+18
View File
@@ -0,0 +1,18 @@
{
"name": "@internal/cache",
"private": true,
"version": "0.0.1",
"main": "./src/index.ts",
"types": "./src/index.ts",
"type": "module",
"dependencies": {
"@unkey/cache": "^1.5.0",
"@unkey/error": "^0.2.0",
"@trigger.dev/core": "workspace:*",
"@internal/redis": "workspace:*",
"superjson": "^2.2.1"
},
"scripts": {
"typecheck": "tsc --noEmit"
}
}
+8
View File
@@ -0,0 +1,8 @@
export {
createCache,
DefaultStatefulContext,
Namespace,
type Cache as UnkeyCache,
} from "@unkey/cache";
export { MemoryStore } from "@unkey/cache/stores";
export { RedisCacheStore } from "./stores/redis.js";
+105
View File
@@ -0,0 +1,105 @@
import { CacheError } from "@unkey/cache";
import type { Entry, Store } from "@unkey/cache/stores";
import { Err, Ok, type Result } from "@unkey/error";
import { createRedisClient, Redis, RedisOptions } from "@internal/redis";
export type RedisCacheStoreConfig = {
connection: RedisOptions;
name?: string;
useModernCacheKeyBuilder?: boolean;
};
export class RedisCacheStore<TNamespace extends string, TValue = any>
implements Store<TNamespace, TValue>
{
public readonly name = "redis";
private readonly redis: Redis;
constructor(private readonly config: RedisCacheStoreConfig) {
this.redis = createRedisClient({
...config.connection,
name: config.name ?? "trigger:cacheStore",
});
}
private buildCacheKey(namespace: TNamespace, key: string): string {
if (this.config.useModernCacheKeyBuilder) {
return [namespace, key].join(":");
}
return [namespace, key].join("::");
}
public async get(
namespace: TNamespace,
key: string
): Promise<Result<Entry<TValue> | undefined, CacheError>> {
let raw: string | null;
try {
raw = await this.redis.get(this.buildCacheKey(namespace, key));
} catch (err) {
return Err(
new CacheError({
tier: this.name,
key,
message: (err as Error).message,
})
);
}
if (!raw) {
return Promise.resolve(Ok(undefined));
}
try {
const superjson = await import("superjson");
const entry = superjson.parse(raw) as Entry<TValue>;
return Ok(entry);
} catch (err) {
return Err(
new CacheError({
tier: this.name,
key,
message: (err as Error).message,
})
);
}
}
public async set(
namespace: TNamespace,
key: string,
entry: Entry<TValue>
): Promise<Result<void, CacheError>> {
const cacheKey = this.buildCacheKey(namespace, key);
try {
const superjson = await import("superjson");
await this.redis.set(cacheKey, superjson.stringify(entry), "PXAT", entry.staleUntil);
return Ok();
} catch (err) {
return Err(
new CacheError({
tier: this.name,
key,
message: (err as Error).message,
})
);
}
}
public async remove(namespace: TNamespace, key: string): Promise<Result<void, CacheError>> {
try {
const cacheKey = this.buildCacheKey(namespace, key);
await this.redis.del(cacheKey);
return Promise.resolve(Ok());
} catch (err) {
return Err(
new CacheError({
tier: this.name,
key,
message: (err as Error).message,
})
);
}
}
}
+23
View File
@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2019",
"lib": ["ES2019", "DOM", "DOM.Iterable", "DOM.AsyncIterable"],
"module": "Node16",
"moduleResolution": "Node16",
"moduleDetection": "force",
"verbatimModuleSyntax": false,
"types": ["vitest/globals"],
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"preserveWatchOutput": true,
"skipLibCheck": true,
"noEmit": true,
"strict": true,
"paths": {
"@trigger.dev/core": ["../../packages/core/src/index"],
"@trigger.dev/core/*": ["../../packages/core/src/*"]
}
},
"exclude": ["node_modules"]
}
+1 -1
View File
@@ -25,7 +25,7 @@
"@internal/tracing": "workspace:*",
"@trigger.dev/core": "workspace:*",
"@trigger.dev/database": "workspace:*",
"@unkey/cache": "^1.5.0",
"@internal/cache": "workspace:*",
"assert-never": "^1.2.1",
"nanoid": "3.3.8",
"redlock": "5.0.0-beta.2",
@@ -9,6 +9,7 @@ import {
ExecutionResult,
RunExecutionData,
StartRunAttemptResult,
TaskRunContext,
TaskRunExecutionResult,
} from "@trigger.dev/core/v3";
import { RunId, WaitpointId } from "@trigger.dev/core/v3/isomorphic";
@@ -288,6 +289,7 @@ export class RunEngine {
delayedRunSystem: this.delayedRunSystem,
machines: this.options.machines,
retryWarmStartThresholdMs: this.options.retryWarmStartThresholdMs,
redisOptions: this.options.cache?.redis ?? this.options.runLock.redis,
});
this.dequeueSystem = new DequeueSystem({
@@ -1054,6 +1056,10 @@ export class RunEngine {
}
}
async resolveTaskRunContext(runId: string): Promise<TaskRunContext> {
return this.runAttemptSystem.resolveTaskRunContext(runId);
}
async getSnapshotsSince({
runId,
snapshotId,
@@ -1,3 +1,12 @@
import {
createCache,
DefaultStatefulContext,
MemoryStore,
Namespace,
RedisCacheStore,
UnkeyCache,
} from "@internal/cache";
import { RedisOptions } from "@internal/redis";
import { startSpan } from "@internal/tracing";
import { tryCatch } from "@trigger.dev/core/utils";
import {
@@ -5,9 +14,16 @@ import {
ExecutionResult,
FlushedRunMetadata,
GitMeta,
MachinePreset,
MachinePresetName,
StartRunAttemptResult,
TaskRunContext,
TaskRunError,
TaskRunExecution,
TaskRunExecutionDeployment,
TaskRunExecutionOrganization,
TaskRunExecutionProject,
TaskRunExecutionQueue,
TaskRunExecutionResult,
TaskRunFailedExecutionResult,
TaskRunInternalError,
@@ -23,7 +39,7 @@ import {
import { MAX_TASK_RUN_ATTEMPTS } from "../consts.js";
import { runStatusFromError, ServiceValidationError } from "../errors.js";
import { sendNotificationToWorker } from "../eventBus.js";
import { getMachinePreset } from "../machinePresets.js";
import { getMachinePreset, machinePresetFromName } from "../machinePresets.js";
import { retryOutcomeFromCompletion } from "../retrying.js";
import { isExecuting, isInitialState } from "../statuses.js";
import { RunEngineOptions } from "../types.js";
@@ -36,6 +52,7 @@ import {
} from "./executionSnapshotSystem.js";
import { SystemResources } from "./systems.js";
import { WaitpointSystem } from "./waitpointSystem.js";
import { BatchId, RunId } from "@trigger.dev/core/v3/isomorphic";
export type RunAttemptSystemOptions = {
resources: SystemResources;
@@ -45,14 +62,54 @@ export type RunAttemptSystemOptions = {
delayedRunSystem: DelayedRunSystem;
retryWarmStartThresholdMs?: number;
machines: RunEngineOptions["machines"];
redisOptions: RedisOptions;
};
type BackwardsCompatibleTaskRunExecution = Omit<TaskRunExecution, "task" | "attempt" | "run"> & {
task: TaskRunExecution["task"] & {
exportName: string | undefined;
};
attempt: TaskRunExecution["attempt"] & {
id: string;
backgroundWorkerId: string;
backgroundWorkerTaskId: string;
status: string;
};
run: TaskRunExecution["run"] & {
context: undefined;
durationMs: number;
costInCents: number;
baseCostInCents: number;
};
};
const ORG_FRESH_TTL = 60000 * 60 * 24; // 1 day
const ORG_STALE_TTL = 60000 * 60 * 24 * 2; // 2 days
const PROJECT_FRESH_TTL = 60000 * 60 * 24; // 1 day
const PROJECT_STALE_TTL = 60000 * 60 * 24 * 2; // 2 days
const TASK_FRESH_TTL = 60000 * 60 * 24; // 1 day
const TASK_STALE_TTL = 60000 * 60 * 24 * 2; // 2 days
const MACHINE_PRESET_FRESH_TTL = 60000 * 60 * 24; // 1 day
const MACHINE_PRESET_STALE_TTL = 60000 * 60 * 24 * 2; // 2 days
const DEPLOYMENT_FRESH_TTL = 60000 * 60 * 24; // 1 day
const DEPLOYMENT_STALE_TTL = 60000 * 60 * 24 * 2; // 2 days
const QUEUE_FRESH_TTL = 60000 * 60; // 1 hour
const QUEUE_STALE_TTL = 60000 * 60 * 2; // 2 hours
export class RunAttemptSystem {
private readonly $: SystemResources;
private readonly executionSnapshotSystem: ExecutionSnapshotSystem;
private readonly batchSystem: BatchSystem;
private readonly waitpointSystem: WaitpointSystem;
private readonly delayedRunSystem: DelayedRunSystem;
private readonly cache: UnkeyCache<{
tasks: BackwardsCompatibleTaskRunExecution["task"];
machinePresets: MachinePreset;
deployments: TaskRunExecutionDeployment;
queues: TaskRunExecutionQueue;
projects: TaskRunExecutionProject;
orgs: TaskRunExecutionOrganization;
}>;
constructor(private readonly options: RunAttemptSystemOptions) {
this.$ = options.resources;
@@ -60,6 +117,170 @@ export class RunAttemptSystem {
this.batchSystem = options.batchSystem;
this.waitpointSystem = options.waitpointSystem;
this.delayedRunSystem = options.delayedRunSystem;
const ctx = new DefaultStatefulContext();
// TODO: use an LRU cache for memory store
const memory = new MemoryStore({ persistentMap: new Map() });
const redisCacheStore = new RedisCacheStore({
name: "run-attempt-system",
connection: {
...options.redisOptions,
keyPrefix: "engine:run-attempt-system:cache:",
},
useModernCacheKeyBuilder: true,
});
this.cache = createCache({
orgs: new Namespace<TaskRunExecutionOrganization>(ctx, {
stores: [memory, redisCacheStore],
fresh: ORG_FRESH_TTL,
stale: ORG_STALE_TTL,
}),
projects: new Namespace<TaskRunExecutionProject>(ctx, {
stores: [memory, redisCacheStore],
fresh: PROJECT_FRESH_TTL,
stale: PROJECT_STALE_TTL,
}),
tasks: new Namespace<BackwardsCompatibleTaskRunExecution["task"]>(ctx, {
stores: [memory, redisCacheStore],
fresh: TASK_FRESH_TTL,
stale: TASK_STALE_TTL,
}),
machinePresets: new Namespace<MachinePreset>(ctx, {
stores: [memory, redisCacheStore],
fresh: MACHINE_PRESET_FRESH_TTL,
stale: MACHINE_PRESET_STALE_TTL,
}),
deployments: new Namespace<TaskRunExecutionDeployment>(ctx, {
stores: [memory, redisCacheStore],
fresh: DEPLOYMENT_FRESH_TTL,
stale: DEPLOYMENT_STALE_TTL,
}),
queues: new Namespace<TaskRunExecutionQueue>(ctx, {
stores: [memory, redisCacheStore],
fresh: QUEUE_FRESH_TTL,
stale: QUEUE_STALE_TTL,
}),
});
}
public async resolveTaskRunContext(runId: string): Promise<TaskRunContext> {
const run = await this.$.prisma.taskRun.findFirst({
where: {
id: runId,
},
select: {
id: true,
createdAt: true,
updatedAt: true,
executedAt: true,
baseCostInCents: true,
projectId: true,
organizationId: true,
friendlyId: true,
lockedById: true,
lockedQueueId: true,
queue: true,
attemptNumber: true,
status: true,
ttl: true,
machinePreset: true,
runTags: true,
isTest: true,
idempotencyKey: true,
startedAt: true,
maxAttempts: true,
taskVersion: true,
maxDurationInSeconds: true,
usageDurationMs: true,
costInCents: true,
traceContext: true,
priorityMs: true,
taskIdentifier: true,
runtimeEnvironment: {
select: {
id: true,
slug: true,
type: true,
branchName: true,
git: true,
organizationId: true,
},
},
parentTaskRunId: true,
rootTaskRunId: true,
batchId: true,
},
});
if (!run) {
throw new ServiceValidationError("Task run not found", 404);
}
const [task, queue, organization, project, machinePreset, deployment] = await Promise.all([
run.lockedById
? this.#resolveTaskRunExecutionTask(run.lockedById)
: Promise.resolve({
id: run.taskIdentifier,
filePath: "unknown",
}),
this.#resolveTaskRunExecutionQueue({
runId,
lockedQueueId: run.lockedQueueId ?? undefined,
queueName: run.queue,
runtimeEnvironmentId: run.runtimeEnvironment.id,
}),
this.#resolveTaskRunExecutionOrganization(run.runtimeEnvironment.organizationId),
this.#resolveTaskRunExecutionProjectByRuntimeEnvironmentId(run.runtimeEnvironment.id),
run.lockedById
? this.#resolveTaskRunExecutionMachinePreset(run.lockedById, run.machinePreset)
: Promise.resolve(
getMachinePreset({
defaultMachine: this.options.machines.defaultMachine,
machines: this.options.machines.machines,
config: undefined,
run,
})
),
run.lockedById
? this.#resolveTaskRunExecutionDeployment(run.lockedById)
: Promise.resolve(undefined),
]);
return {
run: {
id: run.friendlyId,
tags: run.runTags,
isTest: run.isTest,
createdAt: run.createdAt,
startedAt: run.startedAt ?? run.createdAt,
idempotencyKey: run.idempotencyKey ?? undefined,
maxAttempts: run.maxAttempts ?? undefined,
version: run.taskVersion ?? "unknown",
maxDuration: run.maxDurationInSeconds ?? undefined,
priority: run.priorityMs === 0 ? undefined : run.priorityMs / 1_000,
parentTaskRunId: run.parentTaskRunId ? RunId.toFriendlyId(run.parentTaskRunId) : undefined,
rootTaskRunId: run.rootTaskRunId ? RunId.toFriendlyId(run.rootTaskRunId) : undefined,
},
attempt: {
number: run.attemptNumber ?? 1,
startedAt: run.startedAt ?? new Date(),
},
task,
queue,
organization,
project,
machine: machinePreset,
deployment,
environment: {
id: run.runtimeEnvironment.id,
slug: run.runtimeEnvironment.slug,
type: run.runtimeEnvironment.type,
branchName: run.runtimeEnvironment.branchName ?? undefined,
git: safeParseGitMeta(run.runtimeEnvironment.git),
},
batch: run.batchId ? { id: BatchId.toFriendlyId(run.batchId) } : undefined,
};
}
public async startRunAttempt({
@@ -95,35 +316,19 @@ export class RunAttemptSystem {
throw new ServiceValidationError("Snapshot changed", 409);
}
const environment = await this.#getAuthenticatedEnvironmentFromRun(runId, prisma);
if (!environment) {
throw new ServiceValidationError("Environment not found", 404);
}
const taskRun = await prisma.taskRun.findFirst({
where: {
id: runId,
},
include: {
tags: true,
lockedBy: {
include: {
worker: {
select: {
id: true,
version: true,
sdkVersion: true,
cliVersion: true,
supportsLazyAttempts: true,
},
},
},
},
batchItems: {
include: {
batchTaskRun: true,
},
},
select: {
id: true,
friendlyId: true,
attemptNumber: true,
projectId: true,
runtimeEnvironmentId: true,
status: true,
lockedById: true,
ttl: true,
},
});
@@ -142,21 +347,10 @@ export class RunAttemptSystem {
throw new ServiceValidationError("Task run is cancelled", 400);
}
if (!taskRun.lockedBy) {
if (!taskRun.lockedById) {
throw new ServiceValidationError("Task run is not locked", 400);
}
const queue = await prisma.taskQueue.findFirst({
where: {
runtimeEnvironmentId: environment.id,
name: taskRun.queue,
},
});
if (!queue) {
throw new ServiceValidationError("Queue not found", 404);
}
//increment the attempt number (start at 1)
const nextAttemptNumber = (taskRun.attemptNumber ?? 0) + 1;
@@ -190,11 +384,50 @@ export class RunAttemptSystem {
attemptNumber: nextAttemptNumber,
executedAt: taskRun.attemptNumber === null ? new Date() : undefined,
},
include: {
tags: true,
lockedBy: {
include: { worker: true },
select: {
id: true,
createdAt: true,
updatedAt: true,
executedAt: true,
baseCostInCents: true,
projectId: true,
organizationId: true,
friendlyId: true,
lockedById: true,
lockedQueueId: true,
queue: true,
attemptNumber: true,
status: true,
ttl: true,
metadata: true,
metadataType: true,
machinePreset: true,
payload: true,
payloadType: true,
runTags: true,
isTest: true,
idempotencyKey: true,
startedAt: true,
maxAttempts: true,
taskVersion: true,
maxDurationInSeconds: true,
usageDurationMs: true,
costInCents: true,
traceContext: true,
priorityMs: true,
batchId: true,
runtimeEnvironment: {
select: {
id: true,
slug: true,
type: true,
branchName: true,
git: true,
organizationId: true,
},
},
parentTaskRunId: true,
rootTaskRunId: true,
},
});
@@ -222,7 +455,7 @@ export class RunAttemptSystem {
await this.$.worker.ack(`expireRun:${taskRun.id}`);
}
return { run, snapshot: newSnapshot };
return { updatedRun: run, snapshot: newSnapshot };
},
(error) => {
this.$.logger.error("RunEngine.createRunAttempt(): prisma.$transaction error", {
@@ -247,56 +480,59 @@ export class RunAttemptSystem {
throw new ServiceValidationError("Failed to create task run attempt", 500);
}
const { run, snapshot } = result;
const { updatedRun, snapshot } = result;
this.$.eventBus.emit("runAttemptStarted", {
time: new Date(),
run: {
id: run.id,
status: run.status,
createdAt: run.createdAt,
updatedAt: run.updatedAt,
id: updatedRun.id,
status: updatedRun.status,
createdAt: updatedRun.createdAt,
updatedAt: updatedRun.updatedAt,
attemptNumber: nextAttemptNumber,
baseCostInCents: run.baseCostInCents,
executedAt: run.executedAt ?? undefined,
baseCostInCents: updatedRun.baseCostInCents,
executedAt: updatedRun.executedAt ?? undefined,
},
organization: {
id: environment.organization.id,
id: updatedRun.runtimeEnvironment.organizationId,
},
project: {
id: environment.project.id,
id: updatedRun.projectId,
},
environment: {
id: environment.id,
id: updatedRun.runtimeEnvironment.id,
},
});
const machinePreset = getMachinePreset({
machines: this.options.machines.machines,
defaultMachine: this.options.machines.defaultMachine,
config: taskRun.lockedBy.machineConfig ?? {},
run: taskRun,
});
const environmentGit = safeParseGitMeta(updatedRun.runtimeEnvironment.git);
const metadata = await parsePacket({
data: taskRun.metadata ?? undefined,
dataType: taskRun.metadataType,
});
const [metadata, task, queue, organization, project, machinePreset, deployment] =
await Promise.all([
parsePacket({
data: updatedRun.metadata ?? undefined,
dataType: updatedRun.metadataType,
}),
this.#resolveTaskRunExecutionTask(taskRun.lockedById),
this.#resolveTaskRunExecutionQueue({
runId,
lockedQueueId: updatedRun.lockedQueueId ?? undefined,
queueName: updatedRun.queue,
runtimeEnvironmentId: updatedRun.runtimeEnvironment.id,
}),
this.#resolveTaskRunExecutionOrganization(
updatedRun.runtimeEnvironment.organizationId
),
this.#resolveTaskRunExecutionProjectByRuntimeEnvironmentId(
updatedRun.runtimeEnvironment.id
),
this.#resolveTaskRunExecutionMachinePreset(
taskRun.lockedById,
updatedRun.machinePreset
),
this.#resolveTaskRunExecutionDeployment(taskRun.lockedById),
]);
let git: GitMeta | undefined = undefined;
if (environment.git) {
const parsed = GitMeta.safeParse(environment.git);
if (parsed.success) {
git = parsed.data;
}
}
const execution: TaskRunExecution = {
task: {
id: run.lockedBy!.slug,
filePath: run.lockedBy!.filePath,
exportName: run.lockedBy!.exportName ?? undefined,
},
const execution: BackwardsCompatibleTaskRunExecution = {
attempt: {
number: nextAttemptNumber,
startedAt: latestSnapshot.updatedAt,
@@ -310,59 +546,56 @@ export class RunAttemptSystem {
status: "deprecated",
},
run: {
id: run.friendlyId,
payload: run.payload,
payloadType: run.payloadType,
createdAt: run.createdAt,
tags: run.tags.map((tag) => tag.name),
isTest: run.isTest,
idempotencyKey: run.idempotencyKey ?? undefined,
startedAt: run.startedAt ?? run.createdAt,
maxAttempts: run.maxAttempts ?? undefined,
version: run.lockedBy!.worker.version,
id: updatedRun.friendlyId,
payload: updatedRun.payload,
payloadType: updatedRun.payloadType,
createdAt: updatedRun.createdAt,
tags: updatedRun.runTags,
isTest: updatedRun.isTest,
idempotencyKey: updatedRun.idempotencyKey ?? undefined,
startedAt: updatedRun.startedAt ?? updatedRun.createdAt,
maxAttempts: updatedRun.maxAttempts ?? undefined,
version: updatedRun.taskVersion ?? "unknown",
metadata,
maxDuration: run.maxDurationInSeconds ?? undefined,
maxDuration: updatedRun.maxDurationInSeconds ?? undefined,
/** @deprecated */
context: undefined,
/** @deprecated */
durationMs: run.usageDurationMs,
durationMs: updatedRun.usageDurationMs,
/** @deprecated */
costInCents: run.costInCents,
costInCents: updatedRun.costInCents,
/** @deprecated */
baseCostInCents: run.baseCostInCents,
traceContext: run.traceContext as Record<string, string | undefined>,
priority: run.priorityMs === 0 ? undefined : run.priorityMs / 1_000,
},
queue: {
id: queue.friendlyId,
name: queue.name,
},
environment: {
id: environment.id,
slug: environment.slug,
type: environment.type,
branchName: environment.branchName ?? undefined,
git,
},
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 }
baseCostInCents: updatedRun.baseCostInCents,
traceContext: updatedRun.traceContext as Record<string, string | undefined>,
priority: updatedRun.priorityMs === 0 ? undefined : updatedRun.priorityMs / 1_000,
parentTaskRunId: updatedRun.parentTaskRunId
? RunId.toFriendlyId(updatedRun.parentTaskRunId)
: undefined,
rootTaskRunId: updatedRun.rootTaskRunId
? RunId.toFriendlyId(updatedRun.rootTaskRunId)
: undefined,
},
task,
queue,
environment: {
id: updatedRun.runtimeEnvironment.id,
slug: updatedRun.runtimeEnvironment.slug,
type: updatedRun.runtimeEnvironment.type,
branchName: updatedRun.runtimeEnvironment.branchName ?? undefined,
git: environmentGit,
},
organization,
project,
machine: machinePreset,
deployment,
batch: updatedRun.batchId
? {
id: BatchId.toFriendlyId(updatedRun.batchId),
}
: undefined,
};
return { run, snapshot, execution };
return { run: updatedRun, snapshot, execution };
});
},
{
@@ -1311,27 +1544,248 @@ export class RunAttemptSystem {
await this.$.worker.ack(`heartbeatSnapshot.${id}`);
}
async #getAuthenticatedEnvironmentFromRun(runId: string, tx?: PrismaClientOrTransaction) {
const prisma = tx ?? this.$.prisma;
const taskRun = await prisma.taskRun.findFirst({
where: {
id: runId,
},
include: {
runtimeEnvironment: {
include: {
organization: true,
project: true,
},
async #resolveTaskRunExecutionTask(
backgroundWorkerTaskId: string
): Promise<BackwardsCompatibleTaskRunExecution["task"]> {
const result = await this.cache.tasks.swr(backgroundWorkerTaskId, async () => {
const task = await this.$.prisma.backgroundWorkerTask.findFirstOrThrow({
where: {
id: backgroundWorkerTaskId,
},
},
select: {
id: true,
slug: true,
filePath: true,
exportName: true,
},
});
return {
id: task.slug,
filePath: task.filePath,
exportName: task.exportName ?? undefined,
};
});
if (!taskRun) {
return;
if (result.err) {
throw result.err;
}
return taskRun?.runtimeEnvironment;
if (!result.val) {
throw new ServiceValidationError(
`Could not resolve task execution data for task ${backgroundWorkerTaskId}`
);
}
return result.val;
}
async #resolveTaskRunExecutionOrganization(
organizationId: string
): Promise<TaskRunExecutionOrganization> {
const result = await this.cache.orgs.swr(organizationId, async () => {
const organization = await this.$.prisma.organization.findFirstOrThrow({
where: { id: organizationId },
select: {
id: true,
title: true,
slug: true,
},
});
return {
id: organization.id,
name: organization.title,
slug: organization.slug,
};
});
if (result.err) {
throw result.err;
}
if (!result.val) {
throw new ServiceValidationError(
`Could not resolve organization data for organization ${organizationId}`
);
}
return result.val;
}
async #resolveTaskRunExecutionProjectByRuntimeEnvironmentId(
runtimeEnvironmentId: string
): Promise<TaskRunExecutionProject> {
const result = await this.cache.projects.swr(runtimeEnvironmentId, async () => {
const { project } = await this.$.prisma.runtimeEnvironment.findFirstOrThrow({
where: { id: runtimeEnvironmentId },
select: {
id: true,
project: {
select: {
id: true,
name: true,
slug: true,
externalRef: true,
},
},
},
});
return {
id: project.id,
name: project.name,
slug: project.slug,
ref: project.externalRef,
};
});
if (result.err) {
throw result.err;
}
if (!result.val) {
throw new ServiceValidationError(
`Could not resolve project data for project ${runtimeEnvironmentId}`
);
}
return result.val;
}
async #resolveTaskRunExecutionMachinePreset(
backgroundWorkerTaskId: string,
runMachinePreset: string | null
): Promise<MachinePreset> {
if (runMachinePreset) {
return machinePresetFromName(
this.options.machines.machines,
runMachinePreset as MachinePresetName
);
}
const result = await this.cache.machinePresets.swr(backgroundWorkerTaskId, async () => {
const { machineConfig } = await this.$.prisma.backgroundWorkerTask.findFirstOrThrow({
where: {
id: backgroundWorkerTaskId,
},
select: {
machineConfig: true,
},
});
return getMachinePreset({
machines: this.options.machines.machines,
defaultMachine: this.options.machines.defaultMachine,
config: machineConfig,
run: { machinePreset: null },
});
});
if (result.err) {
throw result.err;
}
if (!result.val) {
throw new ServiceValidationError(
`Could not resolve machine preset for task ${backgroundWorkerTaskId}`
);
}
return result.val;
}
async #resolveTaskRunExecutionQueue(params: {
runId: string;
lockedQueueId?: string;
queueName: string;
runtimeEnvironmentId: string;
}): Promise<TaskRunExecutionQueue> {
const result = await this.cache.queues.swr(params.runId, async () => {
const queue = params.lockedQueueId
? await this.$.prisma.taskQueue.findFirst({
where: {
id: params.lockedQueueId,
},
select: {
id: true,
friendlyId: true,
name: true,
},
})
: await this.$.prisma.taskQueue.findFirst({
where: {
runtimeEnvironmentId: params.runtimeEnvironmentId,
name: params.queueName,
},
select: {
id: true,
friendlyId: true,
name: true,
},
});
if (!queue) {
throw new ServiceValidationError(
`Could not resolve queue data for queue ${params.queueName}`,
404
);
}
return {
id: queue.friendlyId,
name: queue.name,
};
});
if (result.err) {
throw result.err;
}
if (!result.val) {
throw new ServiceValidationError(
`Could not resolve queue data for queue ${params.queueName}`,
404
);
}
return result.val;
}
async #resolveTaskRunExecutionDeployment(
backgroundWorkerTaskId: string
): Promise<TaskRunExecutionDeployment | undefined> {
const result = await this.cache.deployments.swr(backgroundWorkerTaskId, async () => {
const { worker } = await this.$.prisma.backgroundWorkerTask.findFirstOrThrow({
where: { id: backgroundWorkerTaskId },
select: {
worker: {
select: {
deployment: true,
},
},
},
});
if (!worker.deployment) {
return undefined;
}
return {
id: worker.deployment.friendlyId,
shortCode: worker.deployment.shortCode,
version: worker.deployment.version,
runtime: worker.deployment.runtime ?? "unknown",
runtimeVersion: worker.deployment.runtimeVersion ?? "unknown",
git: safeParseGitMeta(worker.deployment.git),
};
});
if (result.err) {
throw result.err;
}
return result.val;
}
async #notifyMetadataUpdated(runId: string, completion: TaskRunExecutionResult) {
@@ -1386,3 +1840,11 @@ export class RunAttemptSystem {
}
}
}
export function safeParseGitMeta(git: unknown): GitMeta | undefined {
const parsed = GitMeta.safeParse(git);
if (parsed.success) {
return parsed.data;
}
return undefined;
}
@@ -639,7 +639,7 @@ describe("RunEngine heartbeats", () => {
containerTest("Heartbeat keeps run alive", async ({ prisma, redisOptions }) => {
const authenticatedEnvironment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");
const executingTimeout = 100;
const executingTimeout = 500;
const engine = new RunEngine({
prisma,
@@ -726,24 +726,23 @@ describe("RunEngine heartbeats", () => {
expect(executionData.snapshot.executionStatus).toBe("EXECUTING");
expect(executionData.run.status).toBe("EXECUTING");
// Send heartbeats every 50ms (half the timeout)
for (let i = 0; i < 6; i++) {
await setTimeout(50);
// Send heartbeats every 100ms (to make sure we're not timing out)
for (let i = 0; i < 5; i++) {
await setTimeout(100);
await engine.heartbeatRun({
runId: run.id,
snapshotId: attempt.snapshot.id,
});
}
// After 300ms (3x the timeout) the run should still be executing
// because we've been sending heartbeats
// Should still be executing because we're sending heartbeats
const executionData2 = await engine.getRunExecutionData({ runId: run.id });
assertNonNullable(executionData2);
expect(executionData2.snapshot.executionStatus).toBe("EXECUTING");
expect(executionData2.run.status).toBe("EXECUTING");
// Stop sending heartbeats and wait for timeout
await setTimeout(executingTimeout * 3);
await setTimeout(executingTimeout * 2);
// Now it should have timed out and be queued
const executionData3 = await engine.getRunExecutionData({ runId: run.id });
@@ -58,6 +58,9 @@ export type RunEngineOptions = {
automaticExtensionThreshold?: number;
retryConfig?: LockRetryConfig;
};
cache?: {
redis: RedisOptions;
};
/** If not set then checkpoints won't ever be used */
retryWarmStartThresholdMs?: number;
heartbeatTimeoutsMs?: Partial<HeartbeatTimeouts>;
@@ -1,7 +1,12 @@
import { createRedisClient, Redis, type RedisOptions } from "@internal/redis";
import { startSpan, type Tracer } from "@internal/tracing";
import { createCache, DefaultStatefulContext, Namespace, Cache as UnkeyCache } from "@unkey/cache";
import { MemoryStore } from "@unkey/cache/stores";
import {
createCache,
DefaultStatefulContext,
Namespace,
type UnkeyCache,
MemoryStore,
} from "@internal/cache";
import { randomUUID } from "crypto";
import seedrandom from "seedrandom";
import {
@@ -484,6 +484,8 @@ const zodIpc = new ZodIpcConnection({
}
runMetadataManager.runId = execution.run.id;
runMetadataManager.runIdIsRoot = typeof execution.run.rootTaskRunId === "undefined";
_executionCount++;
const executor = new TaskExecutor(task, {
@@ -503,6 +505,11 @@ const zodIpc = new ZodIpcConnection({
getNumberEnvVar("TRIGGER_RUN_METADATA_FLUSH_INTERVAL", 1000)
);
devUsageManager.setInitialState({
cpuTime: execution.run.durationMs ?? 0,
costInCents: execution.run.costInCents ?? 0,
});
_executionMeasurement = usage.start();
const timeoutController = timeout.abortAfterTimeout(execution.run.maxDuration);
@@ -328,12 +328,17 @@ const zodIpc = new ZodIpcConnection({
resetExecutionEnvironment();
initializeUsageManager({
const prodManager = initializeUsageManager({
usageIntervalMs: getEnvVar("USAGE_HEARTBEAT_INTERVAL_MS"),
usageEventUrl: getEnvVar("USAGE_EVENT_URL"),
triggerJWT: getEnvVar("TRIGGER_JWT"),
});
prodManager.setInitialState({
cpuTime: execution.run.durationMs ?? 0,
costInCents: execution.run.costInCents ?? 0,
});
standardRunTimelineMetricsManager.registerMetricsFromExecution(metrics, isWarmStart);
console.log(`[${new Date().toISOString()}] Received EXECUTE_TASK_RUN`, execution);
@@ -483,6 +488,7 @@ const zodIpc = new ZodIpcConnection({
}
runMetadataManager.runId = execution.run.id;
runMetadataManager.runIdIsRoot = typeof execution.run.rootTaskRunId === "undefined";
_executionCount++;
const executor = new TaskExecutor(task, {
@@ -689,6 +695,8 @@ function initializeUsageManager({
usage.setGlobalUsageManager(prodUsageManager);
timeout.setGlobalManager(new UsageTimeoutManager(devUsageManager));
return prodUsageManager;
}
_sharedWorkerRuntime = new SharedRuntimeManager(zodIpc, true);
@@ -95,6 +95,7 @@ export const SnapshotId = new IdUtil("snapshot");
export const WaitpointId = new IdUtil("waitpoint");
export const BatchId = new IdUtil("batch");
export const BulkActionId = new IdUtil("bulk");
export const AttemptId = new IdUtil("attempt");
export class IdGenerator {
private alphabet: string;
+88 -2
View File
@@ -24,6 +24,7 @@ export class StandardMetadataManager implements RunMetadataManager {
private queuedRootOperations: Set<RunMetadataChangeOperation> = new Set();
public runId: string | undefined;
public runIdIsRoot: boolean = false;
constructor(
private apiClient: ApiClient,
@@ -38,6 +39,7 @@ export class StandardMetadataManager implements RunMetadataManager {
this.activeStreams.clear();
this.store = undefined;
this.runId = undefined;
this.runIdIsRoot = false;
if (this.flushTimeoutId) {
clearTimeout(this.flushTimeoutId);
@@ -54,34 +56,76 @@ export class StandardMetadataManager implements RunMetadataManager {
// Create the updater object and store it in a local variable
const parentUpdater: RunMetadataUpdater = {
set: (key, value) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.set(key, value);
}
self.queuedParentOperations.add({ type: "set", key, value });
return parentUpdater;
},
del: (key) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.del(key);
}
self.queuedParentOperations.add({ type: "delete", key });
return parentUpdater;
},
append: (key, value) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.append(key, value);
}
self.queuedParentOperations.add({ type: "append", key, value });
return parentUpdater;
},
remove: (key, value) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.remove(key, value);
}
self.queuedParentOperations.add({ type: "remove", key, value });
return parentUpdater;
},
increment: (key, value) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.increment(key, value);
}
self.queuedParentOperations.add({ type: "increment", key, value });
return parentUpdater;
},
decrement: (key, value) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.decrement(key, value);
}
self.queuedParentOperations.add({ type: "increment", key, value: -Math.abs(value) });
return parentUpdater;
},
update: (value) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.update(value);
}
self.queuedParentOperations.add({ type: "update", value });
return parentUpdater;
},
stream: (key, value, signal) => self.doStream(key, value, "parent", parentUpdater, signal),
stream: (key, value, signal) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.doStream(key, value, "self", parentUpdater, signal);
}
return self.doStream(key, value, "parent", parentUpdater, signal);
},
};
return parentUpdater;
@@ -94,34 +138,76 @@ export class StandardMetadataManager implements RunMetadataManager {
// Create the updater object and store it in a local variable
const rootUpdater: RunMetadataUpdater = {
set: (key, value) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.set(key, value);
}
self.queuedRootOperations.add({ type: "set", key, value });
return rootUpdater;
},
del: (key) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.del(key);
}
self.queuedRootOperations.add({ type: "delete", key });
return rootUpdater;
},
append: (key, value) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.append(key, value);
}
self.queuedRootOperations.add({ type: "append", key, value });
return rootUpdater;
},
remove: (key, value) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.remove(key, value);
}
self.queuedRootOperations.add({ type: "remove", key, value });
return rootUpdater;
},
increment: (key, value) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.increment(key, value);
}
self.queuedRootOperations.add({ type: "increment", key, value });
return rootUpdater;
},
decrement: (key, value) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.decrement(key, value);
}
self.queuedRootOperations.add({ type: "increment", key, value: -Math.abs(value) });
return rootUpdater;
},
update: (value) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.update(value);
}
self.queuedRootOperations.add({ type: "update", value });
return rootUpdater;
},
stream: (key, value, signal) => self.doStream(key, value, "root", rootUpdater, signal),
stream: (key, value, signal) => {
// We have to check runIdIsRoot here because parent/root are executed before runIdIsRoot is set
if (self.runIdIsRoot) {
return self.doStream(key, value, "self", rootUpdater, signal);
}
return self.doStream(key, value, "root", rootUpdater, signal);
},
};
return rootUpdater;
+132 -46
View File
@@ -219,50 +219,20 @@ export const TaskRun = z.object({
version: z.string().optional(),
metadata: z.record(DeserializedJsonSchema).optional(),
maxDuration: z.number().optional(),
/** @deprecated */
context: z.any(),
/**
* @deprecated For live values use the `usage` SDK functions
* @link https://trigger.dev/docs/run-usage
*/
durationMs: z.number().default(0),
/**
* @deprecated For live values use the `usage` SDK functions
* @link https://trigger.dev/docs/run-usage
*/
costInCents: z.number().default(0),
/**
* @deprecated For live values use the `usage` SDK functions
* @link https://trigger.dev/docs/run-usage
*/
baseCostInCents: z.number().default(0),
/** The priority of the run. Wih a value of 10 it will be dequeued before runs that were triggered 9 seconds before it (assuming they had no priority set). */
priority: z.number().optional(),
baseCostInCents: z.number().optional(),
parentTaskRunId: z.string().optional(),
rootTaskRunId: z.string().optional(),
// These are only used during execution, not in run.ctx
durationMs: z.number().optional(),
costInCents: z.number().optional(),
});
export type TaskRun = z.infer<typeof TaskRun>;
export const TaskRunExecutionTask = z.object({
id: z.string(),
filePath: z.string(),
exportName: z.string().optional(),
});
export type TaskRunExecutionTask = z.infer<typeof TaskRunExecutionTask>;
export const TaskRunExecutionAttempt = z.object({
number: z.number(),
startedAt: z.coerce.date(),
/** @deprecated */
id: z.string(),
/** @deprecated */
backgroundWorkerId: z.string(),
/** @deprecated */
backgroundWorkerTaskId: z.string(),
/** @deprecated */
status: z.string(),
});
export const GitMeta = z.object({
commitAuthorName: z.string().optional(),
commitMessage: z.string().optional(),
@@ -277,6 +247,18 @@ export const GitMeta = z.object({
export type GitMeta = z.infer<typeof GitMeta>;
export const TaskRunExecutionTask = z.object({
id: z.string(),
filePath: z.string(),
});
export type TaskRunExecutionTask = z.infer<typeof TaskRunExecutionTask>;
export const TaskRunExecutionAttempt = z.object({
number: z.number(),
startedAt: z.coerce.date(),
});
export type TaskRunExecutionAttempt = z.infer<typeof TaskRunExecutionAttempt>;
export const TaskRunExecutionEnvironment = z.object({
@@ -317,40 +299,144 @@ export const TaskRunExecutionBatch = z.object({
id: z.string(),
});
export const TaskRunExecution = z.object({
export const TaskRunExecutionDeployment = z.object({
id: z.string(),
shortCode: z.string(),
version: z.string(),
runtime: z.string(),
runtimeVersion: z.string(),
git: GitMeta.optional(),
});
export type TaskRunExecutionDeployment = z.infer<typeof TaskRunExecutionDeployment>;
const StaticTaskRunExecutionShape = {
task: TaskRunExecutionTask,
queue: TaskRunExecutionQueue,
environment: TaskRunExecutionEnvironment,
organization: TaskRunExecutionOrganization,
project: TaskRunExecutionProject,
machine: MachinePreset,
batch: TaskRunExecutionBatch.optional(),
deployment: TaskRunExecutionDeployment.optional(),
};
export const StaticTaskRunExecution = z.object(StaticTaskRunExecutionShape);
export type StaticTaskRunExecution = z.infer<typeof StaticTaskRunExecution>;
export const TaskRunExecution = z.object({
attempt: TaskRunExecutionAttempt,
run: TaskRun.and(
z.object({
traceContext: z.record(z.unknown()).optional(),
})
),
...StaticTaskRunExecutionShape,
});
export type TaskRunExecution = z.infer<typeof TaskRunExecution>;
export const V3TaskRunExecutionTask = z.object({
id: z.string(),
filePath: z.string(),
exportName: z.string().optional(),
});
export type V3TaskRunExecutionTask = z.infer<typeof V3TaskRunExecutionTask>;
export const V3TaskRunExecutionAttempt = z.object({
number: z.number(),
startedAt: z.coerce.date(),
id: z.string(),
backgroundWorkerId: z.string(),
backgroundWorkerTaskId: z.string(),
status: z.string(),
});
export type V3TaskRunExecutionAttempt = z.infer<typeof V3TaskRunExecutionAttempt>;
export const V3TaskRun = z.object({
id: z.string(),
payload: z.string(),
payloadType: z.string(),
tags: z.array(z.string()),
isTest: z.boolean().default(false),
createdAt: z.coerce.date(),
startedAt: z.coerce.date().default(() => new Date()),
idempotencyKey: z.string().optional(),
maxAttempts: z.number().optional(),
version: z.string().optional(),
metadata: z.record(DeserializedJsonSchema).optional(),
maxDuration: z.number().optional(),
context: z.unknown(),
durationMs: z.number(),
costInCents: z.number(),
baseCostInCents: z.number(),
});
export type V3TaskRun = z.infer<typeof V3TaskRun>;
export const V3TaskRunExecution = z.object({
task: V3TaskRunExecutionTask,
attempt: V3TaskRunExecutionAttempt,
run: V3TaskRun.and(
z.object({
traceContext: z.record(z.unknown()).optional(),
})
),
queue: TaskRunExecutionQueue,
environment: TaskRunExecutionEnvironment,
organization: TaskRunExecutionOrganization,
project: TaskRunExecutionProject,
batch: TaskRunExecutionBatch.optional(),
machine: MachinePreset,
batch: TaskRunExecutionBatch.optional(),
});
export type TaskRunExecution = z.infer<typeof TaskRunExecution>;
export type V3TaskRunExecution = z.infer<typeof V3TaskRunExecution>;
export const TaskRunContext = z.object({
task: TaskRunExecutionTask,
attempt: TaskRunExecutionAttempt.omit({
attempt: TaskRunExecutionAttempt,
run: TaskRun.omit({
payload: true,
payloadType: true,
metadata: true,
durationMs: true,
costInCents: true,
}),
...StaticTaskRunExecutionShape,
});
export type TaskRunContext = z.infer<typeof TaskRunContext>;
export const V3TaskRunExecutionEnvironment = z.object({
id: z.string(),
slug: z.string(),
type: z.enum(["PRODUCTION", "STAGING", "DEVELOPMENT", "PREVIEW"]),
});
export type V3TaskRunExecutionEnvironment = z.infer<typeof V3TaskRunExecutionEnvironment>;
export const V3TaskRunContext = z.object({
attempt: V3TaskRunExecutionAttempt.omit({
backgroundWorkerId: true,
backgroundWorkerTaskId: true,
}),
run: TaskRun.omit({ payload: true, payloadType: true, metadata: true }),
run: V3TaskRun.omit({
payload: true,
payloadType: true,
metadata: true,
}),
task: V3TaskRunExecutionTask,
queue: TaskRunExecutionQueue,
environment: TaskRunExecutionEnvironment,
environment: V3TaskRunExecutionEnvironment,
organization: TaskRunExecutionOrganization,
project: TaskRunExecutionProject,
batch: TaskRunExecutionBatch.optional(),
machine: MachinePreset.optional(),
});
export type TaskRunContext = z.infer<typeof TaskRunContext>;
export type V3TaskRunContext = z.infer<typeof V3TaskRunContext>;
export const TaskRunExecutionRetry = z.object({
timestamp: z.number(),
+11 -10
View File
@@ -6,12 +6,13 @@ import {
TaskRunExecutionResult,
TaskRunFailedExecutionResult,
TaskRunInternalError,
V3TaskRunExecution,
} from "./common.js";
import { TaskResource } from "./resources.js";
import {
EnvironmentType,
ProdTaskRunExecution,
ProdTaskRunExecutionPayload,
V3ProdTaskRunExecution,
V3ProdTaskRunExecutionPayload,
RunEngineVersionSchema,
TaskRunExecutionLazyAttemptPayload,
TaskRunExecutionMetrics,
@@ -83,7 +84,7 @@ export const BackgroundWorkerClientMessages = z.discriminatedUnion("type", [
version: z.literal("v1").default("v1"),
type: z.literal("TASK_RUN_COMPLETED"),
completion: TaskRunExecutionResult,
execution: TaskRunExecution,
execution: V3TaskRunExecution,
}),
z.object({
version: z.literal("v1").default("v1"),
@@ -368,7 +369,7 @@ export const CoordinatorToPlatformMessages = {
}),
z.object({
success: z.literal(true),
executionPayload: ProdTaskRunExecutionPayload,
executionPayload: V3ProdTaskRunExecutionPayload,
}),
]),
},
@@ -385,7 +386,7 @@ export const CoordinatorToPlatformMessages = {
}),
z.object({
success: z.literal(true),
payload: ProdTaskRunExecutionPayload,
payload: V3ProdTaskRunExecutionPayload,
}),
]),
},
@@ -417,7 +418,7 @@ export const CoordinatorToPlatformMessages = {
TASK_RUN_COMPLETED: {
message: z.object({
version: z.enum(["v1", "v2"]).default("v1"),
execution: ProdTaskRunExecution,
execution: V3ProdTaskRunExecution,
completion: TaskRunExecutionResult,
checkpoint: z
.object({
@@ -430,7 +431,7 @@ export const CoordinatorToPlatformMessages = {
TASK_RUN_COMPLETED_WITH_ACK: {
message: z.object({
version: z.enum(["v1", "v2"]).default("v2"),
execution: ProdTaskRunExecution,
execution: V3ProdTaskRunExecution,
completion: TaskRunExecutionResult,
checkpoint: z
.object({
@@ -720,7 +721,7 @@ export const ProdWorkerToCoordinatorMessages = {
TASK_RUN_COMPLETED: {
message: z.object({
version: z.enum(["v1", "v2"]).default("v1"),
execution: ProdTaskRunExecution,
execution: V3ProdTaskRunExecution,
completion: TaskRunExecutionResult,
}),
callback: z.object({
@@ -792,7 +793,7 @@ export const ProdWorkerToCoordinatorMessages = {
}),
z.object({
success: z.literal(true),
executionPayload: ProdTaskRunExecutionPayload,
executionPayload: V3ProdTaskRunExecutionPayload,
}),
]),
},
@@ -835,7 +836,7 @@ export const CoordinatorToProdWorkerMessages = {
EXECUTE_TASK_RUN: {
message: z.object({
version: z.literal("v1").default("v1"),
executionPayload: ProdTaskRunExecutionPayload,
executionPayload: V3ProdTaskRunExecutionPayload,
}),
},
EXECUTE_TASK_RUN_LAZY_ATTEMPT: {
+12 -6
View File
@@ -1,6 +1,12 @@
import { z } from "zod";
import { RequireKeys } from "../types/index.js";
import { MachineConfig, MachinePreset, MachinePresetName, TaskRunExecution } from "./common.js";
import {
MachineConfig,
MachinePreset,
MachinePresetName,
TaskRunExecution,
V3TaskRunExecution,
} from "./common.js";
/*
WARNING: Never import anything from ./messages here. If it's needed in both, put it here instead.
@@ -36,7 +42,7 @@ export type TaskRunExecutionPayload = z.infer<typeof TaskRunExecutionPayload>;
// Strategies for not breaking backwards compatibility:
// 1. Add new fields as optional
// 2. If a field is required, add a default value
export const ProdTaskRunExecution = TaskRunExecution.extend({
export const V3ProdTaskRunExecution = V3TaskRunExecution.extend({
worker: z.object({
id: z.string(),
contentHash: z.string(),
@@ -46,16 +52,16 @@ export const ProdTaskRunExecution = TaskRunExecution.extend({
machine: MachinePreset.default({ name: "small-1x", cpu: 1, memory: 1, centsPerMs: 0 }),
});
export type ProdTaskRunExecution = z.infer<typeof ProdTaskRunExecution>;
export type V3ProdTaskRunExecution = z.infer<typeof V3ProdTaskRunExecution>;
export const ProdTaskRunExecutionPayload = z.object({
execution: ProdTaskRunExecution,
export const V3ProdTaskRunExecutionPayload = z.object({
execution: V3ProdTaskRunExecution,
traceContext: z.record(z.unknown()),
environment: z.record(z.string()).optional(),
metrics: TaskRunExecutionMetrics.optional(),
});
export type ProdTaskRunExecutionPayload = z.infer<typeof ProdTaskRunExecutionPayload>;
export type V3ProdTaskRunExecutionPayload = z.infer<typeof V3ProdTaskRunExecutionPayload>;
export const FixedWindowRateLimit = z.object({
type: z.literal("fixed-window"),
@@ -82,11 +82,9 @@ export class TaskContextAPI {
get contextAttributes(): Attributes {
if (this.ctx) {
return {
[SemanticInternalAttributes.ATTEMPT_ID]: this.ctx.attempt.id,
[SemanticInternalAttributes.ATTEMPT_NUMBER]: this.ctx.attempt.number,
[SemanticInternalAttributes.TASK_SLUG]: this.ctx.task.id,
[SemanticInternalAttributes.TASK_PATH]: this.ctx.task.filePath,
[SemanticInternalAttributes.TASK_EXPORT_NAME]: this.ctx.task.exportName,
[SemanticInternalAttributes.QUEUE_NAME]: this.ctx.queue.name,
[SemanticInternalAttributes.QUEUE_ID]: this.ctx.queue.id,
[SemanticInternalAttributes.RUN_ID]: this.ctx.run.id,
+5 -1
View File
@@ -1,7 +1,7 @@
const API_NAME = "usage";
import { getGlobal, registerGlobal, unregisterGlobal } from "../utils/globals.js";
import type { UsageManager, UsageMeasurement, UsageSample } from "./types.js";
import type { InitialUsageState, UsageManager, UsageMeasurement, UsageSample } from "./types.js";
import { NoopUsageManager } from "./noopUsageManager.js";
const NOOP_USAGE_MANAGER = new NoopUsageManager();
@@ -53,6 +53,10 @@ export class UsageAPI implements UsageManager {
this.disable();
}
public getInitialState(): InitialUsageState {
return this.#getUsageManager().getInitialState();
}
#getUsageManager(): UsageManager {
return getGlobal(API_NAME) ?? NOOP_USAGE_MANAGER;
}
+17 -1
View File
@@ -1,4 +1,4 @@
import { UsageManager, UsageMeasurement, UsageSample } from "./types.js";
import { InitialUsageState, UsageManager, UsageMeasurement, UsageSample } from "./types.js";
import { clock } from "../clock-api.js";
import { ClockTime, calculateDurationInMs } from "../clock/clock.js";
@@ -45,6 +45,10 @@ export class DevUsageManager implements UsageManager {
private _firstMeasurement?: DevUsageMeasurement;
private _currentMeasurements: Map<string, DevUsageMeasurement> = new Map();
private _pauses: Map<string, { start: ClockTime; end?: ClockTime }> = new Map();
private _initialState: InitialUsageState = {
cpuTime: 0,
costInCents: 0,
};
disable(): void {}
@@ -54,6 +58,18 @@ export class DevUsageManager implements UsageManager {
this._firstMeasurement = undefined;
this._currentMeasurements.clear();
this._pauses.clear();
this._initialState = {
cpuTime: 0,
costInCents: 0,
};
}
setInitialState(state: InitialUsageState) {
this._initialState = state;
}
getInitialState(): InitialUsageState {
return this._initialState;
}
sample(): UsageSample | undefined {
@@ -1,4 +1,4 @@
import { UsageManager, UsageMeasurement, UsageSample } from "./types.js";
import { InitialUsageState, UsageManager, UsageMeasurement, UsageSample } from "./types.js";
export class NoopUsageManager implements UsageManager {
disable(): void {
@@ -30,4 +30,11 @@ export class NoopUsageManager implements UsageManager {
reset(): void {
// Noop
}
getInitialState(): InitialUsageState {
return {
cpuTime: 0,
costInCents: 0,
};
}
}
+17 -1
View File
@@ -1,5 +1,5 @@
import { setInterval } from "node:timers/promises";
import { UsageManager, UsageMeasurement, UsageSample } from "./types.js";
import { InitialUsageState, UsageManager, UsageMeasurement, UsageSample } from "./types.js";
import { UsageClient } from "./usageClient.js";
export type ProdUsageManagerOptions = {
@@ -13,6 +13,10 @@ export class ProdUsageManager implements UsageManager {
private _abortController: AbortController | undefined;
private _lastSample: UsageSample | undefined;
private _usageClient: UsageClient | undefined;
private _initialState: InitialUsageState = {
cpuTime: 0,
costInCents: 0,
};
constructor(
private readonly delegageUsageManager: UsageManager,
@@ -27,6 +31,14 @@ export class ProdUsageManager implements UsageManager {
return typeof this._usageClient !== "undefined";
}
setInitialState(state: InitialUsageState) {
this._initialState = state;
}
getInitialState(): InitialUsageState {
return this._initialState;
}
reset(): void {
this.delegageUsageManager.reset();
this._abortController?.abort();
@@ -34,6 +46,10 @@ export class ProdUsageManager implements UsageManager {
this._usageClient = undefined;
this._measurement = undefined;
this._lastSample = undefined;
this._initialState = {
cpuTime: 0,
costInCents: 0,
};
}
disable(): void {
+6
View File
@@ -7,8 +7,14 @@ export interface UsageMeasurement {
sample(): UsageSample;
}
export type InitialUsageState = {
cpuTime: number;
costInCents: number;
};
export interface UsageManager {
disable(): void;
getInitialState(): InitialUsageState;
start(): UsageMeasurement;
stop(measurement: UsageMeasurement): UsageSample;
sample(): UsageSample | undefined;
-1
View File
@@ -1313,7 +1313,6 @@ async function triggerAndWait_internal<TIdentifier extends string, TPayload, TOu
{
payload: payloadPacket.data,
options: {
dependentAttempt: ctx.attempt.id,
lockToVersion: taskContext.worker?.version, // Lock to current version because we're waiting for it to finish
queue: options?.queue ? { name: options.queue } : undefined,
concurrencyKey: options?.concurrencyKey,
+7 -6
View File
@@ -54,6 +54,7 @@ export const usage = {
*/
getCurrent: (): CurrentUsage => {
const sample = usageApi.sample();
const initialState = usageApi.getInitialState();
const machine = taskContext.ctx?.machine;
const run = taskContext.ctx?.run;
@@ -65,12 +66,12 @@ export const usage = {
durationMs: 0,
},
total: {
costInCents: run?.costInCents ?? 0,
durationMs: run?.durationMs ?? 0,
costInCents: initialState.costInCents,
durationMs: initialState.cpuTime,
},
},
baseCostInCents: run?.baseCostInCents ?? 0,
totalCostInCents: (run?.costInCents ?? 0) + (run?.baseCostInCents ?? 0),
totalCostInCents: initialState.costInCents + (run?.baseCostInCents ?? 0),
};
}
@@ -83,12 +84,12 @@ export const usage = {
durationMs: sample.cpuTime,
},
total: {
costInCents: (run?.costInCents ?? 0) + currentCostInCents,
durationMs: (run?.durationMs ?? 0) + sample.cpuTime,
costInCents: currentCostInCents + initialState.costInCents,
durationMs: sample.cpuTime + initialState.cpuTime,
},
},
baseCostInCents: run?.baseCostInCents ?? 0,
totalCostInCents: (run?.costInCents ?? 0) + currentCostInCents + (run?.baseCostInCents ?? 0),
totalCostInCents: currentCostInCents + (run?.baseCostInCents ?? 0) + initialState.costInCents,
};
},
/**
+21 -3
View File
@@ -921,6 +921,24 @@ importers:
docs: {}
internal-packages/cache:
dependencies:
'@internal/redis':
specifier: workspace:*
version: link:../redis
'@trigger.dev/core':
specifier: workspace:*
version: link:../../packages/core
'@unkey/cache':
specifier: ^1.5.0
version: 1.5.0
'@unkey/error':
specifier: ^0.2.0
version: 0.2.0
superjson:
specifier: ^2.2.1
version: 2.2.1
internal-packages/clickhouse:
dependencies:
'@clickhouse/client':
@@ -1054,6 +1072,9 @@ importers:
internal-packages/run-engine:
dependencies:
'@internal/cache':
specifier: workspace:*
version: link:../cache
'@internal/redis':
specifier: workspace:*
version: link:../redis
@@ -1069,9 +1090,6 @@ importers:
'@trigger.dev/redis-worker':
specifier: workspace:*
version: link:../../packages/redis-worker
'@unkey/cache':
specifier: ^1.5.0
version: 1.5.0
assert-never:
specifier: ^1.2.1
version: 1.2.1
@@ -57,7 +57,7 @@ export const parentTask = task({
id: "parent",
machine: "medium-1x",
run: async (payload: any, { ctx }) => {
logger.log("Hello, world from the parent", { payload });
logger.log("Hello, world from the parent", { payload, ctx });
await childTask.triggerAndWait({ message: "Hello, world!", aReallyBigInt: BigInt(10000) });
},
});
@@ -107,7 +107,7 @@ export const childTask = task({
}: { message?: string; failureChance?: number; duration?: number; aReallyBigInt?: bigint },
{ ctx }
) => {
logger.info("Hello, world from the child", { message, failureChance, aReallyBigInt });
logger.info("Hello, world from the child", { ctx, failureChance, aReallyBigInt });
if (Math.random() < failureChance) {
throw new Error("Random error at start");
@@ -44,6 +44,8 @@ export const parentTask = task({
metadata.parent.set("test.parent.set", true);
metadata.set("test.set", "test");
logger.info("logging metadata.current()", { current: metadata.current() });
await childTask.triggerAndWait({});
return {
@@ -0,0 +1,35 @@
import { logger, task, wait, usage } from "@trigger.dev/sdk";
import { setTimeout } from "timers/promises";
export const usageExampleTask = task({
id: "usage-example",
retry: {
maxAttempts: 3,
minTimeoutInMs: 500,
maxTimeoutInMs: 1000,
factor: 1.5,
},
run: async (payload: { throwError: boolean }, { ctx }) => {
logger.info("run.ctx", { ctx });
await setTimeout(1000);
const currentUsage = usage.getCurrent();
logger.info("currentUsage", { currentUsage });
if (payload.throwError && ctx.attempt.number === 1) {
throw new Error("Forced error to cause a retry");
}
await setTimeout(5000);
const currentUsage2 = usage.getCurrent();
logger.info("currentUsage2", { currentUsage2 });
return {
message: "Hello, world!",
};
},
});