Compare commits

...

33 Commits

Author SHA1 Message Date
Eric Allam 7e97dcb93e Release 3.0.0-beta.45 2024-07-01 17:17:11 +01:00
github-actions[bot] b9b86c89a7 chore: Update version for release (beta) (#1188)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-07-01 17:16:11 +01:00
nicktrn cd5d2ae92b Override completion on OOM crashes 2024-07-01 13:22:35 +01:00
Eric Allam 0e77e7ef7d v3: Delayed runs and run ttl expiration (#1193)
* v3: Trigger delayed runs and reschedule them

* Create a `@trigger.dev/core/v3/schemas` export

* fixed the `@trigger.dev/core/v3/schemas` export

* Small docs tweak

* Add ttl option when triggering tasks, expire runs after ttl

Dev runs expire in 10m by default
2024-07-01 12:01:35 +01:00
Kai 76a5c6204f fix: prevent filter tooltip from opening on autofocus (#1185)
* Prevent tooltip from opening on autofocus

* Remove autofocus

---------

Co-authored-by: Matt Aitken <matt@mattaitken.com>
2024-07-01 10:50:07 +01:00
nicktrn b171fde483 Fix index failure completion override 2024-06-29 19:23:32 +01:00
nicktrn 5ae3da6b4e v3: various small fixes (#1192)
* Await file watcher cleanup in dev

* Fix artifact detection logs

* Fix next runs table when schedule disabled

* Improve OOM error messages

* Add test link to completed deployment message

* Fix OOM detection, again

* Add changeset
2024-06-29 16:48:01 +01:00
Eric Allam f565829959 v3: Copy over more of the project's package.json keys into the deployed package.json (support for custom config like zenstack) 2024-06-28 20:13:54 +01:00
Eric Allam d57dec6919 Remove e2e tests from CI for now 2024-06-28 16:17:58 +01:00
Eric Allam 75ec4ac6a6 v3: postInstall config option now replaces the postinstall script found in package.json 2024-06-28 15:59:41 +01:00
nicktrn 374b6b9c0c Increase dev worker timeout 2024-06-28 15:56:51 +01:00
nicktrn d0d3a64bd6 v3: misc CLI improvements (#1173)
* prevent downgrades during update check

* detect bun and use npm instead

* detect missing tsconfig during init and print helpful error

* add changeset

* add links to dev worker started message

* allow users to add custom pkg manager args during init

* update changeset

* fix links in unsupported terminals

* deprecate terminalLink

* update changeset
2024-06-28 15:51:17 +01:00
nicktrn 568da01785 v3: improve non-zero exit errors (#1179)
* docker provider will optionally enforce machine presets

* update task monitor oom message

* add oom task to v3 catalog

* improve handling of non-zero exit errors

* add changeset
2024-06-28 15:50:04 +01:00
nicktrn c75e29a9a7 v3: add audio binaries to worker images (#1183)
* add sox

* add audiowaveform

* add changeset
2024-06-28 15:49:24 +01:00
Émile Ré e5d26bd12d Fix infisical-sdk fixture, use in config instead (#1190) 2024-06-28 09:50:24 +01:00
Eric Allam b6f31ab651 Fixed lock file of "Update docs theme (#1189)"
This reverts commit 4cc61ac0ec.
2024-06-27 20:39:39 +01:00
Eric Allam 50d46a8513 Fixed lock file of "Update docs theme (#1189)"
This reverts commit 4cc61ac0ec.
2024-06-27 20:39:24 +01:00
Eric Allam 4cc61ac0ec Revert "Update docs theme (#1189)"
This reverts commit a696359c3e.
2024-06-27 20:31:25 +01:00
James Ritchie a696359c3e Update docs theme (#1189)
* Changed theme to venus and updated primary colours

* Replaced ‘v3 (Developer Preview)’ with ‘v3’

* Removed webhooks and zod from tasks overview and intro

* pnpm lock

* Changed the theme and background + removed the tabs

---------

Co-authored-by: D-K-P <8297864+D-K-P@users.noreply.github.com>
2024-06-27 18:31:47 +01:00
Émile Ré 52b6f48a94 Cli v3 e2e: fixtures (#1184)
* Run tests

* Remove useless comments

* Fix readme

* Add fixtures for config step

* Add fixture for additionalDependencies name parsing scope issue

* Add URL to commit fix

* Some fixtures will only use 1 package manager

* Add resolve-trigger-deps fixture

* use rimraf in test setup

* Add legacy-peer-deps fixture

* Use vitest fixtures

* Add nested peer dep locking issue

* Add missing peer dep resolving fixture

* Get rid of global namespace, fix parallelism

* Add console logs to debug log level

* Add changeset

* Remove comments

* Update README.md
2024-06-27 13:55:54 +01:00
Eric Allam d22a460555 Release 3.0.0-beta.44 2024-06-27 13:41:01 +01:00
github-actions[bot] 9ba2a217a4 chore: Update version for release (beta) (#1187)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-27 13:38:57 +01:00
Eric Allam 39885a427f v3: fix missing init output in task run function when no middleware is defined 2024-06-27 11:49:28 +01:00
Eric Allam ccb0bc510a Release 3.0.0-beta.43 2024-06-27 11:09:27 +01:00
github-actions[bot] 56d66ee07c chore: Update version for release (beta) (#1182)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-27 11:08:30 +01:00
Eric Allam 4ca8887972 v3: fix issue where in progress events weren’t being shown 2024-06-26 16:15:44 +01:00
Eric Allam 89bffc066c Remove the [traceId, startTime] index on TaskEvent for now (not necessary) 2024-06-26 15:32:43 +01:00
Eric Allam 34ca7667d3 Various perf improvements to prevent event loop lag (#1186)
* WIP

* Handle tasks that have failed but are being auto yielded

* Limit trace view to 25k event records, add a download run logs button

Also added two new indexes to TaskEvent:

```
/// Used on eventRepository.getTraceSummary()
  @@index([traceId, startTime])
  // Used for getting all logs for a run
  @@index([runId])
```

* perf improvements on eventRepository.getSpan()

* v2: Add a 5 minute timeout for run execution requests in dev

* v3: Include presigned urls for downloading large payloads and outputs when using runs.retrieve

* v3: better handle large task payloads and outputs

* Change to 512KB

* v2: paginate trigger schedules endpoint

* v3: add 3MB limit on batch and single payloads

* Update task payload and output limits
2024-06-26 15:22:35 +01:00
nicktrn 3e327acc0f Catch zod message sender exceptions 2024-06-26 15:20:33 +01:00
Eric Allam 77ad4127cb v3: Improved ESM module require error detection logic 2024-06-25 09:08:43 +01:00
Eric Allam 8a5076aacf Release 3.0.0-beta.42 2024-06-24 19:52:22 +01:00
github-actions[bot] 5399f6bfb7 chore: Update version for release (beta) (#1181)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-06-24 19:51:48 +01:00
Eric Allam ecef199660 v3: Use global setTimeout to ensure cross-runtime support 2024-06-24 19:49:22 +01:00
263 changed files with 226772 additions and 1390 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Increase dev worker timeout
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Add sox and audiowaveform binaries to worker images
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/core": patch
---
v3: Trigger delayed runs and reschedule them
+9
View File
@@ -0,0 +1,9 @@
---
"trigger.dev": patch
"@trigger.dev/core": patch
---
- Improve non-zero exit code error messages
- Detect OOM conditions within worker child processes
- Internal errors can have optional stack traces
- Docker provider can be set to enforce machine presets
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---
Use global setTimeout to ensure cross-runtime support
+7
View File
@@ -0,0 +1,7 @@
---
"trigger.dev": patch
---
Add e2e fixtures corresponding to past issues
Implement e2e suite parallelism
Enhance log level for specific e2e suite messages
+14
View File
@@ -65,8 +65,10 @@
"eight-pumas-float",
"eleven-paws-join",
"famous-boats-tease",
"fast-colts-relax",
"few-students-share",
"five-toes-destroy",
"friendly-walls-repair",
"funny-swans-destroy",
"gorgeous-gorillas-compete",
"green-bags-wink",
@@ -90,7 +92,9 @@
"mighty-camels-joke",
"mighty-flowers-train",
"mighty-parrots-sin",
"modern-stingrays-end",
"nasty-jars-pump",
"nervous-planets-sparkle",
"new-pants-beg",
"new-rivers-tell",
"nice-bulldogs-turn",
@@ -99,9 +103,12 @@
"pink-pumas-rhyme",
"plenty-ducks-beam",
"polite-ducks-switch",
"polite-pears-grow",
"polite-pots-walk",
"polite-rockets-matter",
"poor-flowers-cross",
"purple-garlics-shop",
"purple-spiders-care",
"rare-lamps-promise",
"rare-roses-float",
"real-planets-stare",
@@ -116,9 +123,11 @@
"sharp-zebras-serve",
"shiny-coats-cry",
"silly-buses-obey",
"silly-forks-kiss",
"silly-suits-switch",
"silver-doors-juggle",
"six-ligers-exist",
"six-rats-hunt",
"sixty-insects-watch",
"slow-buses-own",
"slow-kiwis-hide",
@@ -126,6 +135,7 @@
"smart-needles-move",
"smart-olives-eat",
"sour-pugs-teach",
"spicy-frogs-remain",
"spicy-lamps-smoke",
"spicy-terms-bow",
"strange-ghosts-matter",
@@ -139,10 +149,13 @@
"sweet-lizards-press",
"swift-dragons-peel",
"tall-bees-wave",
"tall-masks-repeat",
"tame-apricots-clap",
"tame-guests-know",
"tender-moose-tell",
"tender-oranges-rhyme",
"tender-turkeys-compete",
"thick-carrots-sneeze",
"thin-parents-heal",
"thirty-islands-kiss",
"tidy-balloons-suffer",
@@ -153,6 +166,7 @@
"tricky-bulldogs-heal",
"tricky-keys-attack",
"tricky-ladybugs-unite",
"twelve-knives-notice",
"two-pumas-wait",
"violet-clocks-notice",
"warm-olives-provide",
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Await file watcher cleanup in dev
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
v3: Copy over more of the project's package.json keys into the deployed package.json (support for custom config like zenstack)
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
Improved ESM module require error detection logic
+10
View File
@@ -0,0 +1,10 @@
---
"trigger.dev": patch
---
- Prevent downgrades during update check and advise to upgrade CLI
- Detect bun and use npm instead
- During init, fail early and advise if not a TypeScript project
- During init, allow specifying custom package manager args
- Add links to dev worker started message
- Fix links in unsupported terminals
+9
View File
@@ -0,0 +1,9 @@
---
"@trigger.dev/core-apps": patch
"trigger.dev": patch
"@trigger.dev/core": patch
---
- Fix artifact detection logs
- Fix OOM detection and error messages
- Add test link to cli deployment completion
+5
View File
@@ -0,0 +1,5 @@
---
"trigger.dev": patch
---
v3: postInstall config option now replaces the postinstall script found in package.json
+6
View File
@@ -0,0 +1,6 @@
---
"@trigger.dev/core": patch
"@trigger.dev/sdk": patch
---
v3: Include presigned urls for downloading large payloads and outputs when using runs.retrieve
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/core": patch
---
v3: fix missing init output in task run function when no middleware is defined
+5 -5
View File
@@ -49,11 +49,11 @@ jobs:
uses: ./.github/workflows/unit-tests.yml
secrets: inherit
e2e:
uses: ./.github/workflows/e2e.yml
with:
package: cli-v3
secrets: inherit
# e2e:
# uses: ./.github/workflows/e2e.yml
# with:
# package: cli-v3
# secrets: inherit
publish:
needs: [typecheck, units]
+20 -15
View File
@@ -109,22 +109,27 @@ class DockerTaskOperations implements TaskOperations {
const containerName = this.#getRunContainerName(opts.runId);
const runArgs = [
"run",
"--network=host",
"--detach",
`--env=TRIGGER_ENV_ID=${opts.envId}`,
`--env=TRIGGER_RUN_ID=${opts.runId}`,
`--env=OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT}`,
`--env=POD_NAME=${containerName}`,
`--env=COORDINATOR_HOST=${COORDINATOR_HOST}`,
`--env=COORDINATOR_PORT=${COORDINATOR_PORT}`,
`--name=${containerName}`,
];
if (process.env.ENFORCE_MACHINE_PRESETS) {
runArgs.push(`--cpus=${opts.machine.cpu}`, `--memory=${opts.machine.memory}G`);
}
runArgs.push(`${opts.image}`);
try {
logger.debug(
await execa("docker", [
"run",
"--network=host",
"--detach",
`--env=TRIGGER_ENV_ID=${opts.envId}`,
`--env=TRIGGER_RUN_ID=${opts.runId}`,
`--env=OTEL_EXPORTER_OTLP_ENDPOINT=${OTEL_EXPORTER_OTLP_ENDPOINT}`,
`--env=POD_NAME=${containerName}`,
`--env=COORDINATOR_HOST=${COORDINATOR_HOST}`,
`--env=COORDINATOR_PORT=${COORDINATOR_PORT}`,
`--name=${containerName}`,
`${opts.image}`,
])
);
logger.debug(await execa("docker", runArgs));
} catch (error) {
if (!isExecaChildProcess(error)) {
throw error;
+9 -8
View File
@@ -529,27 +529,28 @@ provider.listen();
const taskMonitor = new TaskMonitor({
runtimeEnv: RUNTIME_ENV,
onIndexFailure: async (deploymentId, failureInfo) => {
logger.log("Indexing failed", { deploymentId, failureInfo });
onIndexFailure: async (deploymentId, details) => {
logger.log("Indexing failed", { deploymentId, details });
try {
provider.platformSocket.send("INDEXING_FAILED", {
deploymentId,
error: {
name: `Crashed with exit code ${failureInfo.exitCode}`,
message: failureInfo.reason,
stack: failureInfo.logs,
name: `Crashed with exit code ${details.exitCode}`,
message: details.reason,
stack: details.logs,
},
overrideCompletion: details.overrideCompletion,
});
} catch (error) {
logger.error(error);
}
},
onRunFailure: async (runId, failureInfo) => {
logger.log("Run failed:", { runId, failureInfo });
onRunFailure: async (runId, details) => {
logger.log("Run failed:", { runId, details });
try {
provider.platformSocket.send("WORKER_CRASHED", { runId, ...failureInfo });
provider.platformSocket.send("WORKER_CRASHED", { runId, ...details });
} catch (error) {
logger.error(error);
}
+19 -20
View File
@@ -1,25 +1,20 @@
import * as k8s from "@kubernetes/client-node";
import { SimpleLogger } from "@trigger.dev/core-apps";
import { EXIT_CODE_ALREADY_HANDLED, EXIT_CODE_CHILD_NONZERO } from "@trigger.dev/core-apps/process";
import { setTimeout } from "timers/promises";
import PQueue from "p-queue";
import type { Prettify } from "@trigger.dev/core/v3";
type IndexFailureHandler = (
deploymentId: string,
failureInfo: {
exitCode: number;
reason: string;
logs: string;
}
) => Promise<any>;
type FailureDetails = Prettify<{
exitCode: number;
reason: string;
logs: string;
overrideCompletion: boolean;
}>;
type RunFailureHandler = (
runId: string,
failureInfo: {
exitCode: number;
reason: string;
logs: string;
}
) => Promise<any>;
type IndexFailureHandler = (deploymentId: string, details: FailureDetails) => Promise<any>;
type RunFailureHandler = (runId: string, details: FailureDetails) => Promise<any>;
type TaskMonitorOptions = {
runtimeEnv: "local" | "kubernetes";
@@ -144,8 +139,7 @@ export class TaskMonitor {
const containerState = this.#getContainerStateSummary(containerStatus.state);
const exitCode = containerState.exitCode ?? -1;
// We use this special exit code to signal any errors were already handled elsewhere
if (exitCode === 111) {
if (exitCode === EXIT_CODE_ALREADY_HANDLED) {
return;
}
@@ -162,6 +156,7 @@ export class TaskMonitor {
let reason = rawReason || "Unknown error";
let logs = rawLogs || "";
let overrideCompletion = false;
switch (rawReason) {
case "Error":
@@ -181,7 +176,10 @@ export class TaskMonitor {
}
break;
case "OOMKilled":
reason = "Out of memory! Try increasing the memory on this task.";
overrideCompletion = true;
reason = `${
exitCode === EXIT_CODE_CHILD_NONZERO ? "Child process" : "Parent process"
} ran out of memory! Try choosing a machine preset with more memory for this task.`;
break;
default:
break;
@@ -191,7 +189,8 @@ export class TaskMonitor {
exitCode,
reason,
logs,
};
overrideCompletion,
} satisfies FailureDetails;
const app = pod.metadata?.labels?.app;
@@ -146,7 +146,6 @@ function FilterMenu(props: RunFiltersProps) {
const filterTrigger = (
<SelectTrigger
autoFocus
icon={
<div className="flex size-4 items-center justify-center">
<ListFilterIcon className="size-3.5" />
@@ -3,10 +3,12 @@ import {
BoltSlashIcon,
BugAntIcon,
CheckCircleIcon,
ClockIcon,
FireIcon,
NoSymbolIcon,
PauseCircleIcon,
RectangleStackIcon,
TrashIcon,
XCircleIcon,
} from "@heroicons/react/20/solid";
import { TaskRunStatus } from "@trigger.dev/database";
@@ -16,6 +18,7 @@ import { Spinner } from "~/components/primitives/Spinner";
import { cn } from "~/utils/cn";
export const allTaskRunStatuses = [
"DELAYED",
"WAITING_FOR_DEPLOY",
"PENDING",
"EXECUTING",
@@ -28,10 +31,12 @@ export const allTaskRunStatuses = [
"PAUSED",
"INTERRUPTED",
"SYSTEM_FAILURE",
"EXPIRED",
] as const satisfies Readonly<Array<TaskRunStatus>>;
export const filterableTaskRunStatuses = [
"WAITING_FOR_DEPLOY",
"DELAYED",
"PENDING",
"EXECUTING",
"RETRYING_AFTER_FAILURE",
@@ -42,9 +47,11 @@ export const filterableTaskRunStatuses = [
"CRASHED",
"INTERRUPTED",
"SYSTEM_FAILURE",
"EXPIRED",
] as const satisfies Readonly<Array<TaskRunStatus>>;
const taskRunStatusDescriptions: Record<TaskRunStatus, string> = {
DELAYED: "Task has been delayed and is waiting to be executed",
PENDING: "Task is waiting to be executed",
WAITING_FOR_DEPLOY: "Task needs to be deployed first to start executing",
EXECUTING: "Task is currently being executed",
@@ -57,9 +64,10 @@ const taskRunStatusDescriptions: Record<TaskRunStatus, string> = {
SYSTEM_FAILURE: "Task has failed due to a system failure",
PAUSED: "Task has been paused by the user",
CRASHED: "Task has crashed and won't be retried",
EXPIRED: "Task has surpassed its ttl and won't be executed",
};
export const QUEUED_STATUSES: TaskRunStatus[] = ["PENDING", "WAITING_FOR_DEPLOY"];
export const QUEUED_STATUSES: TaskRunStatus[] = ["PENDING", "WAITING_FOR_DEPLOY", "DELAYED"];
export const RUNNING_STATUSES: TaskRunStatus[] = [
"EXECUTING",
@@ -74,6 +82,7 @@ export const FINISHED_STATUSES: TaskRunStatus[] = [
"INTERRUPTED",
"SYSTEM_FAILURE",
"CRASHED",
"EXPIRED",
];
export function descriptionForTaskRunStatus(status: TaskRunStatus): string {
@@ -109,6 +118,8 @@ export function TaskRunStatusIcon({
className: string;
}) {
switch (status) {
case "DELAYED":
return <ClockIcon className={cn(runStatusClassNameColor(status), className)} />;
case "PENDING":
return <RectangleStackIcon className={cn(runStatusClassNameColor(status), className)} />;
case "WAITING_FOR_DEPLOY":
@@ -133,6 +144,8 @@ export function TaskRunStatusIcon({
return <BugAntIcon className={cn(runStatusClassNameColor(status), className)} />;
case "CRASHED":
return <FireIcon className={cn(runStatusClassNameColor(status), className)} />;
case "EXPIRED":
return <TrashIcon className={cn(runStatusClassNameColor(status), className)} />;
default: {
assertNever(status);
@@ -143,6 +156,7 @@ export function TaskRunStatusIcon({
export function runStatusClassNameColor(status: TaskRunStatus): string {
switch (status) {
case "PENDING":
case "DELAYED":
return "text-charcoal-500";
case "WAITING_FOR_DEPLOY":
return "text-amber-500";
@@ -154,6 +168,7 @@ export function runStatusClassNameColor(status: TaskRunStatus): string {
case "PAUSED":
return "text-amber-300";
case "CANCELED":
case "EXPIRED":
return "text-charcoal-500";
case "INTERRUPTED":
return "text-error";
@@ -173,6 +188,8 @@ export function runStatusClassNameColor(status: TaskRunStatus): string {
export function runStatusTitle(status: TaskRunStatus): string {
switch (status) {
case "DELAYED":
return "Delayed";
case "PENDING":
return "Queued";
case "WAITING_FOR_DEPLOY":
@@ -197,6 +214,8 @@ export function runStatusTitle(status: TaskRunStatus): string {
return "System failure";
case "CRASHED":
return "Crashed";
case "EXPIRED":
return "Expired";
default: {
assertNever(status);
}
@@ -118,6 +118,8 @@ export function TaskRunsTable({
<TableHeaderCell>Duration</TableHeaderCell>
<TableHeaderCell>Test</TableHeaderCell>
<TableHeaderCell>Created at</TableHeaderCell>
<TableHeaderCell>Delayed until</TableHeaderCell>
<TableHeaderCell>TTL</TableHeaderCell>
<TableHeaderCell>
<span className="sr-only">Go to page</span>
</TableHeaderCell>
@@ -125,7 +127,7 @@ export function TaskRunsTable({
</TableHeader>
<TableBody>
{total === 0 && !hasFilters ? (
<TableBlankRow colSpan={9}>
<TableBlankRow colSpan={10}>
{!isLoading && <NoRuns title="No runs found" />}
</TableBlankRow>
) : runs.length === 0 ? (
@@ -187,6 +189,10 @@ export function TaskRunsTable({
<TableCell to={path}>
{run.createdAt ? <DateTime date={run.createdAt} /> : ""}
</TableCell>
<TableCell to={path}>
{run.delayUntil ? <DateTime date={run.delayUntil} /> : ""}
</TableCell>
<TableCell to={path}>{run.ttl ?? ""}</TableCell>
<RunActionsCell run={run} path={path} />
</TableRow>
);
+2
View File
@@ -40,6 +40,8 @@ export const TaskRunStatus = {
COMPLETED_WITH_ERRORS: "COMPLETED_WITH_ERRORS",
SYSTEM_FAILURE: "SYSTEM_FAILURE",
CRASHED: "CRASHED",
DELAYED: "DELAYED",
EXPIRED: "EXPIRED",
} as const satisfies Record<TaskRunStatusType, TaskRunStatusType>;
export const JobRunStatus = {
+3
View File
@@ -206,6 +206,9 @@ const EnvironmentSchema = z.object({
USAGE_OPEN_METER_BASE_URL: z.string().optional(),
EVENT_LOOP_MONITOR_ENABLED: z.string().default("1"),
MAXIMUM_LIVE_RELOADING_EVENTS: z.coerce.number().int().default(1000),
MAXIMUM_TRACE_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(25_000),
TASK_PAYLOAD_OFFLOAD_THRESHOLD: z.coerce.number().int().default(524_288), // 512KB
TASK_PAYLOAD_MAXIMUM_SIZE: z.coerce.number().int().default(3_145_728), // 3MB
});
export type Environment = z.infer<typeof EnvironmentSchema>;
+7 -1
View File
@@ -84,7 +84,9 @@ export function createPkApiKeyForEnv(envType: RuntimeEnvironment["type"]) {
return `pk_${envSlug(envType)}_${apiKeyId(20)}`;
}
export function envSlug(environmentType: RuntimeEnvironment["type"]) {
export type EnvSlug = "dev" | "stg" | "prod" | "prev";
export function envSlug(environmentType: RuntimeEnvironment["type"]): EnvSlug {
switch (environmentType) {
case "DEVELOPMENT": {
return "dev";
@@ -100,3 +102,7 @@ export function envSlug(environmentType: RuntimeEnvironment["type"]) {
}
}
}
export function isEnvSlug(maybeSlug: string): maybeSlug is EnvSlug {
return ["dev", "stg", "prod", "prev"].includes(maybeSlug);
}
+2
View File
@@ -118,6 +118,7 @@ export function batchTaskRunItemStatusForRunStatus(
case TaskRunStatus.COMPLETED_WITH_ERRORS:
case TaskRunStatus.SYSTEM_FAILURE:
case TaskRunStatus.CRASHED:
case TaskRunStatus.EXPIRED:
return BatchTaskRunItemStatus.FAILED;
case TaskRunStatus.PENDING:
case TaskRunStatus.WAITING_FOR_DEPLOY:
@@ -125,6 +126,7 @@ export function batchTaskRunItemStatusForRunStatus(
case TaskRunStatus.RETRYING_AFTER_FAILURE:
case TaskRunStatus.EXECUTING:
case TaskRunStatus.PAUSED:
case TaskRunStatus.DELAYED:
return BatchTaskRunItemStatus.PENDING;
default:
assertNever(status);
@@ -1,11 +1,9 @@
import { z } from "zod";
import {
Direction,
FilterableEnvironment,
FilterableStatus,
filterableStatuses,
} from "~/components/runs/RunStatuses";
import { PrismaClient, prisma } from "~/db.server";
import { getUsername } from "~/utils/username";
import { BasePresenter } from "./v3/basePresenter.server";
@@ -29,8 +27,6 @@ const DEFAULT_PAGE_SIZE = 20;
export type RunList = Awaited<ReturnType<RunListPresenter["call"]>>;
export class RunListPresenter extends BasePresenter {
public async call({
userId,
eventId,
@@ -1,27 +1,52 @@
import { User } from "@trigger.dev/database";
import { ScheduleMetadataSchema } from "@trigger.dev/core";
import { PrismaClient, prisma } from "~/db.server";
import { User } from "@trigger.dev/database";
import { Organization } from "~/models/organization.server";
import { Project } from "~/models/project.server";
import { calculateNextScheduledEvent } from "~/services/schedules/nextScheduledEvent.server";
import { BasePresenter } from "./v3/basePresenter.server";
export class ScheduledTriggersPresenter {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
const DEFAULT_PAGE_SIZE = 20;
export class ScheduledTriggersPresenter extends BasePresenter {
public async call({
userId,
projectSlug,
organizationSlug,
direction = "forward",
pageSize = DEFAULT_PAGE_SIZE,
cursor,
}: {
userId: User["id"];
projectSlug: Project["slug"];
organizationSlug: Organization["slug"];
direction?: "forward" | "backward";
pageSize?: number;
cursor?: string;
}) {
const scheduled = await this.#prismaClient.scheduleSource.findMany({
const organization = await this._replica.organization.findFirstOrThrow({
select: {
id: true,
},
where: {
slug: organizationSlug,
members: { some: { userId } },
},
});
// Find the project scoped to the organization
const project = await this._replica.project.findFirstOrThrow({
select: {
id: true,
},
where: {
slug: projectSlug,
organizationId: organization.id,
},
});
const directionMultiplier = direction === "forward" ? 1 : -1;
const scheduled = await this._replica.scheduleSource.findMany({
select: {
id: true,
key: true,
@@ -50,23 +75,50 @@ export class ScheduledTriggersPresenter {
},
},
],
organization: {
slug: organizationSlug,
members: {
some: {
userId,
},
},
},
project: {
slug: projectSlug,
},
projectId: project.id,
},
},
orderBy: [{ id: "desc" }],
//take an extra record to tell if there are more
take: directionMultiplier * (pageSize + 1),
//skip the cursor if there is one
skip: cursor ? 1 : 0,
cursor: cursor
? {
id: cursor,
}
: undefined,
});
const hasMore = scheduled.length > pageSize;
//get cursors for next and previous pages
let next: string | undefined;
let previous: string | undefined;
switch (direction) {
case "forward":
previous = cursor ? scheduled.at(0)?.id : undefined;
if (hasMore) {
next = scheduled[pageSize - 1]?.id;
}
break;
case "backward":
if (hasMore) {
previous = scheduled[1]?.id;
next = scheduled[pageSize]?.id;
} else {
next = scheduled[pageSize - 1]?.id;
}
break;
}
const scheduledToReturn =
direction === "backward" && hasMore
? scheduled.slice(1, pageSize + 1)
: scheduled.slice(0, pageSize);
return {
scheduled: scheduled.map((s) => {
scheduled: scheduledToReturn.map((s) => {
const schedule = ScheduleMetadataSchema.parse(s.schedule);
const nextEventTimestamp = s.active
? calculateNextScheduledEvent(schedule, s.lastEventTimestamp)
@@ -78,6 +130,10 @@ export class ScheduledTriggersPresenter {
nextEventTimestamp,
};
}),
pagination: {
next,
previous,
},
};
}
}
@@ -12,6 +12,7 @@ import {
import { Prisma, TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database";
import assertNever from "assert-never";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { generatePresignedUrl } from "~/v3/r2.server";
import { BasePresenter } from "./basePresenter.server";
export class ApiRetrieveRunPresenter extends BasePresenter {
@@ -44,7 +45,9 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
}
let $payload: any;
let $payloadPresignedUrl: string | undefined;
let $output: any;
let $outputPresignedUrl: string | undefined;
if (showSecretDetails) {
const payloadPacket = await conditionallyImportPacket({
@@ -52,7 +55,19 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
dataType: taskRun.payloadType,
});
$payload = await parsePacket(payloadPacket);
if (
payloadPacket.dataType === "application/store" &&
typeof payloadPacket.data === "string"
) {
$payloadPresignedUrl = await generatePresignedUrl(
env.project.externalRef,
env.slug,
payloadPacket.data,
"GET"
);
} else {
$payload = await parsePacket(payloadPacket);
}
if (taskRun.status === "COMPLETED_SUCCESSFULLY") {
const completedAttempt = taskRun.attempts.find(
@@ -65,7 +80,19 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
dataType: completedAttempt.outputType,
});
$output = await parsePacket(outputPacket);
if (
outputPacket.dataType === "application/store" &&
typeof outputPacket.data === "string"
) {
$outputPresignedUrl = await generatePresignedUrl(
env.project.externalRef,
env.slug,
outputPacket.data,
"GET"
);
} else {
$output = await parsePacket(outputPacket);
}
}
}
}
@@ -84,9 +111,14 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
finishedAt: ApiRetrieveRunPresenter.isStatusFinished(apiStatus)
? taskRun.updatedAt
: undefined,
delayedUntil: taskRun.delayUntil ?? undefined,
payload: $payload,
payloadPresignedUrl: $payloadPresignedUrl,
output: $output,
outputPresignedUrl: $outputPresignedUrl,
isTest: taskRun.isTest,
ttl: taskRun.ttl ?? undefined,
expiredAt: taskRun.expiredAt ?? undefined,
schedule: taskRun.schedule
? {
id: taskRun.schedule.friendlyId,
@@ -142,6 +174,9 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
static apiStatusFromRunStatus(status: TaskRunStatus): RunStatus {
switch (status) {
case "DELAYED": {
return "DELAYED";
}
case "WAITING_FOR_DEPLOY": {
return "WAITING_FOR_DEPLOY";
}
@@ -176,6 +211,9 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
case "COMPLETED_WITH_ERRORS": {
return "FAILED";
}
case "EXPIRED": {
return "EXPIRED";
}
default: {
assertNever(status);
}
@@ -183,7 +221,7 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
}
static apiBooleanHelpersFromRunStatus(status: RunStatus) {
const isQueued = status === "QUEUED" || status === "WAITING_FOR_DEPLOY";
const isQueued = status === "QUEUED" || status === "WAITING_FOR_DEPLOY" || status === "DELAYED";
const isExecuting = status === "EXECUTING" || status === "REATTEMPTING" || status === "FROZEN";
const isCompleted =
status === "COMPLETED" ||
@@ -209,7 +209,10 @@ export class ApiRunListPresenter extends BasePresenter {
updatedAt: new Date(run.updatedAt),
startedAt: run.startedAt ? new Date(run.startedAt) : undefined,
finishedAt: run.finishedAt ? new Date(run.finishedAt) : undefined,
delayedUntil: run.delayUntil ? new Date(run.delayUntil) : undefined,
isTest: run.isTest,
ttl: run.ttl ?? undefined,
expiredAt: run.expiredAt ? new Date(run.expiredAt) : undefined,
env: {
id: run.environment.id,
name: run.environment.slug,
@@ -233,6 +236,8 @@ export class ApiRunListPresenter extends BasePresenter {
static apiStatusToRunStatuses(status: RunStatus): TaskRunStatus[] | TaskRunStatus {
switch (status) {
case "DELAYED":
return "DELAYED";
case "WAITING_FOR_DEPLOY": {
return "WAITING_FOR_DEPLOY";
}
@@ -266,6 +271,9 @@ export class ApiRunListPresenter extends BasePresenter {
case "FAILED": {
return "COMPLETED_WITH_ERRORS";
}
case "EXPIRED": {
return "EXPIRED";
}
default: {
assertNever(status);
}
@@ -158,10 +158,13 @@ export class RunListPresenter extends BasePresenter {
createdAt: Date;
startedAt: Date | null;
lockedAt: Date | null;
delayUntil: Date | null;
updatedAt: Date;
isTest: boolean;
spanId: string;
idempotencyKey: string | null;
ttl: string | null;
expiredAt: Date | null;
}[]
>`
SELECT
@@ -174,11 +177,14 @@ export class RunListPresenter extends BasePresenter {
tr.status AS status,
tr."createdAt" AS "createdAt",
tr."startedAt" AS "startedAt",
tr."delayUntil" AS "delayUntil",
tr."lockedAt" AS "lockedAt",
tr."updatedAt" AS "updatedAt",
tr."isTest" AS "isTest",
tr."spanId" AS "spanId",
tr."idempotencyKey" AS "idempotencyKey"
tr."idempotencyKey" AS "idempotencyKey",
tr."ttl" AS "ttl",
tr."expiredAt" AS "expiredAt"
FROM
${sqlDatabaseSchema}."TaskRun" tr
LEFT JOIN
@@ -283,6 +289,7 @@ export class RunListPresenter extends BasePresenter {
createdAt: run.createdAt.toISOString(),
updatedAt: run.updatedAt.toISOString(),
startedAt: startedAt ? startedAt.toISOString() : undefined,
delayUntil: run.delayUntil ? run.delayUntil.toISOString() : undefined,
hasFinished,
finishedAt: hasFinished ? run.updatedAt.toISOString() : undefined,
isTest: run.isTest,
@@ -294,6 +301,8 @@ export class RunListPresenter extends BasePresenter {
isCancellable: isCancellableRunStatus(run.status),
environment: displayableEnvironment(environment, userId),
idempotencyKey: run.idempotencyKey ? run.idempotencyKey : undefined,
ttl: run.ttl ? run.ttl : undefined,
expiredAt: run.expiredAt ? run.expiredAt.toISOString() : undefined,
};
}),
pagination: {
@@ -2,6 +2,8 @@ import { NoSymbolIcon } from "@heroicons/react/20/solid";
import { CheckCircleIcon, XCircleIcon } from "@heroicons/react/24/solid";
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { ListPagination } from "~/components/ListPagination";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { DateTime } from "~/components/primitives/DateTime";
import { LabelValueStack } from "~/components/primitives/LabelValueStack";
@@ -17,30 +19,38 @@ import {
TableRow,
} from "~/components/primitives/Table";
import { TextLink } from "~/components/primitives/TextLink";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { DirectionSchema } from "~/components/runs/RunStatuses";
import { ScheduledTriggersPresenter } from "~/presenters/ScheduledTriggersPresenter.server";
import { requireUserId } from "~/services/session.server";
import { ProjectParamSchema, docsPath } from "~/utils/pathBuilder";
const SearchSchema = z.object({
cursor: z.string().optional(),
direction: DirectionSchema.optional(),
});
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const userId = await requireUserId(request);
const { organizationSlug, projectParam } = ProjectParamSchema.parse(params);
const url = new URL(request.url);
const s = Object.fromEntries(url.searchParams.entries());
const searchParams = SearchSchema.parse(s);
const presenter = new ScheduledTriggersPresenter();
const data = await presenter.call({
userId,
organizationSlug,
projectSlug: projectParam,
direction: searchParams.direction,
cursor: searchParams.cursor,
});
return typedjson(data);
};
export default function Integrations() {
const { scheduled } = useTypedLoaderData<typeof loader>();
const organization = useOrganization();
const project = useProject();
export default function Route() {
const { scheduled, pagination } = useTypedLoaderData<typeof loader>();
return (
<>
@@ -49,6 +59,10 @@ export default function Integrations() {
expression or an interval.
</Paragraph>
{scheduled.length > 0 && (
<ListPagination list={{ pagination }} className="mt-2 justify-end" />
)}
<Table containerClassName="mt-4">
<TableHeader>
<TableRow>
@@ -23,6 +23,7 @@ import { Paragraph } from "~/components/primitives/Paragraph";
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
import {
Table,
TableBlankRow,
TableBody,
TableCell,
TableHeader,
@@ -180,6 +181,14 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
}
};
function PlaceholderText({ title }: { title: string }) {
return (
<div className="flex items-center justify-center">
<Paragraph className="w-auto">{title}</Paragraph>
</div>
);
}
export default function Page() {
const { schedule } = useTypedLoaderData<typeof loader>();
const location = useLocation();
@@ -252,18 +261,30 @@ export default function Page() {
</TableRow>
</TableHeader>
<TableBody>
{schedule.nextRuns.map((run, index) => (
<TableRow key={index}>
{!isUtc && (
<TableCell>
<DateTime date={run} timeZone={schedule.timezone} />
</TableCell>
)}
<TableCell>
<DateTime date={run} timeZone="UTC" />
</TableCell>
</TableRow>
))}
{schedule.active ? (
schedule.nextRuns.length ? (
schedule.nextRuns.map((run, index) => (
<TableRow key={index}>
{!isUtc && (
<TableCell>
<DateTime date={run} timeZone={schedule.timezone} />
</TableCell>
)}
<TableCell>
<DateTime date={run} timeZone="UTC" />
</TableCell>
</TableRow>
))
) : (
<TableBlankRow colSpan={1}>
<PlaceholderText title="You found a bug" />
</TableBlankRow>
)
) : (
<TableBlankRow colSpan={1}>
<PlaceholderText title="Schedule disabled" />
</TableBlankRow>
)}
</TableBody>
</Table>
</div>
+20 -55
View File
@@ -1,10 +1,8 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { z } from "zod";
import { env } from "~/env.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { r2 } from "~/v3/r2.server";
import { generatePresignedUrl } from "~/v3/r2.server";
const ParamsSchema = z.object({
"*": z.string(),
@@ -26,34 +24,19 @@ export async function action({ request, params }: ActionFunctionArgs) {
const parsedParams = ParamsSchema.parse(params);
const filename = parsedParams["*"];
if (!env.OBJECT_STORE_BASE_URL) {
return json({ error: "Object store base URL is not set" }, { status: 500 });
}
if (!r2) {
return json({ error: "Object store credentials are not set" }, { status: 500 });
}
const url = new URL(env.OBJECT_STORE_BASE_URL);
url.pathname = `/packets/${authenticationResult.environment.project.externalRef}/${authenticationResult.environment.slug}/${filename}`;
url.searchParams.set("X-Amz-Expires", "300"); // 5 minutes
const signed = await r2.sign(
new Request(url, {
method: "PUT",
}),
{
aws: { signQuery: true },
}
const presignedUrl = await generatePresignedUrl(
authenticationResult.environment.project.externalRef,
authenticationResult.environment.slug,
filename,
"PUT"
);
logger.debug("Generated presigned URL", {
url: signed.url,
headers: Object.fromEntries(signed.headers),
});
if (!presignedUrl) {
return json({ error: "Failed to generate presigned URL" }, { status: 500 });
}
// Caller can now use this URL to upload to that object.
return json({ presignedUrl: signed.url });
return json({ presignedUrl });
}
export async function loader({ request, params }: ActionFunctionArgs) {
@@ -67,35 +50,17 @@ export async function loader({ request, params }: ActionFunctionArgs) {
const parsedParams = ParamsSchema.parse(params);
const filename = parsedParams["*"];
if (!env.OBJECT_STORE_BASE_URL) {
return json({ error: "Object store base URL is not set" }, { status: 500 });
}
if (!r2) {
return json({ error: "Object store credentials are not set" }, { status: 500 });
}
const url = new URL(env.OBJECT_STORE_BASE_URL);
url.pathname = `/packets/${authenticationResult.environment.project.externalRef}/${authenticationResult.environment.slug}/${filename}`;
url.searchParams.set("X-Amz-Expires", "300"); // 5 minutes
const signed = await r2.sign(
new Request(url, {
method: request.method,
}),
{
aws: { signQuery: true },
}
const presignedUrl = await generatePresignedUrl(
authenticationResult.environment.project.externalRef,
authenticationResult.environment.slug,
filename,
"GET"
);
logger.debug("Generated presigned URL", {
url: signed.url,
headers: Object.fromEntries(signed.headers),
});
if (!presignedUrl) {
return json({ error: "Failed to generate presigned URL" }, { status: 500 });
}
const getUrl = new URL(url.href);
getUrl.searchParams.delete("X-Amz-Expires");
// Caller can now use this URL to upload to that object.
return json({ presignedUrl: signed.url });
// Caller can now use this URL to fetch that object.
return json({ presignedUrl });
}
@@ -3,6 +3,7 @@ import { PrismaClientOrTransaction, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { startActiveSpan } from "~/v3/tracer.server";
export class CompleteRunTaskService {
#prismaClient: PrismaClientOrTransaction;
@@ -17,76 +18,81 @@ export class CompleteRunTaskService {
id: string,
taskBody: CompleteTaskBodyOutput
): Promise<ServerTask | undefined> {
const existingTask = await this.#prismaClient.task.findUnique({
where: {
id,
},
include: {
run: true,
attempts: {
where: {
status: "PENDING",
},
orderBy: {
number: "desc",
},
take: 1,
return startActiveSpan("CompleteRunTaskService.call", async (span) => {
span.setAttribute("runId", runId);
span.setAttribute("taskId", id);
const existingTask = await this.#prismaClient.task.findUnique({
where: {
id,
},
include: {
run: true,
attempts: {
where: {
status: "PENDING",
},
orderBy: {
number: "desc",
},
take: 1,
},
},
},
});
if (!existingTask) {
return;
}
if (existingTask.runId !== runId) {
return;
}
if (existingTask.run.environmentId !== environment.id) {
return;
}
if (
existingTask.status === "COMPLETED" ||
existingTask.status === "ERRORED" ||
existingTask.status === "CANCELED"
) {
logger.debug("Task already completed", {
existingTask,
});
return taskWithAttemptsToServerTask(existingTask);
}
if (!existingTask) {
return;
}
if (existingTask.attempts.length === 1) {
await this.#prismaClient.taskAttempt.update({
if (existingTask.runId !== runId) {
return;
}
if (existingTask.run.environmentId !== environment.id) {
return;
}
if (
existingTask.status === "COMPLETED" ||
existingTask.status === "ERRORED" ||
existingTask.status === "CANCELED"
) {
logger.debug("Task already completed", {
taskId: id,
});
return taskWithAttemptsToServerTask(existingTask);
}
if (existingTask.attempts.length === 1) {
await this.#prismaClient.taskAttempt.update({
where: {
id: existingTask.attempts[0].id,
},
data: {
status: "COMPLETED",
},
});
}
const updatedTask = await this.#prismaClient.task.update({
where: {
id: existingTask.attempts[0].id,
id,
},
data: {
status: "COMPLETED",
output: taskBody.output as any,
outputIsUndefined: typeof taskBody.output === "undefined",
completedAt: new Date(),
outputProperties: taskBody.properties,
},
include: {
attempts: true,
run: true,
},
});
}
const updatedTask = await this.#prismaClient.task.update({
where: {
id,
},
data: {
status: "COMPLETED",
output: taskBody.output as any,
outputIsUndefined: typeof taskBody.output === "undefined",
completedAt: new Date(),
outputProperties: taskBody.properties,
},
include: {
attempts: true,
run: true,
},
return taskWithAttemptsToServerTask(updatedTask);
});
return taskWithAttemptsToServerTask(updatedTask);
}
}
@@ -9,8 +9,10 @@ import {
import { z } from "zod";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { CompleteRunTaskService } from "./CompleteRunTaskService.server";
import { startActiveSpan } from "~/v3/tracer.server";
import { parseRequestJsonAsync } from "~/utils/parseRequestJson.server";
import { FailRunTaskService } from "../api.v1.runs.$runId.tasks.$id.fail/FailRunTaskService.server";
const ParamsSchema = z.object({
runId: z.string(),
@@ -44,46 +46,51 @@ export async function action({ request, params }: ActionFunctionArgs) {
return json({ error: "Invalid headers" }, { status: 400 });
}
// Check the content size of the request and make sure it's not too large
const contentLength = request.headers.get("content-length");
if (!contentLength || parseInt(contentLength) > 3 * 1024 * 1024) {
const service = new FailRunTaskService();
await service.call(authenticatedEnv, runId, id, {
error: {
message: "Task output is too large. The limit is 3MB",
},
});
return json({ error: "Task output is too large. The limit is 3MB" }, { status: 413 });
}
const { "trigger-version": triggerVersion } = headers.data;
// Now parse the request body
const anyBody = await request.json();
logger.debug("CompleteRunTaskService.call() request body", {
body: anyBody,
runId,
id,
});
const anyBody = await parseRequestJsonAsync(request, { runId });
if (triggerVersion === API_VERSIONS.SERIALIZED_TASK_OUTPUT) {
const body = CompleteTaskBodyV2InputSchema.safeParse(anyBody);
const body = await startActiveSpan("CompleteTaskBodyV2InputSchema.safeParse()", async () => {
return CompleteTaskBodyV2InputSchema.safeParse(anyBody);
});
if (!body.success) {
return json({ error: "Invalid request body" }, { status: 400 });
}
// Make sure the length of the output is less than 3MB
if (body.data.output && body.data.output.length > 3 * 1024 * 1024) {
return json({ error: "Output must be less than 3MB" }, { status: 400 });
}
return await completeRunTask(authenticatedEnv, runId, id, {
...body.data,
output: body.data.output ? (JSON.parse(body.data.output) as any) : undefined,
});
} else {
const body = CompleteTaskBodyInputSchema.safeParse(anyBody);
const body = await startActiveSpan("CompleteTaskBodyInputSchema.safeParse()", async () => {
return CompleteTaskBodyInputSchema.omit({ output: true }).safeParse(anyBody);
});
if (!body.success) {
return json({ error: "Invalid request body" }, { status: 400 });
}
// Make sure the length of the output is less than 3MB
if (JSON.stringify(body.data.output).length > 3 * 1024 * 1024) {
return json({ error: "Output must be less than 3MB" }, { status: 400 });
}
const output = (anyBody as any).output;
return await completeRunTask(authenticatedEnv, runId, id, body.data);
return await completeRunTask(authenticatedEnv, runId, id, { ...body.data, output });
}
}
@@ -98,12 +105,6 @@ async function completeRunTask(
try {
const task = await service.call(environment, runId, id, taskBody);
logger.debug("CompleteRunTaskService.call() response body", {
runId,
id,
task,
});
if (!task) {
return json({ message: "Task not found" }, { status: 404 });
}
@@ -57,10 +57,6 @@ export class FailRunTaskService {
existingTask.status === "ERRORED" ||
existingTask.status === "CANCELED"
) {
logger.debug("Task already completed", {
existingTask,
});
return existingTask;
}
@@ -6,6 +6,8 @@ import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { RunTaskService } from "~/services/tasks/runTask.server";
import { ChangeRequestLazyLoadedCachedTasks } from "./ChangeRequestLazyLoadedCachedTasks.server";
import { startActiveSpan } from "~/v3/tracer.server";
import { parseRequestJsonAsync } from "~/utils/parseRequestJson.server";
const ParamsSchema = z.object({
runId: z.string(),
@@ -17,6 +19,8 @@ const HeadersSchema = z.object({
"x-cached-tasks-cursor": z.string().optional().nullable(),
});
const BodySchema = RunTaskBodyOutputSchema.omit({ params: true });
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
@@ -44,18 +48,26 @@ export async function action({ request, params }: ActionFunctionArgs) {
const { runId } = ParamsSchema.parse(params);
const contentLength = request.headers.get("content-length");
if (!contentLength || parseInt(contentLength) > 3 * 1024 * 1024) {
return json({ error: "Request body too large" }, { status: 413 });
}
// Now parse the request body
const anyBody = await request.json();
const anyBody = await parseRequestJsonAsync(request, { runId });
logger.debug("RunTaskService.call() request body", {
body: anyBody,
runId,
idempotencyKey,
triggerVersion,
cachedTasksCursor,
});
const body = RunTaskBodyOutputSchema.safeParse(anyBody);
const body = await startActiveSpan(
"BodySchema.safeParse",
async () => {
return BodySchema.safeParse(anyBody);
},
{
attributes: {
runId,
},
}
);
if (!body.success) {
return json({ error: "Invalid request body" }, { status: 400 });
@@ -64,12 +76,9 @@ export async function action({ request, params }: ActionFunctionArgs) {
const service = new RunTaskService();
try {
const task = await service.call(runId, idempotencyKey, body.data);
logger.debug("RunTaskService.call() response body", {
runId,
idempotencyKey,
task,
const task = await service.call(runId, idempotencyKey, {
...body.data,
params: (anyBody as any).params,
});
if (!task) {
@@ -84,7 +93,6 @@ export async function action({ request, params }: ActionFunctionArgs) {
logger.debug(
"RunTaskService.call() response migrating with ChangeRequestLazyLoadedCachedTasks",
{
responseBody,
cachedTasksCursor,
}
);
@@ -0,0 +1,85 @@
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { RescheduleRunRequestBody } from "@trigger.dev/core/v3/schemas";
import { z } from "zod";
import { prisma } from "~/db.server";
import { ApiRetrieveRunPresenter } from "~/presenters/v3/ApiRetrieveRunPresenter.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { RescheduleTaskRunService } from "~/v3/services/rescheduleTaskRun.server";
const ParamsSchema = z.object({
runParam: z.string(),
});
export async function action({ request, params }: ActionFunctionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
return { status: 405, body: "Method Not Allowed" };
}
// Authenticate the request
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
return json({ error: "Invalid or missing API Key" }, { status: 401 });
}
const parsed = ParamsSchema.safeParse(params);
if (!parsed.success) {
return json({ error: "Invalid or missing run ID" }, { status: 400 });
}
const { runParam } = parsed.data;
const taskRun = await prisma.taskRun.findUnique({
where: {
friendlyId: runParam,
runtimeEnvironmentId: authenticationResult.environment.id,
},
});
if (!taskRun) {
return json({ error: "Run not found" }, { status: 404 });
}
const anyBody = await request.json();
const body = RescheduleRunRequestBody.safeParse(anyBody);
if (!body.success) {
return json({ error: "Invalid request body" }, { status: 400 });
}
const service = new RescheduleTaskRunService();
try {
const updatedRun = await service.call(taskRun, body.data);
if (!updatedRun) {
return json({ error: "An unknown error occurred" }, { status: 500 });
}
const presenter = new ApiRetrieveRunPresenter();
const result = await presenter.call(
updatedRun.friendlyId,
authenticationResult.environment,
true
);
if (!result) {
return json({ error: "Run not found" }, { status: 404 });
}
return json(result);
} catch (error) {
if (error instanceof ServiceValidationError) {
return json({ error: error.message }, { status: 400 });
} else if (error instanceof Error) {
return json({ error: error.message }, { status: 500 });
} else {
return json({ error: "An unknown error occurred" }, { status: 500 });
}
}
}
@@ -7,6 +7,7 @@ import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { BatchTriggerTaskService } from "~/v3/services/batchTriggerTask.server";
import { HeadersSchema } from "./api.v1.tasks.$taskId.trigger";
import { env } from "~/env.server";
const ParamsSchema = z.object({
taskId: z.string(),
@@ -43,6 +44,12 @@ export async function action({ request, params }: ActionFunctionArgs) {
const { taskId } = ParamsSchema.parse(params);
const contentLength = request.headers.get("content-length");
if (!contentLength || parseInt(contentLength) > env.TASK_PAYLOAD_MAXIMUM_SIZE) {
return json({ error: "Request body too large" }, { status: 413 });
}
// Now parse the request body
const anyBody = await request.json();
@@ -2,9 +2,12 @@ import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { TriggerTaskRequestBody } from "@trigger.dev/core/v3";
import { z } from "zod";
import { env } from "~/env.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { parseRequestJsonAsync } from "~/utils/parseRequestJson.server";
import { TriggerTaskService } from "~/v3/services/triggerTask.server";
import { startActiveSpan } from "~/v3/tracer.server";
const ParamsSchema = z.object({
taskId: z.string(),
@@ -32,6 +35,12 @@ export async function action({ request, params }: ActionFunctionArgs) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}
const contentLength = request.headers.get("content-length");
if (!contentLength || parseInt(contentLength) > env.TASK_PAYLOAD_MAXIMUM_SIZE) {
return json({ error: "Request body too large" }, { status: 413 });
}
const rawHeaders = Object.fromEntries(request.headers);
const headers = HeadersSchema.safeParse(rawHeaders);
@@ -52,9 +61,11 @@ export async function action({ request, params }: ActionFunctionArgs) {
const { taskId } = ParamsSchema.parse(params);
// Now parse the request body
const anyBody = await request.json();
const anyBody = await parseRequestJsonAsync(request, { taskId });
const body = TriggerTaskRequestBody.safeParse(anyBody);
const body = await startActiveSpan("TriggerTaskRequestBody.safeParse()", async (span) => {
return TriggerTaskRequestBody.safeParse(anyBody);
});
if (!body.success) {
return json({ error: "Invalid request body" }, { status: 400 });
@@ -76,17 +87,23 @@ export async function action({ request, params }: ActionFunctionArgs) {
idempotencyKey,
triggerVersion,
headers: Object.fromEntries(request.headers),
body: body.data,
options: body.data.options,
isFromWorker,
traceContext,
});
const run = await service.call(taskId, authenticationResult.environment, body.data, {
idempotencyKey: idempotencyKey ?? undefined,
triggerVersion: triggerVersion ?? undefined,
traceContext,
spanParentAsLink: spanParentAsLink === 1,
});
const run = await service.call(
taskId,
authenticationResult.environment,
{ ...body.data },
// { ...body.data, payload: (anyBody as any).payload },
{
idempotencyKey: idempotencyKey ?? undefined,
triggerVersion: triggerVersion ?? undefined,
traceContext,
spanParentAsLink: spanParentAsLink === 1,
}
);
if (!run) {
return json({ error: "Task not found" }, { status: 404 });
@@ -33,6 +33,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
const taskRun = await prisma.taskRun.findUnique({
where: {
friendlyId: runParam,
runtimeEnvironmentId: authenticationResult.environment.id,
},
});
@@ -0,0 +1,74 @@
import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import { EnvSlug, isEnvSlug } from "~/models/api-key.server";
import { requireUserId } from "~/services/session.server";
const ParamsSchema = z.object({
projectRef: z.string(),
});
export async function loader({ params, request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const { projectRef } = ParamsSchema.parse(params);
const project = await prisma.project.findFirst({
where: {
externalRef: projectRef,
organization: {
members: {
some: {
userId,
},
},
},
},
include: {
organization: true,
},
});
if (!project) {
return new Response("Project not found", { status: 404 });
}
const url = new URL(request.url);
const envSlug = url.searchParams.get("envSlug");
// Get the environment from the slug
if (envSlug && isEnvSlug(envSlug)) {
const env = await getEnvFromSlug(project.id, userId, envSlug);
if (env) {
url.searchParams.set("environments", env.id);
}
url.searchParams.delete("envSlug");
}
return redirect(
`/orgs/${project.organization.slug}/projects/v3/${project.slug}/runs${url.search}`
);
}
async function getEnvFromSlug(projectId: string, userId: string, envSlug: EnvSlug) {
if (envSlug === "dev") {
return await prisma.runtimeEnvironment.findFirst({
where: {
projectId,
slug: envSlug,
orgMember: {
userId,
},
},
});
}
return await prisma.runtimeEnvironment.findFirst({
where: {
projectId,
slug: envSlug,
},
});
}
@@ -0,0 +1,40 @@
import { LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import { requireUserId } from "~/services/session.server";
const ParamsSchema = z.object({
projectRef: z.string(),
});
export async function loader({ params, request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const validatedParams = ParamsSchema.parse(params);
const project = await prisma.project.findFirst({
where: {
externalRef: validatedParams.projectRef,
organization: {
members: {
some: {
userId,
},
},
},
},
include: {
organization: true,
},
});
if (!project) {
return new Response("Not found", { status: 404 });
}
const url = new URL(request.url);
return redirect(
`/orgs/${project.organization.slug}/projects/v3/${project.slug}/test${url.search}`
);
}
@@ -33,7 +33,13 @@ import { redirectWithErrorMessage } from "~/models/message.server";
import { Span, SpanPresenter } from "~/presenters/v3/SpanPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { v3RunPath, v3RunSpanPath, v3SpanParamsSchema, v3TraceSpanPath } from "~/utils/pathBuilder";
import {
v3RunDownloadLogsPath,
v3RunPath,
v3RunSpanPath,
v3SpanParamsSchema,
v3TraceSpanPath,
} from "~/utils/pathBuilder";
import { SpanLink } from "~/v3/eventRepository.server";
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
@@ -256,6 +262,15 @@ function RunActionButtons({ span }: { span: Span }) {
if (span.isPartial) {
return (
<Dialog>
<LinkButton
to={v3RunDownloadLogsPath({ friendlyId: runParam })}
LeadingIcon={CloudArrowDownIcon}
variant="tertiary/medium"
target="_blank"
download
>
Download logs
</LinkButton>
<DialogTrigger asChild>
<Button variant="danger/medium" LeadingIcon={StopCircleIcon}>
Cancel run
@@ -276,6 +291,15 @@ function RunActionButtons({ span }: { span: Span }) {
return (
<Dialog>
<LinkButton
to={v3RunDownloadLogsPath({ friendlyId: runParam })}
LeadingIcon={CloudArrowDownIcon}
variant="tertiary/medium"
target="_blank"
download
>
Download logs
</LinkButton>
<DialogTrigger asChild>
<Button variant="tertiary/medium" LeadingIcon={ArrowPathIcon}>
Replay run
@@ -2,9 +2,8 @@ import { LoaderFunctionArgs } from "@remix-run/node";
import { basename } from "node:path";
import { z } from "zod";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { requireUserId } from "~/services/session.server";
import { r2 } from "~/v3/r2.server";
import { generatePresignedRequest } from "~/v3/r2.server";
const ParamSchema = z.object({
environmentId: z.string(),
@@ -35,27 +34,17 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
return new Response("Not found", { status: 404 });
}
if (!env.OBJECT_STORE_BASE_URL) {
return new Response("Object store base URL is not set", { status: 500 });
}
if (!r2) {
return new Response("Object store credentials are not set", { status: 500 });
}
const url = new URL(env.OBJECT_STORE_BASE_URL);
url.pathname = `/packets/${environment.project.externalRef}/${environment.slug}/${filename}`;
url.searchParams.set("X-Amz-Expires", "30"); // 30 seconds
const signed = await r2.sign(
new Request(url, {
method: "GET",
}),
{
aws: { signQuery: true },
}
const signed = await generatePresignedRequest(
environment.project.externalRef,
environment.slug,
filename,
"GET"
);
if (!signed) {
return new Response("Failed to generate presigned URL", { status: 500 });
}
const response = await fetch(signed.url, {
headers: signed.headers,
});
@@ -64,7 +53,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
status: 200,
headers: {
"Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename="${basename(url.pathname)}"`,
"Content-Disposition": `attachment; filename="${basename(filename)}"`,
},
});
}
@@ -0,0 +1,112 @@
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { prisma } from "~/db.server";
import { requireUserId } from "~/services/session.server";
import { v3RunParamsSchema } from "~/utils/pathBuilder";
import {
PreparedEvent,
RunPreparedEvent,
eventRepository,
getDateFromNanoseconds,
} from "~/v3/eventRepository.server";
import { createGzip } from "zlib";
import { Readable } from "stream";
import { formatDurationMilliseconds } from "@trigger.dev/core/v3/utils/durations";
export async function loader({ params, request }: LoaderFunctionArgs) {
const userId = await requireUserId(request);
const parsedParams = v3RunParamsSchema.pick({ runParam: true }).parse(params);
const run = await prisma.taskRun.findFirst({
where: {
friendlyId: parsedParams.runParam,
project: {
organization: {
members: {
some: {
userId,
},
},
},
},
},
});
if (!run) {
return new Response("Not found", { status: 404 });
}
const runEvents = await eventRepository.getRunEvents(run.friendlyId);
// Create a Readable stream from the runEvents array
const readable = new Readable({
read() {
runEvents.forEach((event) => {
try {
this.push(formatRunEvent(event) + "\n");
} catch {}
});
this.push(null); // End of stream
},
});
// Create a gzip transform stream
const gzip = createGzip();
// Pipe the readable stream into the gzip stream
const compressedStream = readable.pipe(gzip);
// Return the response with the compressed stream
return new Response(compressedStream as any, {
status: 200,
headers: {
"Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename="${parsedParams.runParam}.log"`,
"Content-Encoding": "gzip",
},
});
}
function formatRunEvent(event: RunPreparedEvent): string {
const entries = [];
const parts: string[] = [];
parts.push(getDateFromNanoseconds(event.startTime).toISOString());
if (event.taskSlug) {
parts.push(event.taskSlug);
}
parts.push(event.level);
parts.push(event.message);
if (event.level === "TRACE") {
parts.push(`(${formatDurationMilliseconds(event.duration / 1_000_000)})`);
}
entries.push(parts.join(" "));
if (event.events) {
for (const subEvent of event.events) {
if (subEvent.name === "exception") {
const subEventParts: string[] = [];
subEventParts.push(subEvent.time as unknown as string);
if (event.taskSlug) {
subEventParts.push(event.taskSlug);
}
subEventParts.push(subEvent.name);
subEventParts.push((subEvent.properties as any).exception.message);
if ((subEvent.properties as any).exception.stack) {
subEventParts.push((subEvent.properties as any).exception.stack);
}
entries.push(subEventParts.join(" "));
}
}
}
return entries.join("\n");
}
+15 -2
View File
@@ -136,7 +136,7 @@ export class EndpointApi {
};
}
async executeJobRequest(options: RunJobBody) {
async executeJobRequest(options: RunJobBody, timeoutInMs?: number) {
const startTimeInMs = performance.now();
const response = await safeFetch(this.url, {
@@ -147,8 +147,18 @@ export class EndpointApi {
"x-trigger-action": "EXECUTE_JOB",
},
body: JSON.stringify(options),
signal: timeoutInMs ? AbortSignal.timeout(timeoutInMs) : undefined,
});
if (response) {
logger.debug("executeJobRequest() response from endpoint", {
status: response.status,
headers: Object.fromEntries(response.headers.entries()),
});
} else {
logger.debug("executeJobRequest() no response from endpoint");
}
return {
response,
parser: RunJobResponseSchema,
@@ -434,7 +444,10 @@ async function safeFetch(url: string, options: RequestInit) {
} catch (error) {
logger.debug("Error while trying to connect to endpoint", {
url,
error,
error:
error instanceof Error
? { name: error.name, message: error.message, stack: error.stack }
: String(error),
});
}
}
@@ -269,7 +269,10 @@ export class PerformRunExecutionV3Service {
// TODO: add the ability to abort the execution from any server using Redis pub/sub
const { response, parser, errorParser, headersParser, durationInMs } =
await client.executeJobRequest(executionBody);
await client.executeJobRequest(
executionBody,
run.environment.type === "DEVELOPMENT" ? 60_000 * 5 : undefined
);
await createExecutionEvent({
eventType: "finish",
@@ -929,6 +932,25 @@ export class PerformRunExecutionV3Service {
executionCount: number = 1
) {
await $transaction(this.#prismaClient, async (tx) => {
const service = new CompleteRunTaskService(tx);
const task = await service.call(run.environment, run.id, data.id, {
properties: data.properties,
output: data.output ? (JSON.parse(data.output) as any) : undefined,
});
if (!task || task.status === "ERRORED") {
return await this.#failRunExecution(
tx,
run,
{
message: task ? `Task '${task.name}' failed to complete` : "Task failed to complete",
},
"FAILURE",
durationInMs
);
}
await tx.jobRun.update({
where: {
id: run.id,
@@ -958,13 +980,6 @@ export class PerformRunExecutionV3Service {
},
});
const service = new CompleteRunTaskService(tx);
await service.call(run.environment, run.id, data.id, {
properties: data.properties,
output: data.output ? (JSON.parse(data.output) as any) : undefined,
});
await ResumeRunService.enqueue(run, tx);
});
}
+134 -121
View File
@@ -6,6 +6,7 @@ import { taskWithAttemptsToServerTask } from "~/models/task.server";
import { generateSecret } from "~/services/sources/utils.server";
import { ulid } from "~/services/ulid.server";
import { taskOperationWorker, workerQueue } from "~/services/worker.server";
import { startActiveSpan } from "~/v3/tracer.server";
export class RunTaskService {
#prismaClient: PrismaClient;
@@ -19,142 +20,154 @@ export class RunTaskService {
idempotencyKey: string,
taskBody: RunTaskBodyOutput
): Promise<ServerTask | undefined> {
const delayUntilInFuture = taskBody.delayUntil
? taskBody.delayUntil.getTime() > Date.now()
: false;
const callbackEnabled = taskBody.callback?.enabled ?? false;
return startActiveSpan("RunTaskService.call", async (span) => {
span.setAttribute("runId", runId);
// First
const existingTask = await this.#handleExistingTask(
runId,
idempotencyKey,
taskBody,
delayUntilInFuture,
callbackEnabled
);
const delayUntilInFuture = taskBody.delayUntil
? taskBody.delayUntil.getTime() > Date.now()
: false;
const callbackEnabled = taskBody.callback?.enabled ?? false;
if (existingTask) {
return taskWithAttemptsToServerTask(existingTask);
}
// First
const existingTask = await this.#handleExistingTask(
runId,
idempotencyKey,
taskBody,
delayUntilInFuture,
callbackEnabled
);
const run = await this.#prismaClient.jobRun.findUnique({
where: {
id: runId,
},
select: {
status: true,
forceYieldImmediately: true,
},
});
if (existingTask) {
span.setAttribute("taskId", existingTask.id);
if (!run) throw new Error("Run not found");
const runConnection = taskBody.connectionKey
? await this.#prismaClient.runConnection.findUnique({
where: {
runId_key: {
runId,
key: taskBody.connectionKey,
},
},
select: {
id: true,
},
})
: undefined;
const results = await $transaction(this.#prismaClient, async (tx) => {
// If task.delayUntil is set and is in the future, we'll set the task's status to "WAITING", else set it to RUNNING
let status: TaskStatus;
if (run.status === "CANCELED") {
status = "CANCELED";
} else {
status =
delayUntilInFuture || callbackEnabled
? "WAITING"
: taskBody.noop
? "COMPLETED"
: "RUNNING";
return taskWithAttemptsToServerTask(existingTask);
}
const taskId = ulid();
const callbackUrl = callbackEnabled
? `${env.APP_ORIGIN}/api/v1/tasks/${taskId}/callback/${generateSecret(12)}`
const run = await this.#prismaClient.jobRun.findUnique({
where: {
id: runId,
},
select: {
status: true,
forceYieldImmediately: true,
},
});
if (!run) throw new Error("Run not found");
const runConnection = taskBody.connectionKey
? await this.#prismaClient.runConnection.findUnique({
where: {
runId_key: {
runId,
key: taskBody.connectionKey,
},
},
select: {
id: true,
},
})
: undefined;
const task = await tx.task.create({
data: {
id: taskId,
idempotencyKey,
displayKey: taskBody.displayKey,
runConnectionId: runConnection ? runConnection.id : undefined,
icon: taskBody.icon,
runId,
parentId: taskBody.parentId,
name: taskBody.name ?? "Task",
description: taskBody.description,
status,
startedAt: new Date(),
completedAt: status === "COMPLETED" || status === "CANCELED" ? new Date() : undefined,
noop: taskBody.noop,
delayUntil: taskBody.delayUntil,
params: taskBody.params ?? undefined,
properties: this.#filterProperties(taskBody.properties) ?? undefined,
redact: taskBody.redact ?? undefined,
operation: taskBody.operation,
callbackUrl,
style: taskBody.style ?? { style: "normal" },
childExecutionMode: taskBody.parallel ? "PARALLEL" : "SEQUENTIAL",
},
});
const results = await $transaction(
this.#prismaClient,
async (tx) => {
// If task.delayUntil is set and is in the future, we'll set the task's status to "WAITING", else set it to RUNNING
let status: TaskStatus;
const taskAttempt = await tx.taskAttempt.create({
data: {
number: 1,
taskId: task.id,
status: "PENDING",
},
});
if (run.status === "CANCELED") {
status = "CANCELED";
} else {
status =
delayUntilInFuture || callbackEnabled
? "WAITING"
: taskBody.noop
? "COMPLETED"
: "RUNNING";
}
if (task.status === "RUNNING" && typeof taskBody.operation === "string") {
// We need to schedule the operation
await taskOperationWorker.enqueue(
"performTaskOperation",
{
id: task.id,
},
{ tx, runAt: task.delayUntil ?? undefined, jobKey: `operation:${task.id}` }
);
} else if (task.status === "WAITING" && callbackUrl && taskBody.callback) {
if (taskBody.callback.timeoutInSeconds > 0) {
// We need to schedule the callback timeout
await workerQueue.enqueue(
"processCallbackTimeout",
{
id: task.id,
const taskId = ulid();
const callbackUrl = callbackEnabled
? `${env.APP_ORIGIN}/api/v1/tasks/${taskId}/callback/${generateSecret(12)}`
: undefined;
const task = await tx.task.create({
data: {
id: taskId,
idempotencyKey,
displayKey: taskBody.displayKey,
runConnectionId: runConnection ? runConnection.id : undefined,
icon: taskBody.icon,
runId,
parentId: taskBody.parentId,
name: taskBody.name ?? "Task",
description: taskBody.description,
status,
startedAt: new Date(),
completedAt: status === "COMPLETED" || status === "CANCELED" ? new Date() : undefined,
noop: taskBody.noop,
delayUntil: taskBody.delayUntil,
params: taskBody.params ?? undefined,
properties: this.#filterProperties(taskBody.properties) ?? undefined,
redact: taskBody.redact ?? undefined,
operation: taskBody.operation,
callbackUrl,
style: taskBody.style ?? { style: "normal" },
childExecutionMode: taskBody.parallel ? "PARALLEL" : "SEQUENTIAL",
},
{
tx,
runAt: new Date(Date.now() + taskBody.callback.timeoutInSeconds * 1000),
jobKey: `process-callback:${task.id}`,
});
span.setAttribute("taskId", task.id);
const taskAttempt = await tx.taskAttempt.create({
data: {
number: 1,
taskId: task.id,
status: "PENDING",
},
});
if (task.status === "RUNNING" && typeof taskBody.operation === "string") {
// We need to schedule the operation
await taskOperationWorker.enqueue(
"performTaskOperation",
{
id: task.id,
},
{ tx, runAt: task.delayUntil ?? undefined, jobKey: `operation:${task.id}` }
);
} else if (task.status === "WAITING" && callbackUrl && taskBody.callback) {
if (taskBody.callback.timeoutInSeconds > 0) {
// We need to schedule the callback timeout
await workerQueue.enqueue(
"processCallbackTimeout",
{
id: task.id,
},
{
tx,
runAt: new Date(Date.now() + taskBody.callback.timeoutInSeconds * 1000),
jobKey: `process-callback:${task.id}`,
}
);
}
);
}
}
return { task, taskAttempt };
},
{ timeout: 10000 }
);
if (!results) {
return;
}
return { task, taskAttempt };
const { task, taskAttempt } = results;
return task
? taskWithAttemptsToServerTask({ ...task, attempts: [taskAttempt], run })
: undefined;
});
if (!results) {
return;
}
const { task, taskAttempt } = results;
return task
? taskWithAttemptsToServerTask({ ...task, attempts: [taskAttempt], run })
: undefined;
}
async #handleExistingTask(
+26
View File
@@ -47,6 +47,8 @@ import { ResumeTaskService } from "./tasks/resumeTask.server";
import { RequeueV2Message } from "~/v3/marqs/requeueV2Message.server";
import { MarqsConcurrencyMonitor } from "~/v3/marqs/concurrencyMonitor.server";
import { reportUsageEvent } from "~/v3/openMeter.server";
import { EnqueueDelayedRunService } from "~/v3/services/enqueueDelayedRun.server";
import { ExpireEnqueuedRunService } from "~/v3/services/expireEnqueuedRun.server";
const workerCatalog = {
indexEndpoint: z.object({
@@ -177,6 +179,12 @@ const workerCatalog = {
}),
additionalData: z.record(z.any()).optional(),
}),
"v3.enqueueDelayedRun": z.object({
runId: z.string(),
}),
"v3.expireRun": z.object({
runId: z.string(),
}),
};
const executionWorkerCatalog = {
@@ -672,6 +680,24 @@ function getWorkerQueue() {
});
},
},
"v3.enqueueDelayedRun": {
priority: 0,
maxAttempts: 8,
handler: async (payload, job) => {
const service = new EnqueueDelayedRunService();
return await service.call(payload.runId);
},
},
"v3.expireRun": {
priority: 0,
maxAttempts: 8,
handler: async (payload, job) => {
const service = new ExpireEnqueuedRunService();
return await service.call(payload.runId);
},
},
},
});
}
@@ -0,0 +1,29 @@
import { Attributes } from "@opentelemetry/api";
import { startActiveSpan } from "~/v3/tracer.server";
export async function parseRequestJsonAsync(
request: Request,
attributes?: Attributes
): Promise<unknown> {
return await startActiveSpan(
"parseRequestJsonAsync()",
async (span) => {
span.setAttribute("content-length", parseInt(request.headers.get("content-length") ?? "0"));
span.setAttribute("content-type", request.headers.get("content-type") ?? "application/json");
span.setAttribute("experiment.async", false);
const rawText = await startActiveSpan("request.text()", async () => {
return await request.text();
});
if (rawText.length === 0) {
return;
}
return JSON.parse(rawText);
},
{
attributes,
}
);
}
+4
View File
@@ -375,6 +375,10 @@ export function v3RunPath(organization: OrgForPath, project: ProjectForPath, run
return `${v3RunsPath(organization, project)}/${run.friendlyId}`;
}
export function v3RunDownloadLogsPath(run: v3RunForPath) {
return `/resources/runs/${run.friendlyId}/logs/download`;
}
export function v3RunSpanPath(
organization: OrgForPath,
project: ProjectForPath,
+257 -63
View File
@@ -129,6 +129,10 @@ export type PreparedEvent = Omit<QueriedEvent, "events" | "style" | "duration">
style: TaskEventStyle;
};
export type RunPreparedEvent = PreparedEvent & {
taskSlug?: string;
};
export type SpanLink =
| {
type: "run";
@@ -340,7 +344,7 @@ export class EventRepository {
});
}
async queryIncompleteEvents(queryOptions: QueryOptions) {
async queryIncompleteEvents(queryOptions: QueryOptions, allowCompleteDuplicate = false) {
// First we will find all the events that match the query options (selecting minimal data).
const taskEvents = await this.readReplica.taskEvent.findMany({
where: queryOptions,
@@ -358,6 +362,10 @@ export class EventRepository {
// If the event is cancelled, it is not incomplete
if (event.isCancelled) return false;
if (allowCompleteDuplicate) {
return true;
}
// There must not be another complete event with the same spanId
const hasCompleteDuplicate = taskEvents.some(
(otherEvent) =>
@@ -400,13 +408,19 @@ export class EventRepository {
orderBy: {
startTime: "asc",
},
take: env.MAXIMUM_TRACE_SUMMARY_VIEW_COUNT,
});
let preparedEvents: Array<PreparedEvent> = [];
let rootSpanId: string | undefined;
const eventsBySpanId = new Map<string, PreparedEvent>();
for (const event of events) {
preparedEvents.push(prepareEvent(event));
if (!rootSpanId && !event.parentId) {
rootSpanId = event.spanId;
}
}
for (const event of preparedEvents) {
@@ -424,6 +438,8 @@ export class EventRepository {
preparedEvents = Array.from(eventsBySpanId.values());
const spansBySpanId = new Map<string, SpanSummary>();
const spans = preparedEvents.map((event) => {
const ancestorCancelled = isAncestorCancelled(eventsBySpanId, event.spanId);
const duration = calculateDurationIfAncestorIsCancelled(
@@ -432,7 +448,7 @@ export class EventRepository {
event.duration
);
return {
const span = {
recordId: event.id,
id: event.spanId,
parentId: event.parentId ?? undefined,
@@ -451,14 +467,17 @@ export class EventRepository {
environmentType: event.environmentType,
},
};
spansBySpanId.set(event.spanId, span);
return span;
});
const rootSpanId = events.find((event) => !event.parentId);
if (!rootSpanId) {
return;
}
const rootSpan = spans.find((span) => span.id === rootSpanId.spanId);
const rootSpan = spansBySpanId.get(rootSpanId);
if (!rootSpan) {
return;
@@ -471,85 +490,259 @@ export class EventRepository {
});
}
public async getRunEvents(runId: string): Promise<RunPreparedEvent[]> {
return await startActiveSpan("getRunEvents", async (span) => {
const events = await this.readReplica.taskEvent.findMany({
select: {
id: true,
spanId: true,
parentId: true,
runId: true,
idempotencyKey: true,
message: true,
style: true,
startTime: true,
duration: true,
isError: true,
isPartial: true,
isCancelled: true,
level: true,
events: true,
environmentType: true,
taskSlug: true,
},
where: {
runId,
isPartial: false,
},
orderBy: {
startTime: "asc",
},
});
let preparedEvents: Array<PreparedEvent> = [];
for (const event of events) {
preparedEvents.push(prepareEvent(event));
}
return preparedEvents;
});
}
// A Span can be cancelled if it is partial and has a parent that is cancelled
// And a span's duration, if it is partial and has a cancelled parent, is the time between the start of the span and the time of the cancellation event of the parent
public async getSpan(spanId: string, traceId: string) {
const traceSummary = await this.getTraceSummary(traceId);
return await startActiveSpan("getSpan", async (s) => {
const spanEvent = await this.#getSpanEvent(spanId);
const span = traceSummary?.spans.find((span) => span.id === spanId);
if (!spanEvent) {
return;
}
if (!span) {
return;
}
const preparedEvent = prepareEvent(spanEvent);
const fullEvent = await this.readReplica.taskEvent.findUnique({
where: {
id: span.recordId,
},
});
const span = await this.#createSpanFromEvent(preparedEvent);
if (!fullEvent) {
return;
}
const output = rehydrateJson(spanEvent.output);
const payload = rehydrateJson(spanEvent.payload);
const output = rehydrateJson(fullEvent.output);
const payload = rehydrateJson(fullEvent.payload);
const show = rehydrateShow(spanEvent.properties);
const show = rehydrateShow(fullEvent.properties);
const properties = sanitizedAttributes(spanEvent.properties);
const properties = sanitizedAttributes(fullEvent.properties);
const messagingEvent = SpanMessagingEvent.optional().safeParse(
(properties as any)?.messaging
);
const messagingEvent = SpanMessagingEvent.optional().safeParse((properties as any)?.messaging);
const links: SpanLink[] = [];
const links: SpanLink[] = [];
if (messagingEvent.success && messagingEvent.data) {
if (messagingEvent.data.message && "id" in messagingEvent.data.message) {
if (messagingEvent.data.message.id.startsWith("run_")) {
links.push({
type: "run",
icon: "runs",
title: `Run ${messagingEvent.data.message.id}`,
runId: messagingEvent.data.message.id,
});
if (messagingEvent.success && messagingEvent.data) {
if (messagingEvent.data.message && "id" in messagingEvent.data.message) {
if (messagingEvent.data.message.id.startsWith("run_")) {
links.push({
type: "run",
icon: "runs",
title: `Run ${messagingEvent.data.message.id}`,
runId: messagingEvent.data.message.id,
});
}
}
}
}
const backLinks = fullEvent.links as any as Link[] | undefined;
const backLinks = spanEvent.links as any as Link[] | undefined;
if (backLinks && backLinks.length > 0) {
backLinks.forEach((l) => {
const title = String(
l.attributes?.[SemanticInternalAttributes.LINK_TITLE] ?? "Triggered by"
);
if (backLinks && backLinks.length > 0) {
backLinks.forEach((l) => {
const title = String(
l.attributes?.[SemanticInternalAttributes.LINK_TITLE] ?? "Triggered by"
);
links.push({
type: "span",
icon: "trigger",
title,
traceId: l.context.traceId,
spanId: l.context.spanId,
links.push({
type: "span",
icon: "trigger",
title,
traceId: l.context.traceId,
spanId: l.context.spanId,
});
});
});
}
const spanEvents = transformEvents(
preparedEvent.events,
spanEvent.metadata as Attributes,
spanEvent.environmentType === "DEVELOPMENT"
);
return {
...spanEvent,
...span.data,
payload,
output,
properties,
events: spanEvents,
show,
links,
};
});
}
async #createSpanFromEvent(event: PreparedEvent) {
return await startActiveSpan("createSpanFromEvent", async (s) => {
let ancestorCancelled = false;
let duration = event.duration;
if (!event.isCancelled && event.isPartial) {
await this.#walkSpanAncestors(event, (ancestorEvent, level) => {
if (level >= 8) {
return { stop: true };
}
if (ancestorEvent.isCancelled) {
ancestorCancelled = true;
// We need to get the cancellation time from the cancellation span event
const cancellationEvent = ancestorEvent.events.find(
(event) => event.name === "cancellation"
);
if (cancellationEvent) {
duration = calculateDurationFromStart(event.startTime, cancellationEvent.time);
}
return { stop: true };
}
return { stop: false };
});
}
const span = {
recordId: event.id,
id: event.spanId,
parentId: event.parentId ?? undefined,
runId: event.runId,
idempotencyKey: event.idempotencyKey,
data: {
message: event.message,
style: event.style,
duration,
isError: event.isError,
isPartial: ancestorCancelled ? false : event.isPartial,
isCancelled: event.isCancelled === true ? true : event.isPartial && ancestorCancelled,
startTime: getDateFromNanoseconds(event.startTime),
level: event.level,
events: event.events,
environmentType: event.environmentType,
},
};
return span;
});
}
async #walkSpanAncestors(
event: PreparedEvent,
callback: (event: PreparedEvent, level: number) => { stop: boolean }
) {
const parentId = event.parentId;
if (!parentId) {
return;
}
const events = transformEvents(
span.data.events,
fullEvent.metadata as Attributes,
traceSummary?.rootSpan.data.environmentType === "DEVELOPMENT"
);
await startActiveSpan("walkSpanAncestors", async (s) => {
let parentEvent = await this.#getSpanEvent(parentId);
let level = 1;
return {
...fullEvent,
...span.data,
payload,
output,
properties,
events,
show,
links,
};
while (parentEvent) {
const preparedParentEvent = prepareEvent(parentEvent);
const result = callback(preparedParentEvent, level);
if (result.stop) {
return;
}
if (!preparedParentEvent.parentId) {
return;
}
parentEvent = await this.#getSpanEvent(preparedParentEvent.parentId);
level++;
}
});
}
async #getSpanAncestors(event: PreparedEvent, levels = 1): Promise<Array<PreparedEvent>> {
if (levels >= 8) {
return [];
}
if (!event.parentId) {
return [];
}
const parentEvent = await this.#getSpanEvent(event.parentId);
if (!parentEvent) {
return [];
}
const preparedParentEvent = prepareEvent(parentEvent);
if (!preparedParentEvent.parentId) {
return [preparedParentEvent];
}
const moreAncestors = await this.#getSpanAncestors(preparedParentEvent, levels + 1);
return [preparedParentEvent, ...moreAncestors];
}
async #getSpanEvent(spanId: string) {
return await startActiveSpan("getSpanEvent", async (s) => {
const events = await this.readReplica.taskEvent.findMany({
where: {
spanId,
},
orderBy: {
startTime: "asc",
},
});
let finalEvent: TaskEvent | undefined;
for (const event of events) {
if (event.isPartial && finalEvent) {
continue;
}
finalEvent = event;
}
return finalEvent;
});
}
public async recordEvent(message: string, options: TraceEventOptions) {
@@ -929,6 +1122,7 @@ export function createExceptionPropertiesFromError(error: TaskRunError): Excepti
return {
type: "Internal error",
message: [error.code, error.message].filter(Boolean).join(": "),
stacktrace: error.stackTrace,
};
}
case "STRING_ERROR": {
@@ -1224,7 +1418,7 @@ function getNowInNanoseconds(): bigint {
return BigInt(new Date().getTime() * 1_000_000);
}
function getDateFromNanoseconds(nanoseconds: bigint) {
export function getDateFromNanoseconds(nanoseconds: bigint) {
return new Date(Number(nanoseconds) / 1_000_000);
}
+1 -1
View File
@@ -267,7 +267,7 @@ function createProviderNamespace(io: Server) {
try {
const service = new DeploymentIndexFailed();
await service.call(message.deploymentId, message.error);
await service.call(message.deploymentId, message.error, message.overrideCompletion);
} catch (e) {
logger.error("Error while indexing", { error: e });
}
+75 -17
View File
@@ -3,6 +3,7 @@ import { env } from "~/env.server";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { singleton } from "~/utils/singleton";
import { startActiveSpan } from "./tracer.server";
export const r2 = singleton("r2", initializeR2);
@@ -23,30 +24,87 @@ export async function uploadToObjectStore(
contentType: string,
environment: AuthenticatedEnvironment
): Promise<string> {
if (!r2) {
throw new Error("Object store credentials are not set");
return await startActiveSpan("uploadToObjectStore()", async (span) => {
if (!r2) {
throw new Error("Object store credentials are not set");
}
if (!env.OBJECT_STORE_BASE_URL) {
throw new Error("Object store base URL is not set");
}
span.setAttributes({
projectRef: environment.project.externalRef,
environmentSlug: environment.slug,
filename: filename,
});
const url = new URL(env.OBJECT_STORE_BASE_URL);
url.pathname = `/packets/${environment.project.externalRef}/${environment.slug}/${filename}`;
logger.debug("Uploading to object store", { url: url.href });
const response = await r2.fetch(url.toString(), {
method: "PUT",
headers: {
"Content-Type": contentType,
},
body: data,
});
if (!response.ok) {
throw new Error(`Failed to upload output to ${url}: ${response.statusText}`);
}
return url.href;
});
}
export async function generatePresignedRequest(
projectRef: string,
envSlug: string,
filename: string,
method: "PUT" | "GET" = "PUT"
) {
if (!env.OBJECT_STORE_BASE_URL) {
return;
}
if (!env.OBJECT_STORE_BASE_URL) {
throw new Error("Object store base URL is not set");
if (!r2) {
return;
}
const url = new URL(env.OBJECT_STORE_BASE_URL);
url.pathname = `/packets/${environment.project.externalRef}/${environment.slug}/${filename}`;
url.pathname = `/packets/${projectRef}/${envSlug}/${filename}`;
url.searchParams.set("X-Amz-Expires", "300"); // 5 minutes
logger.debug("Uploading to object store", { url: url.href });
const signed = await r2.sign(
new Request(url, {
method,
}),
{
aws: { signQuery: true },
}
);
const response = await r2.fetch(url.toString(), {
method: "PUT",
headers: {
"Content-Type": contentType,
},
body: data,
logger.debug("Generated presigned URL", {
url: signed.url,
headers: Object.fromEntries(signed.headers),
projectRef,
envSlug,
filename,
});
if (!response.ok) {
throw new Error(`Failed to upload output to ${url}: ${response.statusText}`);
}
return url.href;
return signed;
}
export async function generatePresignedUrl(
projectRef: string,
envSlug: string,
filename: string,
method: "PUT" | "GET" = "PUT"
) {
const signed = await generatePresignedRequest(projectRef, envSlug, filename, method);
return signed?.url;
}
@@ -48,6 +48,7 @@ export class RequeueTaskRunService extends BaseService {
break;
}
case "DELAYED":
case "WAITING_FOR_DEPLOY": {
logger.debug("[RequeueTaskRunService] Removing task run from queue", { taskRun });
@@ -68,6 +69,7 @@ export class RequeueTaskRunService extends BaseService {
case "CRASHED":
case "COMPLETED_WITH_ERRORS":
case "COMPLETED_SUCCESSFULLY":
case "EXPIRED":
case "CANCELED": {
logger.debug("[RequeueTaskRunService] Task run is completed", { taskRun });
@@ -13,6 +13,7 @@ export type CrashTaskRunServiceOptions = {
logs?: string;
crashAttempts?: boolean;
crashedAt?: Date;
overrideCompletion?: boolean;
};
export class CrashTaskRunService extends BaseService {
@@ -36,7 +37,7 @@ export class CrashTaskRunService extends BaseService {
}
// Make sure the task run is in a crashable state
if (!isCrashableRunStatus(taskRun.status)) {
if (!opts.overrideCompletion && !isCrashableRunStatus(taskRun.status)) {
logger.error("Task run is not in a crashable state", { runId, status: taskRun.status });
return;
}
@@ -74,9 +75,12 @@ export class CrashTaskRunService extends BaseService {
},
});
const inProgressEvents = await eventRepository.queryIncompleteEvents({
runId: taskRun.friendlyId,
});
const inProgressEvents = await eventRepository.queryIncompleteEvents(
{
runId: taskRun.friendlyId,
},
options?.overrideCompletion
);
logger.debug("Crashing in-progress events", {
inProgressEvents: inProgressEvents.map((event) => event.id),
@@ -18,7 +18,8 @@ export class DeploymentIndexFailed extends BaseService {
message: string;
stack?: string;
stderr?: string;
}
},
overrideCompletion = false
) {
const isFriendlyId = maybeFriendlyId.startsWith("deployment_");
@@ -38,6 +39,15 @@ export class DeploymentIndexFailed extends BaseService {
}
if (FINAL_DEPLOYMENT_STATUSES.includes(deployment.status)) {
if (overrideCompletion) {
logger.error("No support for overriding final deployment statuses just yet", {
id: deployment.id,
status: deployment.status,
previousError: deployment.errorData,
incomingError: error,
});
}
logger.error("Worker deployment already in final state", {
id: deployment.id,
status: deployment.status,
@@ -0,0 +1,72 @@
import { logger } from "~/services/logger.server";
import { marqs } from "~/v3/marqs/index.server";
import { BaseService } from "./baseService.server";
import { parseNaturalLanguageDuration } from "./triggerTask.server";
import { workerQueue } from "~/services/worker.server";
import { $transaction } from "~/db.server";
export class EnqueueDelayedRunService extends BaseService {
public async call(runId: string) {
const run = await this._prisma.taskRun.findUnique({
where: {
id: runId,
},
include: {
runtimeEnvironment: {
include: {
organization: true,
project: true,
},
},
},
});
if (!run) {
logger.debug("Could not find delayed run to enqueue", {
runId,
});
return;
}
if (run.status !== "DELAYED") {
logger.debug("Delayed run cannot be enqueued because it's not in DELAYED status", {
run,
});
return;
}
await $transaction(this._prisma, async (tx) => {
await tx.taskRun.update({
where: {
id: run.id,
},
data: {
status: "PENDING",
queuedAt: new Date(),
},
});
if (run.ttl) {
const expireAt = parseNaturalLanguageDuration(run.ttl);
if (expireAt) {
await workerQueue.enqueue(
"v3.expireRun",
{ runId: run.id },
{ tx, runAt: expireAt, jobKey: `v3.expireRun.${run.id}` }
);
}
}
});
await marqs?.enqueueMessage(
run.runtimeEnvironment,
run.queue,
run.id,
{ type: "EXECUTE", taskIdentifier: run.taskIdentifier },
run.concurrencyKey ?? undefined
);
}
}
@@ -0,0 +1,53 @@
import { logger } from "~/services/logger.server";
import { marqs } from "~/v3/marqs/index.server";
import { BaseService } from "./baseService.server";
export class ExpireEnqueuedRunService extends BaseService {
public async call(runId: string) {
const run = await this._prisma.taskRun.findUnique({
where: {
id: runId,
},
include: {
runtimeEnvironment: {
include: {
organization: true,
project: true,
},
},
},
});
if (!run) {
logger.debug("Could not find enqueued run to expire", {
runId,
});
return;
}
if (run.status !== "PENDING") {
logger.debug("Run cannot be expired because it's not in PENDING status", {
run,
});
return;
}
logger.debug("Expiring enqueued run", {
run,
});
await this._prisma.taskRun.update({
where: {
id: run.id,
},
data: {
status: "EXPIRED",
expiredAt: new Date(),
},
});
await marqs?.acknowledgeMessage(run.id);
}
}
@@ -0,0 +1,39 @@
import { TaskRun } from "@trigger.dev/database";
import { BaseService, ServiceValidationError } from "./baseService.server";
import { RescheduleRunRequestBody } from "@trigger.dev/core/v3";
import { parseDelay } from "./triggerTask.server";
import { $transaction } from "~/db.server";
import { workerQueue } from "~/services/worker.server";
export class RescheduleTaskRunService extends BaseService {
public async call(taskRun: TaskRun, body: RescheduleRunRequestBody) {
if (taskRun.status !== "DELAYED") {
throw new ServiceValidationError("Cannot reschedule a run that is not delayed");
}
const delay = await parseDelay(body.delay);
if (!delay) {
throw new ServiceValidationError(`Invalid delay: ${body.delay}`);
}
return await $transaction(this._prisma, async (tx) => {
const updatedRun = await tx.taskRun.update({
where: {
id: taskRun.id,
},
data: {
delayUntil: delay,
},
});
await workerQueue.enqueue(
"v3.enqueueDelayedRun",
{ runId: taskRun.id },
{ tx, runAt: delay, jobKey: `v3.enqueueDelayedRun.${taskRun.id}` }
);
return updatedRun;
});
}
}
+172 -32
View File
@@ -4,13 +4,15 @@ import {
TriggerTaskRequestBody,
packetRequiresOffloading,
} from "@trigger.dev/core/v3";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { autoIncrementCounter } from "~/services/autoIncrementCounter.server";
import { workerQueue } from "~/services/worker.server";
import { marqs, sanitizeQueueName } from "~/v3/marqs/index.server";
import { eventRepository } from "../eventRepository.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { uploadToObjectStore } from "../r2.server";
import { startActiveSpan } from "../tracer.server";
import { BaseService } from "./baseService.server";
export type TriggerTaskServiceOptions = {
@@ -34,6 +36,12 @@ export class TriggerTaskService extends BaseService {
span.setAttribute("taskId", taskId);
const idempotencyKey = options.idempotencyKey ?? body.options?.idempotencyKey;
const delayUntil = await parseDelay(body.options?.delay);
const ttl =
typeof body.options?.ttl === "number"
? stringifyDuration(body.options?.ttl)
: body.options?.ttl ?? (environment.type === "DEVELOPMENT" ? "10m" : undefined);
const existingRun = idempotencyKey
? await this._prisma.taskRun.findUnique({
@@ -48,9 +56,19 @@ export class TriggerTaskService extends BaseService {
if (existingRun && existingRun.taskIdentifier === taskId) {
span.setAttribute("runId", existingRun.friendlyId);
return existingRun;
}
const runFriendlyId = generateFriendlyId("run");
const payloadPacket = await this.#handlePayloadPacket(
body.payload,
body.options?.payloadType ?? "application/json",
runFriendlyId,
environment
);
return await eventRepository.traceEvent(
taskId,
{
@@ -75,15 +93,6 @@ export class TriggerTaskService extends BaseService {
immediate: true,
},
async (event, traceContext) => {
const runFriendlyId = generateFriendlyId("run");
const payloadPacket = await this.#handlePayloadPacket(
body.payload,
body.options?.payloadType ?? "application/json",
runFriendlyId,
environment
);
const run = await autoIncrementCounter.incrementInTransaction(
`v3-run:${environment.id}:${taskId}`,
async (num, tx) => {
@@ -111,7 +120,7 @@ export class TriggerTaskService extends BaseService {
const taskRun = await tx.taskRun.create({
data: {
status: "PENDING",
status: delayUntil ? "DELAYED" : "PENDING",
number: num,
friendlyId: runFriendlyId,
runtimeEnvironmentId: environment.id,
@@ -128,6 +137,9 @@ export class TriggerTaskService extends BaseService {
concurrencyKey: body.options?.concurrencyKey,
queue: queueName,
isTest: body.options?.test ?? false,
delayUntil,
queuedAt: delayUntil ? undefined : new Date(),
ttl,
},
});
@@ -214,6 +226,26 @@ export class TriggerTaskService extends BaseService {
}
}
if (taskRun.delayUntil) {
await workerQueue.enqueue(
"v3.enqueueDelayedRun",
{ runId: taskRun.id },
{ tx, runAt: delayUntil, jobKey: `v3.enqueueDelayedRun.${taskRun.id}` }
);
}
if (!taskRun.delayUntil && taskRun.ttl) {
const expireAt = parseNaturalLanguageDuration(taskRun.ttl);
if (expireAt) {
await workerQueue.enqueue(
"v3.expireRun",
{ runId: taskRun.id },
{ tx, runAt: expireAt, jobKey: `v3.expireRun.${taskRun.id}` }
);
}
}
return taskRun;
},
async (_, tx) => {
@@ -237,13 +269,15 @@ export class TriggerTaskService extends BaseService {
}
// We need to enqueue the task run into the appropriate queue. This is done after the tx completes to prevent a race condition where the task run hasn't been created yet by the time we dequeue.
await marqs?.enqueueMessage(
environment,
run.queue,
run.id,
{ type: "EXECUTE", taskIdentifier: taskId },
body.options?.concurrencyKey
);
if (run.status === "PENDING") {
await marqs?.enqueueMessage(
environment,
run.queue,
run.id,
{ type: "EXECUTE", taskIdentifier: taskId },
body.options?.concurrencyKey
);
}
return run;
}
@@ -257,26 +291,31 @@ export class TriggerTaskService extends BaseService {
pathPrefix: string,
environment: AuthenticatedEnvironment
) {
const packet = this.#createPayloadPacket(payload, payloadType);
return await startActiveSpan("handlePayloadPacket()", async (span) => {
const packet = this.#createPayloadPacket(payload, payloadType);
if (!packet.data) {
return packet;
}
if (!packet.data) {
return packet;
}
const { needsOffloading, size } = packetRequiresOffloading(packet);
const { needsOffloading, size } = packetRequiresOffloading(
packet,
env.TASK_PAYLOAD_OFFLOAD_THRESHOLD
);
if (!needsOffloading) {
return packet;
}
if (!needsOffloading) {
return packet;
}
const filename = `${pathPrefix}/payload.json`;
const filename = `${pathPrefix}/payload.json`;
await uploadToObjectStore(filename, packet.data, packet.dataType, environment);
await uploadToObjectStore(filename, packet.data, packet.dataType, environment);
return {
data: filename,
dataType: "application/store",
};
return {
data: filename,
dataType: "application/store",
};
});
}
#createPayloadPacket(payload: any, payloadType: string): IOPacket {
@@ -291,3 +330,104 @@ export class TriggerTaskService extends BaseService {
return { dataType: payloadType };
}
}
export async function parseDelay(value?: string | Date): Promise<Date | undefined> {
if (!value) {
return;
}
if (value instanceof Date) {
return value;
}
try {
const date = new Date(value);
// Check if the date is valid
if (isNaN(date.getTime())) {
return parseNaturalLanguageDuration(value);
}
if (date.getTime() <= Date.now()) {
return;
}
return date;
} catch (error) {
return parseNaturalLanguageDuration(value);
}
}
export function parseNaturalLanguageDuration(duration: string): Date | undefined {
const regexPattern = /^(\d+w)?(\d+d)?(\d+h)?(\d+m)?(\d+s)?$/;
const result: Date = new Date();
let hasMatch = false;
const elements = duration.match(regexPattern);
if (elements) {
if (elements[1]) {
const weeks = Number(elements[1].slice(0, -1));
if (weeks >= 0) {
result.setDate(result.getDate() + 7 * weeks);
hasMatch = true;
}
}
if (elements[2]) {
const days = Number(elements[2].slice(0, -1));
if (days >= 0) {
result.setDate(result.getDate() + days);
hasMatch = true;
}
}
if (elements[3]) {
const hours = Number(elements[3].slice(0, -1));
if (hours >= 0) {
result.setHours(result.getHours() + hours);
hasMatch = true;
}
}
if (elements[4]) {
const minutes = Number(elements[4].slice(0, -1));
if (minutes >= 0) {
result.setMinutes(result.getMinutes() + minutes);
hasMatch = true;
}
}
if (elements[5]) {
const seconds = Number(elements[5].slice(0, -1));
if (seconds >= 0) {
result.setSeconds(result.getSeconds() + seconds);
hasMatch = true;
}
}
}
if (hasMatch) {
return result;
}
return undefined;
}
function stringifyDuration(seconds: number): string | undefined {
if (seconds <= 0) {
return;
}
const units = {
w: Math.floor(seconds / 604800),
d: Math.floor((seconds % 604800) / 86400),
h: Math.floor((seconds % 86400) / 3600),
m: Math.floor((seconds % 3600) / 60),
s: Math.floor(seconds % 60),
};
// Filter the units having non-zero values and join them
const result: string = Object.entries(units)
.filter(([unit, val]) => val != 0)
.map(([unit, val]) => `${val}${unit}`)
.join("");
return result;
}
+2
View File
@@ -1,6 +1,7 @@
import type { TaskRunAttemptStatus, TaskRunStatus } from "@trigger.dev/database";
export const CANCELLABLE_RUN_STATUSES: TaskRunStatus[] = [
"DELAYED",
"PENDING",
"WAITING_FOR_DEPLOY",
"EXECUTING",
@@ -38,6 +39,7 @@ export const FINAL_RUN_STATUSES: TaskRunStatus[] = [
"COMPLETED_WITH_ERRORS",
"INTERRUPTED",
"SYSTEM_FAILURE",
"EXPIRED",
];
export const FINAL_ATTEMPT_STATUSES: TaskRunAttemptStatus[] = ["CANCELED", "COMPLETED", "FAILED"];
+2 -2
View File
@@ -117,7 +117,7 @@ function getTracer() {
const samplingRate = 1.0 / Math.max(parseInt(env.INTERNAL_OTEL_TRACE_SAMPLING_RATE, 10), 1);
const provider = new NodeTracerProvider({
forceFlushTimeoutMillis: 5000,
forceFlushTimeoutMillis: 15_000,
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: env.SERVICE_NAME,
}),
@@ -129,7 +129,7 @@ function getTracer() {
if (env.INTERNAL_OTEL_TRACE_EXPORTER_URL) {
const exporter = new OTLPTraceExporter({
url: env.INTERNAL_OTEL_TRACE_EXPORTER_URL,
timeoutMillis: 10_000,
timeoutMillis: 15_000,
headers:
env.INTERNAL_OTEL_TRACE_EXPORTER_AUTH_HEADER_NAME &&
env.INTERNAL_OTEL_TRACE_EXPORTER_AUTH_HEADER_VALUE
Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

+21 -46
View File
@@ -6,7 +6,7 @@
"/v3-openapi.yaml"
],
"versions": [
"v3 (Developer Preview)",
"v3",
"v2"
],
"api": {
@@ -22,15 +22,19 @@
},
"favicon": "/images/favicon.png",
"colors": {
"primary": "#818cf8",
"light": "#818cf8",
"dark": "#818cf8",
"primary": "#A8FF53",
"light": "#A8FF53",
"dark": "#A8FF53",
"anchors": {
"from": "#fff",
"to": "#eee"
},
"background": {
"light": "#fff",
"dark": "#121317"
}
},
"backgroundImage": "/images/docs-background.png",
"theme": "quill",
"modeToggle": {
"default": "dark",
"isHidden": true
@@ -50,36 +54,6 @@
"url": "https://discord.gg/kA47vcd8P6"
}
],
"primaryTab": {
"name": "Home"
},
"tabs": [
{
"name": "Integrations",
"url": "integrations",
"version": "v2"
},
{
"name": "Reference",
"url": "sdk",
"version": "v2"
},
{
"name": "Examples",
"url": "https://trigger.dev/apis",
"version": "v2"
},
{
"name": "v3 Developer Preview",
"url": "https://trigger.dev/docs/v3",
"version": "v2"
},
{
"name": "v2",
"url": "https://trigger.dev/docs/documentation",
"version": "v3 (Developer Preview)"
}
],
"redirects": [
{
"source": "/documentation/quickstart",
@@ -101,14 +75,14 @@
"navigation": [
{
"group": "",
"version": "v3 (Developer Preview)",
"version": "v3",
"pages": [
"v3/introduction"
]
},
{
"group": "Getting Started",
"version": "v3 (Developer Preview)",
"version": "v3",
"pages": [
"v3/quick-start",
"v3/upgrading-from-v2",
@@ -119,7 +93,7 @@
},
{
"group": "Fundamentals",
"version": "v3 (Developer Preview)",
"version": "v3",
"pages": [
"v3/trigger-folder",
"v3/tasks-overview",
@@ -137,7 +111,7 @@
},
{
"group": "Development",
"version": "v3 (Developer Preview)",
"version": "v3",
"pages": [
"v3/cli-dev",
"v3/run-tests"
@@ -145,7 +119,7 @@
},
{
"group": "Deployment",
"version": "v3 (Developer Preview)",
"version": "v3",
"pages": [
"v3/deploy-environment-variables",
"v3/cli-deploy",
@@ -160,7 +134,7 @@
},
{
"group": "Writing tasks",
"version": "v3 (Developer Preview)",
"version": "v3",
"pages": [
"v3/writing-tasks-introduction",
"v3/logging",
@@ -185,7 +159,7 @@
},
{
"group": "API reference",
"version": "v3 (Developer Preview)",
"version": "v3",
"pages": [
"v3/management/overview",
{
@@ -201,7 +175,8 @@
"v3/management/runs/list",
"v3/management/runs/retrieve",
"v3/management/runs/replay",
"v3/management/runs/cancel"
"v3/management/runs/cancel",
"v3/management/runs/reschedule"
]
},
{
@@ -238,7 +213,7 @@
},
{
"group": "Open source",
"version": "v3 (Developer Preview)",
"version": "v3",
"pages": [
"v3/github-repo",
"v3/open-source-self-hosting",
@@ -247,7 +222,7 @@
},
{
"group": "Troubleshooting",
"version": "v3 (Developer Preview)",
"version": "v3",
"pages": [
"v3/troubleshooting",
"v3/troubleshooting-alerts",
@@ -258,7 +233,7 @@
},
{
"group": "Help",
"version": "v3 (Developer Preview)",
"version": "v3",
"pages": [
"v3/community",
"v3/help-slack",
+91
View File
@@ -269,6 +269,7 @@ paths:
"/api/v1/timezones":
get:
security: []
operationId: get_timezones_v1
summary: Get all supported timezones
description: Get all supported timezones that schedule tasks support.
@@ -424,6 +425,71 @@ paths:
await runs.cancel("run_1234");
"/api/v1/runs/{runId}/reschedule":
parameters:
- $ref: "#/components/parameters/runId"
post:
operationId: reschedule_run_v1
summary: Rescheduled a delayed run
description: Updates a delayed run with a new delay. Only valid when the run is in the DELAYED state.
requestBody:
required: true
content:
application/json:
schema:
"$ref": "#/components/schemas/RescheduleRunRequestBody"
responses:
"200":
description: Successful request
content:
application/json:
schema:
"$ref": "#/components/schemas/RetrieveRunResponse"
"400":
description: Invalid request
content:
application/json:
schema:
type: object
properties:
error:
type: string
enum:
- Invalid or missing run ID
- Failed to create new run
"401":
description: Unauthorized request
content:
application/json:
schema:
type: object
properties:
error:
type: string
enum:
- Invalid or Missing API key
"404":
description: Resource not found
content:
application/json:
schema:
type: object
properties:
error:
type: string
enum:
- Run not found
tags:
- runs
security:
- secretKey: []
x-codeSamples:
- lang: typescript
source: |-
import { runs } from "@trigger.dev/sdk/v3";
const handle = await runs.reschedule("run_1234", { delay: new Date("2024-06-29T20:45:56.340Z") });
"/api/v3/runs/{runId}":
parameters:
- $ref: "#/components/parameters/runId"
@@ -1681,6 +1747,18 @@ components:
type: string
example: slack_123456
required: ["name", "value"]
RescheduleRunRequestBody:
type: object
properties:
delay:
oneOf:
- type: string
description: The duration to delay the run by. The duration should be in the format of `1d`, `6h`, `10m`, `11s`, etc.
example: 1hr
- type: string
format: date-time
description: The Date to delay the run until, e.g. `new Date()` or `"2024-06-25T15:45:26Z"`
example: 2024-06-25T15:45:26Z
RetrieveRunResponse:
type: object
required:
@@ -1699,6 +1777,7 @@ components:
type: string
description: The status of the run
enum:
- DELAYED
- WAITING_FOR_DEPLOY
- QUEUED
- EXECUTING
@@ -1722,10 +1801,18 @@ components:
type: object
description: The payload that was sent to the task. Will be omitted if the request was made with a Public API key
example: { "foo": "bar" }
payloadPresignedUrl:
type: string
description: The presigned URL to download the payload. Will only be included if the payload is too large to be included in the response. Expires in 5 minutes.
example: "https://r2.cloudflarestorage.com/packets/yubjwjsfkxnylobaqvqz/dev/run_p4omhh45hgxxnq1re6ovy/payload.json?X-Amz-Expires=300&X-Amz-Date=20240625T154526Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=10b064e58a0680db5b5e077be2be3b2a%2F20240625%2Fauto%2Fs3%2Faws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=88604cb993ffc151b4d73f2439da431d9928488e4b3dcfa4a7c8f1819"
output:
type: object
description: The output of the run. Will be omitted if the request was made with a Public API key
example: { "foo": "bar" }
outputPresignedUrl:
type: string
description: The presigned URL to download the output. Will only be included if the output is too large to be included in the response. Expires in 5 minutes.
example: "https://r2.cloudflarestorage.com/packets/yubjwjsfkxnylobaqvqz/dev/run_p4omhh45hgxxnq1re6ovy/payload.json?X-Amz-Expires=300&X-Amz-Date=20240625T154526Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=10b064e58a0680db5b5e077be2be3b2a%2F20240625%2Fauto%2Fs3%2Faws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=88604cb993ffc151b4d73f2439da431d9928488e4b3dcfa4a7c8f1819"
idempotencyKey:
type: string
description: The idempotency key used to prevent creating duplicate runs, if provided
@@ -1748,6 +1835,10 @@ components:
type: string
format: date-time
description: The time the run finished
delayedUntil:
type: string
format: date-time
description: If the run was triggered with a delay, this will be the time the run will be enqueued to execute
schedule:
type: object
description: The schedule that triggered the run. Will be omitted if the run was not triggered by a schedule
+1 -1
View File
@@ -15,7 +15,7 @@ Trigger.dev v3 makes it easy to write reliable long-running tasks without timeou
- We run your tasks with no timeouts. You don't have to manage any infrastructure (unless you [self-host](/v3/open-source-self-hosting)). Workers are automatically scaled and managed for you.
- We provide a multi-tenant queue that is used when triggering tasks.
- We provide an SDK and CLI for writing tasks in your existing codebase, inside [/trigger folders](/v3/trigger-folder).
- We provide different types of tasks: [regular](/v3/tasks-regular), [scheduled](/v3/tasks-scheduled), [zod](/v3/tasks-zod), [webhooks](/v3/tasks-webhooks).
- We provide different types of tasks: [regular](/v3/tasks-regular) and [scheduled](/v3/tasks-scheduled).
- We provide a dashboard for monitoring, debugging, and managing your tasks.
We're [open source](https://github.com/triggerdotdev/trigger.dev) and you can choose to use the [Trigger.dev Cloud](https://cloud.trigger.dev) or [Self-host Trigger.dev](/v3/open-source-self-hosting) on your own infrastructure.
+10
View File
@@ -37,3 +37,13 @@ If you add them dynamically using code make sure you add a `deduplicationKey` so
If you're creating schedules for your user you will definitely need to request more schedules from us.
<Snippet file="v3/soft-limit.mdx" />
## Task payloads and outputs
| Limit | Details |
| ---------------------- | ---------------------------------------------- |
| Single trigger payload | Must not exceed 10MB |
| Batch trigger payload | The total of all payloads must not exceed 10MB |
| Task outputs | Must not exceed 10MB |
Payloads and outputs that exceed 512KB will be offloaded to object storage and a presigned URL will be provided to download the data when calling `runs.retrieve`. You don't need to do anything to handle this in your tasks however, as we will transparently upload/download these during operation.
+4
View File
@@ -0,0 +1,4 @@
---
title: "Reschedule run"
openapi: "v3-openapi POST /api/v1/runs/{runId}/reschedule"
---
+1 -1
View File
@@ -4,7 +4,7 @@ sidebarTitle: "Tasks"
description: "Tasks are functions that can run for a long time and provide strong resilience to failure."
---
There are different types of tasks including [regular tasks](/v3/tasks-regular), [scheduled tasks](/v3/tasks-scheduled), [zod tasks](/v3/tasks-zod) and [webhook tasks](/v3/tasks-webhooks).
There are different types of tasks including [regular tasks](/v3/tasks-regular) and [scheduled tasks](/v3/tasks-scheduled).
## Hello world task and how to trigger it
+174
View File
@@ -536,3 +536,177 @@ export async function create() {
```
</CodeGroup>
## Large Payloads
We recommend keeping your task payloads as small as possible. We currently have a hard limit on task payloads above 10MB.
If your payload size is larger than 512KB, instead of saving the payload to the database, we will upload it to an S3-compatible object store and store the URL in the database.
When your task runs, we automatically download the payload from the object store and pass it to your task function. We also will return to you a `payloadPresignedUrl` from the `runs.retrieve` SDK function so you can download the payload if needed:
```ts
import { runs } from "@trigger.dev/sdk/v3";
const run = await runs.retrieve(handle);
if (run.payloadPresignedUrl) {
const response = await fetch(run.payloadPresignedUrl);
const payload = await response.json();
console.log("Payload", payload);
}
```
<Note>
We also use this same system for dealing with large task outputs, and subsequently will return a
corresponding `outputPresignedUrl`. Task outputs are limited to 100MB.
</Note>
If you need to pass larger payloads, you'll need to upload the payload to your own storage and pass a URL to the file in the payload instead. For example, uploading to S3 and then sending a presigned URL that expires in URL:
<CodeGroup>
```ts /yourServer.ts
import { myTask } from "./trigger/myTasks";
import { s3Client, getSignedUrl, PutObjectCommand, GetObjectCommand } from "./s3";
import { createReadStream } from "node:fs";
// Upload file to S3
await s3Client.send(
new PutObjectCommand({
Bucket: "my-bucket",
Key: "myfile.json",
Body: createReadStream("large-payload.json"),
})
);
// Create presigned URL
const presignedUrl = await getSignedUrl(
s3Client,
new GetObjectCommand({
Bucket: "my-bucket",
Key: "my-file.json",
}),
{
expiresIn: 3600, // expires in 1 hour
}
);
// Now send the URL to the task
const handle = await myTask.trigger({
url: presignedUrl,
});
```
```ts /trigger/myTasks.ts
import { task } from "@trigger.dev/sdk/v3";
export const myTask = task({
id: "my-task",
run: async (payload: { url: string }) => {
// Download the file from the URL
const response = await fetch(payload.url);
const data = await response.json();
// Do something with the data
},
});
```
</CodeGroup>
### Batch Triggering
When using `batchTrigger` or `batchTriggerAndWait`, the total size of all payloads cannot exceed 10MB. This means if you are doing a batch of 100 runs, each payload should be less than 100KB.
## Delayed runs
When you want to trigger a task now, but have it run at a later time, you can use the `delay` option:
```ts
// Delay the task run by 1 hour
await myTask.trigger({ some: "data" }, { delay: "1h" });
// Delay the task run by 88 seconds
await myTask.trigger({ some: "data" }, { delay: "88s" });
// Delay the task run by 1 hour and 52 minutes and 18 seconds
await myTask.trigger({ some: "data" }, { delay: "1h52m18s" });
// Delay until a specific time
await myTask.trigger({ some: "data" }, { delay: "2024-12-01T00:00:00" });
// Delay using a Date object
await myTask.trigger({ some: "data" }, { delay: new Date(Date.now() + 1000 * 60 * 60) });
```
Runs that are delayed and have not been enqueued yet will display in the dashboard with a "Delayed" status:
![Delayed run in the dashboard](/images/v3/delayed-runs.png)
<Note>
Delayed runs will be enqueued at the time specified, and will run as soon as possible after that
time, just as a normally triggered run would.
</Note>
You can cancel a delayed run using the `runs.cancel` SDK function:
```ts
import { runs } from "@trigger.dev/sdk/v3";
await runs.cancel("run_1234");
```
You can also reschedule a delayed run using the `runs.reschedule` SDK function:
```ts
import { runs } from "@trigger.dev/sdk/v3";
// The delay option here takes the same format as the trigger delay option
await runs.reschedule("run_1234", { delay: "1h" });
```
The `delay` option is also available when using `batchTrigger`:
```ts
await myTask.batchTrigger([{ payload: { some: "data" }, options: { delay: "1h" } }]);
```
## TTL
You can set a TTL (time to live) when triggering a task, which will automatically expire the run if it hasn't started within the specified time. This is useful for ensuring that a run doesn't get stuck in the queue for too long.
<Note>
All runs in development have a default `ttl` of 10 minutes. You can disable this by setting the
`ttl` option.
</Note>
```ts
import { myTask } from "./trigger/myTasks";
// Expire the run if it hasn't started within 1 hour
await myTask.trigger({ some: "data" }, { ttl: "1h" });
// If you specify a number, it will be treated as seconds
await myTask.trigger({ some: "data" }, { ttl: 3600 }); // 1 hour
```
When a run is expired, it will be marked as "Expired" in the dashboard:
![Expired runs in the dashboard](/images/v3/expired-runs.png)
### Delayed runs and TTL
When you use both `delay` and `ttl`, the TTL will start counting down from the time the run is enqueued, not from the time the run is triggered.
So for example, when using the following code:
```ts
await myTask.trigger({ some: "data" }, { delay: "10m", ttl: "1h" });
```
The timeline would look like this:
1. The run is created at 12:00:00
2. The run is enqueued at 12:10:00
3. The TTL starts counting down from 12:10:00
4. If the run hasn't started by 13:10:00, it will be expired
For this reason, the `ttl` option only accepts durations and not absolute timestamps.
+31
View File
@@ -1,5 +1,36 @@
# @trigger.dev/airtable
## 3.0.0-beta.45
### Patch Changes
- Updated dependencies [0e77e7ef7]
- @trigger.dev/sdk@3.0.0-beta.45
- @trigger.dev/integration-kit@3.0.0-beta.45
## 3.0.0-beta.44
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.44
- @trigger.dev/sdk@3.0.0-beta.44
## 3.0.0-beta.43
### Patch Changes
- Updated dependencies [34ca7667d]
- @trigger.dev/sdk@3.0.0-beta.43
- @trigger.dev/integration-kit@3.0.0-beta.43
## 3.0.0-beta.42
### Patch Changes
- Updated dependencies [ecef19966]
- @trigger.dev/sdk@3.0.0-beta.42
- @trigger.dev/integration-kit@3.0.0-beta.42
## 3.0.0-beta.41
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/airtable",
"version": "3.0.0-beta.41",
"version": "3.0.0-beta.45",
"description": "Trigger.dev integration for airtable",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,8 +25,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.41",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.41",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.45",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.45",
"airtable": "^0.12.1",
"zod": "3.22.3"
},
+31
View File
@@ -1,5 +1,36 @@
# @trigger.dev/github
## 3.0.0-beta.45
### Patch Changes
- Updated dependencies [0e77e7ef7]
- @trigger.dev/sdk@3.0.0-beta.45
- @trigger.dev/integration-kit@3.0.0-beta.45
## 3.0.0-beta.44
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.44
- @trigger.dev/sdk@3.0.0-beta.44
## 3.0.0-beta.43
### Patch Changes
- Updated dependencies [34ca7667d]
- @trigger.dev/sdk@3.0.0-beta.43
- @trigger.dev/integration-kit@3.0.0-beta.43
## 3.0.0-beta.42
### Patch Changes
- Updated dependencies [ecef19966]
- @trigger.dev/sdk@3.0.0-beta.42
- @trigger.dev/integration-kit@3.0.0-beta.42
## 3.0.0-beta.41
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/github",
"version": "3.0.0-beta.41",
"version": "3.0.0-beta.45",
"description": "The official GitHub integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -30,8 +30,8 @@
"@octokit/request-error": "^5.0.1",
"@octokit/webhooks": "^12.0.10",
"octokit": "^3.1.2",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.41",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.41",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.45",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.45",
"zod": "3.22.3"
},
"engines": {
+31
View File
@@ -1,5 +1,36 @@
# @trigger.dev/linear
## 3.0.0-beta.45
### Patch Changes
- Updated dependencies [0e77e7ef7]
- @trigger.dev/sdk@3.0.0-beta.45
- @trigger.dev/integration-kit@3.0.0-beta.45
## 3.0.0-beta.44
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.44
- @trigger.dev/sdk@3.0.0-beta.44
## 3.0.0-beta.43
### Patch Changes
- Updated dependencies [34ca7667d]
- @trigger.dev/sdk@3.0.0-beta.43
- @trigger.dev/integration-kit@3.0.0-beta.43
## 3.0.0-beta.42
### Patch Changes
- Updated dependencies [ecef19966]
- @trigger.dev/sdk@3.0.0-beta.42
- @trigger.dev/integration-kit@3.0.0-beta.42
## 3.0.0-beta.41
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/linear",
"version": "3.0.0-beta.41",
"version": "3.0.0-beta.45",
"description": "Trigger.dev integration for @linear/sdk",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@linear/sdk": "^8.0.0",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.41",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.41",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.45",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.45",
"zod": "3.22.3"
},
"engines": {
+31
View File
@@ -1,5 +1,36 @@
# @trigger.dev/slack
## 3.0.0-beta.45
### Patch Changes
- Updated dependencies [0e77e7ef7]
- @trigger.dev/sdk@3.0.0-beta.45
- @trigger.dev/integration-kit@3.0.0-beta.45
## 3.0.0-beta.44
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.44
- @trigger.dev/sdk@3.0.0-beta.44
## 3.0.0-beta.43
### Patch Changes
- Updated dependencies [34ca7667d]
- @trigger.dev/sdk@3.0.0-beta.43
- @trigger.dev/integration-kit@3.0.0-beta.43
## 3.0.0-beta.42
### Patch Changes
- Updated dependencies [ecef19966]
- @trigger.dev/sdk@3.0.0-beta.42
- @trigger.dev/integration-kit@3.0.0-beta.42
## 3.0.0-beta.41
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/openai",
"version": "3.0.0-beta.41",
"version": "3.0.0-beta.45",
"description": "The official OpenAI integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -42,8 +42,8 @@
},
"dependencies": {
"openai": "^4.16.1",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.41",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.41"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.45",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.45"
},
"engines": {
"node": ">=18.0.0"
+31
View File
@@ -1,5 +1,36 @@
# @trigger.dev/plain
## 3.0.0-beta.45
### Patch Changes
- Updated dependencies [0e77e7ef7]
- @trigger.dev/sdk@3.0.0-beta.45
- @trigger.dev/integration-kit@3.0.0-beta.45
## 3.0.0-beta.44
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.44
- @trigger.dev/sdk@3.0.0-beta.44
## 3.0.0-beta.43
### Patch Changes
- Updated dependencies [34ca7667d]
- @trigger.dev/sdk@3.0.0-beta.43
- @trigger.dev/integration-kit@3.0.0-beta.43
## 3.0.0-beta.42
### Patch Changes
- Updated dependencies [ecef19966]
- @trigger.dev/sdk@3.0.0-beta.42
- @trigger.dev/integration-kit@3.0.0-beta.42
## 3.0.0-beta.41
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/plain",
"version": "3.0.0-beta.41",
"version": "3.0.0-beta.45",
"description": "The official Plain.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.41",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.41",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.45",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.45",
"@team-plain/typescript-sdk": "^2.7.0"
},
"engines": {
+31
View File
@@ -1,5 +1,36 @@
# @trigger.dev/replicate
## 3.0.0-beta.45
### Patch Changes
- Updated dependencies [0e77e7ef7]
- @trigger.dev/sdk@3.0.0-beta.45
- @trigger.dev/integration-kit@3.0.0-beta.45
## 3.0.0-beta.44
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.44
- @trigger.dev/sdk@3.0.0-beta.44
## 3.0.0-beta.43
### Patch Changes
- Updated dependencies [34ca7667d]
- @trigger.dev/sdk@3.0.0-beta.43
- @trigger.dev/integration-kit@3.0.0-beta.43
## 3.0.0-beta.42
### Patch Changes
- Updated dependencies [ecef19966]
- @trigger.dev/sdk@3.0.0-beta.42
- @trigger.dev/integration-kit@3.0.0-beta.42
## 3.0.0-beta.41
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/replicate",
"version": "3.0.0-beta.41",
"version": "3.0.0-beta.45",
"description": "Trigger.dev integration for replicate",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,8 +25,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.41",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.41",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.45",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.45",
"replicate": "^0.18.1",
"zod": "3.22.3"
},
+31
View File
@@ -1,5 +1,36 @@
# @trigger.dev/resend
## 3.0.0-beta.45
### Patch Changes
- Updated dependencies [0e77e7ef7]
- @trigger.dev/sdk@3.0.0-beta.45
- @trigger.dev/integration-kit@3.0.0-beta.45
## 3.0.0-beta.44
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.44
- @trigger.dev/sdk@3.0.0-beta.44
## 3.0.0-beta.43
### Patch Changes
- Updated dependencies [34ca7667d]
- @trigger.dev/sdk@3.0.0-beta.43
- @trigger.dev/integration-kit@3.0.0-beta.43
## 3.0.0-beta.42
### Patch Changes
- Updated dependencies [ecef19966]
- @trigger.dev/sdk@3.0.0-beta.42
- @trigger.dev/integration-kit@3.0.0-beta.42
## 3.0.0-beta.41
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/resend",
"version": "3.0.0-beta.41",
"version": "3.0.0-beta.45",
"description": "The official Resend.com integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"build:tsup": "tsup"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.41",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.41",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.45",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.45",
"resend": "^2.1.0"
},
"engines": {
+31
View File
@@ -1,5 +1,36 @@
# @trigger.dev/sendgrid
## 3.0.0-beta.45
### Patch Changes
- Updated dependencies [0e77e7ef7]
- @trigger.dev/sdk@3.0.0-beta.45
- @trigger.dev/integration-kit@3.0.0-beta.45
## 3.0.0-beta.44
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.44
- @trigger.dev/sdk@3.0.0-beta.44
## 3.0.0-beta.43
### Patch Changes
- Updated dependencies [34ca7667d]
- @trigger.dev/sdk@3.0.0-beta.43
- @trigger.dev/integration-kit@3.0.0-beta.43
## 3.0.0-beta.42
### Patch Changes
- Updated dependencies [ecef19966]
- @trigger.dev/sdk@3.0.0-beta.42
- @trigger.dev/integration-kit@3.0.0-beta.42
## 3.0.0-beta.41
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/sendgrid",
"version": "3.0.0-beta.41",
"version": "3.0.0-beta.45",
"description": "Trigger.dev integration for @sendgrid/mail",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@sendgrid/mail": "^7.7.0",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.41",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.41"
"@trigger.dev/sdk": "workspace:^3.0.0-beta.45",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.45"
},
"engines": {
"node": ">=16.8.0"
+31
View File
@@ -1,5 +1,36 @@
# @trigger.dev/shopify
## 3.0.0-beta.45
### Patch Changes
- Updated dependencies [0e77e7ef7]
- @trigger.dev/sdk@3.0.0-beta.45
- @trigger.dev/integration-kit@3.0.0-beta.45
## 3.0.0-beta.44
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.44
- @trigger.dev/sdk@3.0.0-beta.44
## 3.0.0-beta.43
### Patch Changes
- Updated dependencies [34ca7667d]
- @trigger.dev/sdk@3.0.0-beta.43
- @trigger.dev/integration-kit@3.0.0-beta.43
## 3.0.0-beta.42
### Patch Changes
- Updated dependencies [ecef19966]
- @trigger.dev/sdk@3.0.0-beta.42
- @trigger.dev/integration-kit@3.0.0-beta.42
## 3.0.0-beta.41
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/shopify",
"version": "3.0.0-beta.41",
"version": "3.0.0-beta.45",
"description": "Trigger.dev integration for @shopify/shopify-api",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@shopify/shopify-api": "^8.0.2",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.41",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.41",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.45",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.45",
"zod": "3.22.3"
},
"engines": {
+27
View File
@@ -1,5 +1,32 @@
# @trigger.dev/slack
## 3.0.0-beta.45
### Patch Changes
- Updated dependencies [0e77e7ef7]
- @trigger.dev/sdk@3.0.0-beta.45
## 3.0.0-beta.44
### Patch Changes
- @trigger.dev/sdk@3.0.0-beta.44
## 3.0.0-beta.43
### Patch Changes
- Updated dependencies [34ca7667d]
- @trigger.dev/sdk@3.0.0-beta.43
## 3.0.0-beta.42
### Patch Changes
- Updated dependencies [ecef19966]
- @trigger.dev/sdk@3.0.0-beta.42
## 3.0.0-beta.41
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/slack",
"version": "3.0.0-beta.41",
"version": "3.0.0-beta.45",
"description": "The official Slack integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,7 +25,7 @@
},
"dependencies": {
"@slack/web-api": "^6.8.1",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.41",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.45",
"zod": "3.22.3"
},
"engines": {
+31
View File
@@ -1,5 +1,36 @@
# @trigger.dev/stripe
## 3.0.0-beta.45
### Patch Changes
- Updated dependencies [0e77e7ef7]
- @trigger.dev/sdk@3.0.0-beta.45
- @trigger.dev/integration-kit@3.0.0-beta.45
## 3.0.0-beta.44
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.44
- @trigger.dev/sdk@3.0.0-beta.44
## 3.0.0-beta.43
### Patch Changes
- Updated dependencies [34ca7667d]
- @trigger.dev/sdk@3.0.0-beta.43
- @trigger.dev/integration-kit@3.0.0-beta.43
## 3.0.0-beta.42
### Patch Changes
- Updated dependencies [ecef19966]
- @trigger.dev/sdk@3.0.0-beta.42
- @trigger.dev/integration-kit@3.0.0-beta.42
## 3.0.0-beta.41
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/stripe",
"version": "3.0.0-beta.41",
"version": "3.0.0-beta.45",
"description": "Trigger.dev integration for stripe",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -25,8 +25,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.41",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.41",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.45",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.45",
"stripe": "^12.14.0",
"zod": "3.22.3"
},
+31
View File
@@ -1,5 +1,36 @@
# @trigger.dev/supabase
## 3.0.0-beta.45
### Patch Changes
- Updated dependencies [0e77e7ef7]
- @trigger.dev/sdk@3.0.0-beta.45
- @trigger.dev/integration-kit@3.0.0-beta.45
## 3.0.0-beta.44
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.44
- @trigger.dev/sdk@3.0.0-beta.44
## 3.0.0-beta.43
### Patch Changes
- Updated dependencies [34ca7667d]
- @trigger.dev/sdk@3.0.0-beta.43
- @trigger.dev/integration-kit@3.0.0-beta.43
## 3.0.0-beta.42
### Patch Changes
- Updated dependencies [ecef19966]
- @trigger.dev/sdk@3.0.0-beta.42
- @trigger.dev/integration-kit@3.0.0-beta.42
## 3.0.0-beta.41
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/supabase",
"version": "3.0.0-beta.41",
"version": "3.0.0-beta.45",
"description": "Trigger.dev integration for @supabase/supabase-js",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -26,8 +26,8 @@
},
"dependencies": {
"@supabase/supabase-js": "^2.26.0",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.41",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.41",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.45",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.45",
"supabase-management-js": "^1.0.0",
"zod": "3.22.3"
},
+31
View File
@@ -1,5 +1,36 @@
# @trigger.dev/typeform
## 3.0.0-beta.45
### Patch Changes
- Updated dependencies [0e77e7ef7]
- @trigger.dev/sdk@3.0.0-beta.45
- @trigger.dev/integration-kit@3.0.0-beta.45
## 3.0.0-beta.44
### Patch Changes
- @trigger.dev/integration-kit@3.0.0-beta.44
- @trigger.dev/sdk@3.0.0-beta.44
## 3.0.0-beta.43
### Patch Changes
- Updated dependencies [34ca7667d]
- @trigger.dev/sdk@3.0.0-beta.43
- @trigger.dev/integration-kit@3.0.0-beta.43
## 3.0.0-beta.42
### Patch Changes
- Updated dependencies [ecef19966]
- @trigger.dev/sdk@3.0.0-beta.42
- @trigger.dev/integration-kit@3.0.0-beta.42
## 3.0.0-beta.41
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@trigger.dev/typeform",
"version": "3.0.0-beta.41",
"version": "3.0.0-beta.45",
"description": "The official Typeform integration for Trigger.dev",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -24,8 +24,8 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.41",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.41",
"@trigger.dev/integration-kit": "workspace:^3.0.0-beta.45",
"@trigger.dev/sdk": "workspace:^3.0.0-beta.45",
"@typeform/api-client": "^1.8.0",
"zod": "3.22.3"
},

Some files were not shown because too many files have changed in this diff Show More