Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e97dcb93e | |||
| b9b86c89a7 | |||
| cd5d2ae92b | |||
| 0e77e7ef7d | |||
| 76a5c6204f | |||
| b171fde483 | |||
| 5ae3da6b4e | |||
| f565829959 | |||
| d57dec6919 | |||
| 75ec4ac6a6 | |||
| 374b6b9c0c | |||
| d0d3a64bd6 | |||
| 568da01785 | |||
| c75e29a9a7 | |||
| e5d26bd12d | |||
| b6f31ab651 | |||
| 50d46a8513 | |||
| 4cc61ac0ec | |||
| a696359c3e | |||
| 52b6f48a94 | |||
| d22a460555 | |||
| 9ba2a217a4 | |||
| 39885a427f | |||
| ccb0bc510a | |||
| 56d66ee07c | |||
| 4ca8887972 | |||
| 89bffc066c | |||
| 34ca7667d3 | |||
| 3e327acc0f | |||
| 77ad4127cb | |||
| 8a5076aacf | |||
| 5399f6bfb7 | |||
| ecef199660 | |||
| 4acfb8f4bb | |||
| 2ef278db67 | |||
| c7a55804d9 | |||
| da6a66efff | |||
| 7c36a1a4b0 | |||
| 225effb599 | |||
| 98eb6ed4f9 | |||
| 3069ebf0d8 | |||
| e133e628ca | |||
| 098932ea96 | |||
| 65f960e883 | |||
| ccbeff47e6 | |||
| 7c8f2df105 | |||
| fd44dabfe0 | |||
| 5daed3f69d | |||
| 596bf78e55 | |||
| 6ca66b76f4 | |||
| 29ef0395ce | |||
| 55d1f8c677 | |||
| 9835f4ec55 | |||
| 7fae10db23 | |||
| 8cf1f0a37d | |||
| dba4313c5c | |||
| 506613dc92 | |||
| 764df23d19 | |||
| 4b961a6ae2 | |||
| 8757fdceef | |||
| 2404e88ac5 | |||
| 88b36f5090 | |||
| b73ae3f927 | |||
| b605b892ac | |||
| 233316f7e8 | |||
| 1b90ffbb8c | |||
| b45ca4e146 | |||
| 25d15578f7 | |||
| fe865a0f49 | |||
| 0ed93a748e | |||
| e02320f65d | |||
| 85a543d8ec | |||
| 10ceb85a92 | |||
| c405ae7117 | |||
| 3687fcb61e | |||
| d4ccdf7105 | |||
| e08b4569e5 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Add an e2e suite to test compiling with v3 CLI.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Increase dev worker timeout
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Add sox and audiowaveform binaries to worker images
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
v3: Usage tracking
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
v3: Remove aggressive otel flush timeouts in dev/prod
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
v3: Trigger delayed runs and reschedule them
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
Use global setTimeout to ensure cross-runtime support
|
||||
@@ -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
|
||||
@@ -46,6 +46,7 @@
|
||||
"changesets": [
|
||||
"afraid-sheep-joke",
|
||||
"angry-eagles-trade",
|
||||
"beige-pears-explode",
|
||||
"beige-pens-dance",
|
||||
"big-tomatoes-deliver",
|
||||
"blue-pumas-whisper",
|
||||
@@ -64,13 +65,16 @@
|
||||
"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",
|
||||
"hot-buckets-behave",
|
||||
"hot-fishes-retire",
|
||||
"hot-wasps-sin",
|
||||
"itchy-chairs-itch",
|
||||
"khaki-apricots-design",
|
||||
"khaki-poems-lay",
|
||||
@@ -87,7 +91,10 @@
|
||||
"many-ligers-pump",
|
||||
"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",
|
||||
@@ -96,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",
|
||||
@@ -107,31 +117,45 @@
|
||||
"rotten-dryers-exercise",
|
||||
"rude-toys-compare",
|
||||
"selfish-ducks-sort",
|
||||
"serious-hats-rest",
|
||||
"shaggy-spoons-taste",
|
||||
"sharp-emus-compare",
|
||||
"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",
|
||||
"slow-sloths-retire",
|
||||
"smart-needles-move",
|
||||
"smart-olives-eat",
|
||||
"sour-pugs-teach",
|
||||
"spicy-frogs-remain",
|
||||
"spicy-lamps-smoke",
|
||||
"spicy-terms-bow",
|
||||
"strange-ghosts-matter",
|
||||
"strange-sheep-pull",
|
||||
"strong-lemons-add",
|
||||
"strong-owls-know",
|
||||
"strong-phones-smoke",
|
||||
"stupid-adults-sniff",
|
||||
"stupid-bulldogs-applaud",
|
||||
"sweet-ducks-remember",
|
||||
"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",
|
||||
@@ -142,7 +166,9 @@
|
||||
"tricky-bulldogs-heal",
|
||||
"tricky-keys-attack",
|
||||
"tricky-ladybugs-unite",
|
||||
"twelve-knives-notice",
|
||||
"two-pumas-wait",
|
||||
"violet-clocks-notice",
|
||||
"warm-olives-provide",
|
||||
"warm-planes-taste",
|
||||
"young-snails-sell"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Await file watcher cleanup in dev
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
v3: [prod] force flush timeout should be 1s
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Add callback to checkpoint created message
|
||||
@@ -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)
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Make deduplicationKey required when creating/updating a schedule
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Improved ESM module require error detection logic
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
v3: vercel edge runtime support
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
v3: fix otel flushing causing CLEANUP ack timeout errors by always setting a forceFlushTimeoutMillis value
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
fix: allow command login to read api url from cli args
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
---
|
||||
|
||||
v3: Adding SDK functions for triggering tasks in a typesafe way, without importing task 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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
v3: postInstall config option now replaces the postinstall script found in package.json
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@trigger.dev/sdk": patch
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
Added timezone support to schedules
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/core": patch
|
||||
---
|
||||
|
||||
v3: fix missing init output in task run function when no middleware is defined
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"trigger.dev": patch
|
||||
---
|
||||
|
||||
Fix jsonc-parser import
|
||||
@@ -1,9 +1,53 @@
|
||||
name: "🧪 E2E Tests"
|
||||
name: "E2E"
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
package:
|
||||
description: The identifier of the job to run
|
||||
default: webapp
|
||||
required: false
|
||||
type: string
|
||||
jobs:
|
||||
e2e:
|
||||
name: "🧪 E2E Tests"
|
||||
cli-v3:
|
||||
name: "🧪 CLI v3 tests"
|
||||
if: inputs.package == 'cli-v3' || inputs.package == ''
|
||||
runs-on: buildjet-8vcpu-ubuntu-2204
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
package-manager: ["npm", "pnpm", "yarn"]
|
||||
steps:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⎔ Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8.15.5
|
||||
|
||||
- name: ⎔ Setup node
|
||||
uses: buildjet/setup-node@v3
|
||||
with:
|
||||
node-version: 20.11.1
|
||||
cache: "pnpm"
|
||||
|
||||
- name: 📥 Download deps
|
||||
run: pnpm install --frozen-lockfile --filter trigger.dev...
|
||||
|
||||
- name: 🔧 Build v3 cli monorepo dependencies
|
||||
run: pnpm run build --filter trigger.dev^...
|
||||
|
||||
- name: 🔧 Build worker template files
|
||||
run: pnpm --filter trigger.dev run build:workers
|
||||
|
||||
- name: Run E2E Tests
|
||||
run: |
|
||||
PM=${{ matrix.package-manager }} pnpm --filter trigger.dev run test:e2e
|
||||
webapp:
|
||||
name: "🧪 Webapp tests"
|
||||
if: inputs.package == 'webapp' || inputs.package == ''
|
||||
runs-on: buildjet-16vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: 🐳 Login to Docker Hub
|
||||
|
||||
@@ -29,4 +29,6 @@ jobs:
|
||||
|
||||
# e2e:
|
||||
# uses: ./.github/workflows/e2e.yml
|
||||
# with:
|
||||
# package: webapp
|
||||
# secrets: inherit
|
||||
|
||||
@@ -51,6 +51,8 @@ jobs:
|
||||
|
||||
# e2e:
|
||||
# uses: ./.github/workflows/e2e.yml
|
||||
# with:
|
||||
# package: cli-v3
|
||||
# secrets: inherit
|
||||
|
||||
publish:
|
||||
|
||||
@@ -1162,13 +1162,7 @@ class TaskCoordinator {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkpoint.docker || !willSimulate) {
|
||||
socket.emit("REQUEST_EXIT", {
|
||||
version: "v1",
|
||||
});
|
||||
}
|
||||
|
||||
this.#platformSocket?.send("CHECKPOINT_CREATED", {
|
||||
const ack = await this.#platformSocket?.sendWithAck("CHECKPOINT_CREATED", {
|
||||
version: "v1",
|
||||
attemptFriendlyId: message.attemptFriendlyId,
|
||||
docker: checkpoint.docker,
|
||||
@@ -1179,6 +1173,17 @@ class TaskCoordinator {
|
||||
now: message.now,
|
||||
},
|
||||
});
|
||||
|
||||
if (ack?.keepRunAlive) {
|
||||
logger.log("keeping run alive after duration checkpoint", { runId: socket.data.runId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkpoint.docker || !willSimulate) {
|
||||
socket.emit("REQUEST_EXIT", {
|
||||
version: "v1",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("WAIT_FOR_TASK", async (message, callback) => {
|
||||
@@ -1205,13 +1210,7 @@ class TaskCoordinator {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkpoint.docker || !willSimulate) {
|
||||
socket.emit("REQUEST_EXIT", {
|
||||
version: "v1",
|
||||
});
|
||||
}
|
||||
|
||||
this.#platformSocket?.send("CHECKPOINT_CREATED", {
|
||||
const ack = await this.#platformSocket?.sendWithAck("CHECKPOINT_CREATED", {
|
||||
version: "v1",
|
||||
attemptFriendlyId: message.attemptFriendlyId,
|
||||
docker: checkpoint.docker,
|
||||
@@ -1221,6 +1220,17 @@ class TaskCoordinator {
|
||||
friendlyId: message.friendlyId,
|
||||
},
|
||||
});
|
||||
|
||||
if (ack?.keepRunAlive) {
|
||||
logger.log("keeping run alive after task checkpoint", { runId: socket.data.runId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkpoint.docker || !willSimulate) {
|
||||
socket.emit("REQUEST_EXIT", {
|
||||
version: "v1",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("WAIT_FOR_BATCH", async (message, callback) => {
|
||||
@@ -1247,13 +1257,7 @@ class TaskCoordinator {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkpoint.docker || !willSimulate) {
|
||||
socket.emit("REQUEST_EXIT", {
|
||||
version: "v1",
|
||||
});
|
||||
}
|
||||
|
||||
this.#platformSocket?.send("CHECKPOINT_CREATED", {
|
||||
const ack = await this.#platformSocket?.sendWithAck("CHECKPOINT_CREATED", {
|
||||
version: "v1",
|
||||
attemptFriendlyId: message.attemptFriendlyId,
|
||||
docker: checkpoint.docker,
|
||||
@@ -1264,6 +1268,17 @@ class TaskCoordinator {
|
||||
runFriendlyIds: message.runFriendlyIds,
|
||||
},
|
||||
});
|
||||
|
||||
if (ack?.keepRunAlive) {
|
||||
logger.log("keeping run alive after batch checkpoint", { runId: socket.data.runId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkpoint.docker || !willSimulate) {
|
||||
socket.emit("REQUEST_EXIT", {
|
||||
version: "v1",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("INDEX_TASKS", async (message, callback) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,7 +7,12 @@ import {
|
||||
TaskOperationsIndexOptions,
|
||||
TaskOperationsRestoreOptions,
|
||||
} from "@trigger.dev/core-apps";
|
||||
import { Machine, PostStartCauses, PreStopCauses, EnvironmentType } from "@trigger.dev/core/v3";
|
||||
import {
|
||||
MachinePreset,
|
||||
PostStartCauses,
|
||||
PreStopCauses,
|
||||
EnvironmentType,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { randomUUID } from "crypto";
|
||||
import { TaskMonitor } from "./taskMonitor";
|
||||
import { PodCleaner } from "./podCleaner";
|
||||
@@ -16,6 +21,7 @@ const RUNTIME_ENV = process.env.KUBERNETES_PORT ? "kubernetes" : "local";
|
||||
const NODE_NAME = process.env.NODE_NAME || "local";
|
||||
const OTEL_EXPORTER_OTLP_ENDPOINT =
|
||||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://0.0.0.0:4318";
|
||||
const POD_CLEANER_INTERVAL_SECONDS = Number(process.env.POD_CLEANER_INTERVAL_SECONDS || "300");
|
||||
|
||||
const logger = new SimpleLogger(`[${NODE_NAME}]`);
|
||||
logger.log(`running in ${RUNTIME_ENV} mode`);
|
||||
@@ -398,10 +404,10 @@ class KubernetesTaskOperations implements TaskOperations {
|
||||
};
|
||||
}
|
||||
|
||||
#getResourcesFromMachineConfig(config: Machine): ComputeResources {
|
||||
#getResourcesFromMachineConfig(preset: MachinePreset): ComputeResources {
|
||||
return {
|
||||
cpu: `${config.cpu}`,
|
||||
memory: `${config.memory}G`,
|
||||
cpu: `${preset.cpu}`,
|
||||
memory: `${preset.memory}G`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -523,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);
|
||||
}
|
||||
@@ -555,7 +562,7 @@ taskMonitor.start();
|
||||
const podCleaner = new PodCleaner({
|
||||
runtimeEnv: RUNTIME_ENV,
|
||||
namespace: "default",
|
||||
intervalInSeconds: 300,
|
||||
intervalInSeconds: POD_CLEANER_INTERVAL_SECONDS,
|
||||
});
|
||||
|
||||
podCleaner.start();
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ type DateTimeProps = {
|
||||
timeZone?: string;
|
||||
includeSeconds?: boolean;
|
||||
includeTime?: boolean;
|
||||
showTimezone?: boolean;
|
||||
};
|
||||
|
||||
export const DateTime = ({
|
||||
@@ -13,6 +14,7 @@ export const DateTime = ({
|
||||
timeZone,
|
||||
includeSeconds = true,
|
||||
includeTime = true,
|
||||
showTimezone = false,
|
||||
}: DateTimeProps) => {
|
||||
const locales = useLocales();
|
||||
|
||||
@@ -42,7 +44,12 @@ export const DateTime = ({
|
||||
);
|
||||
}, [locales, includeSeconds, realDate]);
|
||||
|
||||
return <Fragment>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</Fragment>;
|
||||
return (
|
||||
<Fragment>
|
||||
{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}
|
||||
{showTimezone ? ` (${timeZone ?? "UTC"})` : null}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export function formatDateTime(
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { cn } from "~/utils/cn";
|
||||
import { ShortcutKey } from "./ShortcutKey";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { MatchSorterOptions, matchSorter } from "match-sorter";
|
||||
|
||||
const sizes = {
|
||||
small: {
|
||||
@@ -75,7 +76,10 @@ export interface SelectProps<TValue extends string | string[], TItem>
|
||||
showHeading?: boolean;
|
||||
items?: TItem[] | Section<TItem>[];
|
||||
empty?: React.ReactNode;
|
||||
filter?: (item: ItemFromSection<TItem>, search: string, title?: string) => boolean;
|
||||
filter?:
|
||||
| boolean
|
||||
| MatchSorterOptions<TItem>
|
||||
| ((item: ItemFromSection<TItem>, search: string, title?: string) => boolean);
|
||||
children:
|
||||
| React.ReactNode
|
||||
| ((
|
||||
@@ -129,18 +133,44 @@ export function Select<TValue extends string | string[], TItem>({
|
||||
if (!items) return [];
|
||||
if (!searchValue || !filter) return items;
|
||||
|
||||
if (typeof filter === "function") {
|
||||
if (isSection(items)) {
|
||||
return items
|
||||
.map((section) => ({
|
||||
...section,
|
||||
items: section.items.filter((item) =>
|
||||
filter(item as ItemFromSection<TItem>, searchValue, section.title)
|
||||
),
|
||||
}))
|
||||
.filter((section) => section.items.length > 0);
|
||||
}
|
||||
|
||||
return items.filter((item) => filter(item as ItemFromSection<TItem>, searchValue));
|
||||
}
|
||||
|
||||
if (typeof filter === "boolean" && filter) {
|
||||
if (isSection(items)) {
|
||||
return items
|
||||
.map((section) => ({
|
||||
...section,
|
||||
items: matchSorter(section.items, searchValue),
|
||||
}))
|
||||
.filter((section) => section.items.length > 0);
|
||||
}
|
||||
|
||||
return matchSorter(items, searchValue);
|
||||
}
|
||||
|
||||
if (isSection(items)) {
|
||||
return items
|
||||
.map((section) => ({
|
||||
...section,
|
||||
items: section.items.filter((item) =>
|
||||
filter(item as ItemFromSection<TItem>, searchValue, section.title)
|
||||
),
|
||||
items: matchSorter(section.items, searchValue, filter),
|
||||
}))
|
||||
.filter((section) => section.items.length > 0);
|
||||
}
|
||||
|
||||
return items.filter((item) => filter(item as ItemFromSection<TItem>, searchValue));
|
||||
return matchSorter(items, searchValue, filter);
|
||||
}, [searchValue, items]);
|
||||
|
||||
const enableItemShortcuts = allowItemShortcuts && matches.length === items?.length;
|
||||
|
||||
@@ -528,7 +528,7 @@ export type Tree<TData> = {
|
||||
/** A tree but flattened so it can easily be used for DOM elements */
|
||||
export type FlatTreeItem<TData> = {
|
||||
id: string;
|
||||
parentId: string | undefined;
|
||||
parentId?: string | undefined;
|
||||
children: string[];
|
||||
hasChildren: boolean;
|
||||
/** The indentation level, the root is 0 */
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useRef } from "react";
|
||||
import { SelectItem } from "../primitives/Select";
|
||||
|
||||
export function TimezoneList({ timezones }: { timezones: string[] }) {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: timezones.length,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => 28,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="max-h-[calc(min(480px,var(--popover-available-height))-2.35rem)] overflow-y-auto overscroll-contain scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
|
||||
<TimezoneCell
|
||||
key={virtualItem.key}
|
||||
size={virtualItem.size}
|
||||
start={virtualItem.start}
|
||||
timezone={timezones[virtualItem.index]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TimezoneCell({
|
||||
timezone,
|
||||
size,
|
||||
start,
|
||||
}: {
|
||||
timezone: string;
|
||||
size: number;
|
||||
start: number;
|
||||
}) {
|
||||
return (
|
||||
<SelectItem
|
||||
value={timezone}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: `${size}px`,
|
||||
transform: `translateY(${start}px)`,
|
||||
}}
|
||||
>
|
||||
{timezone}
|
||||
</SelectItem>
|
||||
);
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -186,3 +186,9 @@ export { apiRateLimiter } from "./services/apiRateLimit.server";
|
||||
export { socketIo } from "./v3/handleSocketIo.server";
|
||||
export { wss } from "./v3/handleWebsockets.server";
|
||||
export { registryProxy } from "./v3/registryProxy.server";
|
||||
import { eventLoopMonitor } from "./eventLoopMonitor.server";
|
||||
import { env } from "./env.server";
|
||||
|
||||
if (env.EVENT_LOOP_MONITOR_ENABLED === "1") {
|
||||
eventLoopMonitor.enable();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { SecretStoreOptionsSchema } from "./services/secrets/secretStoreOptionsSchema.server";
|
||||
import { z } from "zod";
|
||||
import { isValidRegex } from "./utils/regex";
|
||||
import { isValidDatabaseUrl } from "./utils/db";
|
||||
|
||||
@@ -189,6 +189,26 @@ const EnvironmentSchema = z.object({
|
||||
V2_MARQS_VERBOSE: z.string().default("0"),
|
||||
V3_MARQS_CONCURRENCY_MONITOR_ENABLED: z.string().default("0"),
|
||||
V2_MARQS_CONCURRENCY_MONITOR_ENABLED: z.string().default("0"),
|
||||
/* Usage settings */
|
||||
USAGE_EVENT_URL: z.string().optional(),
|
||||
PROD_USAGE_HEARTBEAT_INTERVAL_MS: z.coerce.number().int().optional(),
|
||||
|
||||
CENTS_PER_HOUR_MICRO: z.coerce.number().default(0),
|
||||
CENTS_PER_HOUR_SMALL_1X: z.coerce.number().default(0),
|
||||
CENTS_PER_HOUR_SMALL_2X: z.coerce.number().default(0),
|
||||
CENTS_PER_HOUR_MEDIUM_1X: z.coerce.number().default(0),
|
||||
CENTS_PER_HOUR_MEDIUM_2X: z.coerce.number().default(0),
|
||||
CENTS_PER_HOUR_LARGE_1X: z.coerce.number().default(0),
|
||||
CENTS_PER_HOUR_LARGE_2X: z.coerce.number().default(0),
|
||||
BASE_RUN_COST_IN_CENTS: z.coerce.number().default(0),
|
||||
|
||||
USAGE_OPEN_METER_API_KEY: z.string().optional(),
|
||||
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>;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { createHook } from "node:async_hooks";
|
||||
import { singleton } from "./utils/singleton";
|
||||
import { tracer } from "./v3/tracer.server";
|
||||
|
||||
const THRESHOLD_NS = 1e8; // 100ms
|
||||
|
||||
const cache = new Map<number, { type: string; start?: [number, number] }>();
|
||||
|
||||
function init(asyncId: number, type: string, triggerAsyncId: number, resource: any) {
|
||||
cache.set(asyncId, {
|
||||
type,
|
||||
});
|
||||
}
|
||||
|
||||
function destroy(asyncId: number) {
|
||||
cache.delete(asyncId);
|
||||
}
|
||||
|
||||
function before(asyncId: number) {
|
||||
const cached = cache.get(asyncId);
|
||||
|
||||
if (!cached) {
|
||||
return;
|
||||
}
|
||||
|
||||
cache.set(asyncId, {
|
||||
...cached,
|
||||
start: process.hrtime(),
|
||||
});
|
||||
}
|
||||
|
||||
function after(asyncId: number) {
|
||||
const cached = cache.get(asyncId);
|
||||
|
||||
if (!cached) {
|
||||
return;
|
||||
}
|
||||
|
||||
cache.delete(asyncId);
|
||||
|
||||
if (!cached.start) {
|
||||
return;
|
||||
}
|
||||
|
||||
const diff = process.hrtime(cached.start);
|
||||
const diffNs = diff[0] * 1e9 + diff[1];
|
||||
if (diffNs > THRESHOLD_NS) {
|
||||
const time = diffNs / 1e6; // in ms
|
||||
|
||||
const newSpan = tracer.startSpan("event-loop-blocked", {
|
||||
startTime: new Date(new Date().getTime() - time),
|
||||
attributes: {
|
||||
asyncType: cached.type,
|
||||
label: "EventLoopMonitor",
|
||||
},
|
||||
});
|
||||
|
||||
newSpan.end();
|
||||
}
|
||||
}
|
||||
|
||||
export const eventLoopMonitor = singleton("eventLoopMonitor", () => {
|
||||
const hook = createHook({ init, before, after, destroy });
|
||||
|
||||
return {
|
||||
enable: () => {
|
||||
console.log("🥸 Initializing event loop monitor");
|
||||
|
||||
hook.enable();
|
||||
},
|
||||
disable: () => {
|
||||
console.log("🥸 Disabling event loop monitor");
|
||||
|
||||
hook.disable();
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -26,7 +26,7 @@ export function useEventSource(
|
||||
const eventSource = new EventSource(url, init);
|
||||
eventSource.addEventListener(event ?? "message", handler);
|
||||
|
||||
// rest data if dependencies change
|
||||
// reset data if dependencies change
|
||||
setData(null);
|
||||
|
||||
function handler(event: MessageEvent) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export function detectResponseIsTimeout(rawBody: string, response?: Response) {
|
||||
|
||||
return (
|
||||
isResponseVercelTimeout(response) ||
|
||||
isResponseCloudfrontTimeout(response) ||
|
||||
isResponseDenoDeployTimeout(rawBody, response) ||
|
||||
isResponseCloudflareTimeout(rawBody, response)
|
||||
);
|
||||
@@ -50,3 +51,7 @@ function isResponseVercelTimeout(response: Response) {
|
||||
function isResponseDenoDeployTimeout(rawBody: string, response: Response) {
|
||||
return response.status === 502 && rawBody.includes("TIME_LIMIT");
|
||||
}
|
||||
|
||||
function isResponseCloudfrontTimeout(response: Response) {
|
||||
return response.status === 504 && typeof response.headers.get("x-amz-cf-id") === "string";
|
||||
}
|
||||
|
||||
@@ -65,59 +65,55 @@ export async function findEnvironmentById(id: string) {
|
||||
}
|
||||
|
||||
export async function createNewSession(environment: RuntimeEnvironment, ipAddress: string) {
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const session = await tx.runtimeEnvironmentSession.create({
|
||||
data: {
|
||||
environmentId: environment.id,
|
||||
ipAddress,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.runtimeEnvironment.update({
|
||||
where: {
|
||||
id: environment.id,
|
||||
},
|
||||
data: {
|
||||
currentSessionId: session.id,
|
||||
},
|
||||
});
|
||||
|
||||
return session;
|
||||
const session = await prisma.runtimeEnvironmentSession.create({
|
||||
data: {
|
||||
environmentId: environment.id,
|
||||
ipAddress,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.runtimeEnvironment.update({
|
||||
where: {
|
||||
id: environment.id,
|
||||
},
|
||||
data: {
|
||||
currentSessionId: session.id,
|
||||
},
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function disconnectSession(environmentId: string) {
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const environment = await tx.runtimeEnvironment.findUnique({
|
||||
where: {
|
||||
id: environmentId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!environment || !environment.currentSessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const session = await tx.runtimeEnvironmentSession.update({
|
||||
where: {
|
||||
id: environment.currentSessionId,
|
||||
},
|
||||
data: {
|
||||
disconnectedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await tx.runtimeEnvironment.update({
|
||||
where: {
|
||||
id: environment.id,
|
||||
},
|
||||
data: {
|
||||
currentSessionId: null,
|
||||
},
|
||||
});
|
||||
|
||||
return session;
|
||||
const environment = await prisma.runtimeEnvironment.findUnique({
|
||||
where: {
|
||||
id: environmentId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!environment || !environment.currentSessionId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const session = await prisma.runtimeEnvironmentSession.update({
|
||||
where: {
|
||||
id: environment.currentSessionId,
|
||||
},
|
||||
data: {
|
||||
disconnectedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.runtimeEnvironment.update({
|
||||
where: {
|
||||
id: environment.id,
|
||||
},
|
||||
data: {
|
||||
currentSessionId: null,
|
||||
},
|
||||
});
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
type DisplayableInputEnvironment = Prisma.RuntimeEnvironmentGetPayload<{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,13 +107,18 @@ export class ApiRetrieveRunPresenter extends BasePresenter {
|
||||
version: taskRun.lockedToVersion ? taskRun.lockedToVersion.version : undefined,
|
||||
createdAt: taskRun.createdAt ?? undefined,
|
||||
updatedAt: taskRun.updatedAt ?? undefined,
|
||||
startedAt: taskRun.lockedAt ?? undefined,
|
||||
startedAt: taskRun.startedAt ?? taskRun.lockedAt ?? undefined,
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { getTimezones } from "~/utils/timezones.server";
|
||||
|
||||
type EditScheduleOptions = {
|
||||
userId: string;
|
||||
@@ -74,6 +75,7 @@ export class EditSchedulePresenter {
|
||||
return {
|
||||
possibleTasks: possibleTasks.map((task) => task.slug),
|
||||
possibleEnvironments,
|
||||
possibleTimezones: getTimezones(),
|
||||
schedule: await this.#getExistingSchedule(friendlyId, possibleEnvironments),
|
||||
};
|
||||
}
|
||||
@@ -91,6 +93,7 @@ export class EditSchedulePresenter {
|
||||
externalId: true,
|
||||
deduplicationKey: true,
|
||||
userProvidedDeduplicationKey: true,
|
||||
timezone: true,
|
||||
taskIdentifier: true,
|
||||
instances: {
|
||||
select: {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { Direction } from "~/components/runs/RunStatuses";
|
||||
import { FINISHED_STATUSES } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { sqlDatabaseSchema } from "~/db.server";
|
||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { CANCELLABLE_STATUSES } from "~/v3/services/cancelTaskRun.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
import { isCancellableRunStatus } from "~/v3/taskStatus";
|
||||
|
||||
export type RunListOptions = {
|
||||
userId?: string;
|
||||
@@ -156,11 +156,15 @@ export class RunListPresenter extends BasePresenter {
|
||||
runtimeEnvironmentId: string;
|
||||
status: TaskRunStatus;
|
||||
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
|
||||
@@ -172,11 +176,15 @@ export class RunListPresenter extends BasePresenter {
|
||||
tr."runtimeEnvironmentId" AS "runtimeEnvironmentId",
|
||||
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
|
||||
@@ -272,13 +280,16 @@ export class RunListPresenter extends BasePresenter {
|
||||
|
||||
const hasFinished = FINISHED_STATUSES.includes(run.status);
|
||||
|
||||
const startedAt = run.startedAt ?? run.lockedAt;
|
||||
|
||||
return {
|
||||
id: run.id,
|
||||
friendlyId: run.runFriendlyId,
|
||||
number: Number(run.number),
|
||||
createdAt: run.createdAt.toISOString(),
|
||||
updatedAt: run.updatedAt.toISOString(),
|
||||
startedAt: run.lockedAt ? run.lockedAt.toISOString() : undefined,
|
||||
startedAt: startedAt ? startedAt.toISOString() : undefined,
|
||||
delayUntil: run.delayUntil ? run.delayUntil.toISOString() : undefined,
|
||||
hasFinished,
|
||||
finishedAt: hasFinished ? run.updatedAt.toISOString() : undefined,
|
||||
isTest: run.isTest,
|
||||
@@ -287,9 +298,11 @@ export class RunListPresenter extends BasePresenter {
|
||||
taskIdentifier: run.taskIdentifier,
|
||||
spanId: run.spanId,
|
||||
isReplayable: true,
|
||||
isCancellable: CANCELLABLE_STATUSES.includes(run.status),
|
||||
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: {
|
||||
|
||||
@@ -94,17 +94,24 @@ export class RunStreamPresenter {
|
||||
|
||||
eventEmitter.removeAllListeners();
|
||||
|
||||
unsubscribe().catch((error) => {
|
||||
logger.error("RunStreamPresenter.abort.unsubscribe", {
|
||||
runFriendlyId,
|
||||
traceId: run.traceId,
|
||||
error: {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
},
|
||||
unsubscribe()
|
||||
.then(() => {
|
||||
logger.info("RunStreamPresenter.abort.unsubscribe succeeded", {
|
||||
runFriendlyId,
|
||||
traceId: run.traceId,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("RunStreamPresenter.abort.unsubscribe failed", {
|
||||
runFriendlyId,
|
||||
traceId: run.traceId,
|
||||
error: {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export type ScheduleListItem = {
|
||||
userProvidedDeduplicationKey: boolean;
|
||||
cron: string;
|
||||
cronDescription: string;
|
||||
timezone: string;
|
||||
externalId: string | null;
|
||||
nextRun: Date;
|
||||
lastRun: Date | undefined;
|
||||
@@ -36,7 +37,6 @@ export type ScheduleList = Awaited<ReturnType<ScheduleListPresenter["call"]>>;
|
||||
export type ScheduleListAppliedFilters = ScheduleList["filters"];
|
||||
|
||||
export class ScheduleListPresenter extends BasePresenter {
|
||||
|
||||
public async call({
|
||||
userId,
|
||||
projectId,
|
||||
@@ -71,12 +71,23 @@ export class ScheduleListPresenter extends BasePresenter {
|
||||
},
|
||||
},
|
||||
},
|
||||
organization: {
|
||||
select: {
|
||||
maximumSchedulesLimit: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
id: projectId,
|
||||
},
|
||||
});
|
||||
|
||||
const schedulesCount = await this._prisma.taskSchedule.count({
|
||||
where: {
|
||||
projectId,
|
||||
},
|
||||
});
|
||||
|
||||
//get all possible scheduled tasks
|
||||
const possibleTasks = await this._replica.backgroundWorkerTask.findMany({
|
||||
distinct: ["slug"],
|
||||
@@ -140,6 +151,7 @@ export class ScheduleListPresenter extends BasePresenter {
|
||||
userProvidedDeduplicationKey: true,
|
||||
generatorExpression: true,
|
||||
generatorDescription: true,
|
||||
timezone: true,
|
||||
externalId: true,
|
||||
instances: {
|
||||
select: {
|
||||
@@ -218,10 +230,11 @@ export class ScheduleListPresenter extends BasePresenter {
|
||||
userProvidedDeduplicationKey: schedule.userProvidedDeduplicationKey,
|
||||
cron: schedule.generatorExpression,
|
||||
cronDescription: schedule.generatorDescription,
|
||||
timezone: schedule.timezone,
|
||||
active: schedule.active,
|
||||
externalId: schedule.externalId,
|
||||
lastRun: latestRun?.createdAt,
|
||||
nextRun: calculateNextScheduledTimestamp(schedule.generatorExpression),
|
||||
nextRun: calculateNextScheduledTimestamp(schedule.generatorExpression, schedule.timezone),
|
||||
environments: schedule.instances.map((instance) => {
|
||||
const environment = project.environments.find((env) => env.id === instance.environmentId);
|
||||
if (!environment) {
|
||||
@@ -245,6 +258,10 @@ export class ScheduleListPresenter extends BasePresenter {
|
||||
return displayableEnvironment(environment, userId);
|
||||
}),
|
||||
hasFilters,
|
||||
limits: {
|
||||
used: schedulesCount,
|
||||
limit: project.organization.maximumSchedulesLimit,
|
||||
},
|
||||
filters: {
|
||||
tasks,
|
||||
environments,
|
||||
|
||||
@@ -311,7 +311,7 @@ export class TaskListPresenter extends BasePresenter {
|
||||
>`
|
||||
SELECT
|
||||
tr."taskIdentifier",
|
||||
AVG(EXTRACT(EPOCH FROM (tr."updatedAt" - tr."lockedAt"))) as duration
|
||||
AVG(EXTRACT(EPOCH FROM (tr."updatedAt" - COALESCE(tr."startedAt", tr."lockedAt")))) as duration
|
||||
FROM
|
||||
${sqlDatabaseSchema}."TaskRun" as tr
|
||||
WHERE
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
TaskTriggerSource,
|
||||
} from "@trigger.dev/database";
|
||||
import { sqlDatabaseSchema, PrismaClient, prisma } from "~/db.server";
|
||||
import { getTimezones } from "~/utils/timezones.server";
|
||||
import { getUsername } from "~/utils/username";
|
||||
|
||||
type TestTaskOptions = {
|
||||
@@ -37,6 +38,7 @@ export type TestTask =
|
||||
| {
|
||||
triggerSource: "SCHEDULED";
|
||||
task: Task;
|
||||
possibleTimezones: string[];
|
||||
runs: ScheduledRun[];
|
||||
};
|
||||
|
||||
@@ -61,6 +63,7 @@ export type ScheduledRun = Omit<RawRun, "number" | "payload"> & {
|
||||
timestamp: Date;
|
||||
lastTimestamp?: Date;
|
||||
externalId?: string;
|
||||
timezone: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -168,9 +171,11 @@ export class TestTaskPresenter {
|
||||
),
|
||||
};
|
||||
case "SCHEDULED":
|
||||
const possibleTimezones = getTimezones();
|
||||
return {
|
||||
triggerSource: "SCHEDULED",
|
||||
task: taskWithEnvironment,
|
||||
possibleTimezones,
|
||||
runs: (
|
||||
await Promise.all(
|
||||
latestRuns.map(async (r) => {
|
||||
@@ -195,6 +200,9 @@ export class TestTaskPresenter {
|
||||
|
||||
async function getScheduleTaskRunPayload(run: RawRun) {
|
||||
const payload = await parsePacket({ data: run.payload, dataType: run.payloadType });
|
||||
if (!payload.timezone) {
|
||||
payload.timezone = "UTC";
|
||||
}
|
||||
const parsed = ScheduledTaskPayload.safeParse(payload);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ScheduleObject } from "@trigger.dev/core/v3";
|
||||
import { PrismaClient, prisma } from "~/db.server";
|
||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
import { nextScheduledTimestamps } from "~/v3/utils/calculateNextSchedule.server";
|
||||
import { RunListPresenter } from "./RunListPresenter.server";
|
||||
import { ScheduleObject } from "@trigger.dev/core/v3";
|
||||
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
|
||||
|
||||
type ViewScheduleOptions = {
|
||||
userId?: string;
|
||||
@@ -24,6 +24,7 @@ export class ViewSchedulePresenter {
|
||||
friendlyId: true,
|
||||
generatorExpression: true,
|
||||
generatorDescription: true,
|
||||
timezone: true,
|
||||
externalId: true,
|
||||
deduplicationKey: true,
|
||||
userProvidedDeduplicationKey: true,
|
||||
@@ -68,7 +69,7 @@ export class ViewSchedulePresenter {
|
||||
}
|
||||
|
||||
const nextRuns = schedule.active
|
||||
? nextScheduledTimestamps(schedule.generatorExpression, new Date(), 5)
|
||||
? nextScheduledTimestamps(schedule.generatorExpression, schedule.timezone, new Date(), 5)
|
||||
: [];
|
||||
|
||||
const runPresenter = new RunListPresenter(this.#prismaClient);
|
||||
@@ -82,6 +83,7 @@ export class ViewSchedulePresenter {
|
||||
return {
|
||||
schedule: {
|
||||
...schedule,
|
||||
timezone: schedule.timezone,
|
||||
cron: schedule.generatorExpression,
|
||||
cronDescription: schedule.generatorDescription,
|
||||
nextRuns,
|
||||
@@ -105,6 +107,7 @@ export class ViewSchedulePresenter {
|
||||
expression: result.schedule.cron,
|
||||
description: result.schedule.cronDescription,
|
||||
},
|
||||
timezone: result.schedule.timezone,
|
||||
externalId: result.schedule.externalId ?? undefined,
|
||||
deduplicationKey: result.schedule.userProvidedDeduplicationKey
|
||||
? result.schedule.deduplicationKey ?? undefined
|
||||
|
||||
+20
-6
@@ -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>
|
||||
|
||||
+66
-23
@@ -1,12 +1,13 @@
|
||||
import {
|
||||
BoltSlashIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
MagnifyingGlassMinusIcon,
|
||||
MagnifyingGlassPlusIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import type { Location } from "@remix-run/react";
|
||||
import { useParams, useRevalidator } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { useLoaderData, useParams, useRevalidator } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs, SerializeFrom } from "@remix-run/server-runtime";
|
||||
import { Virtualizer } from "@tanstack/react-virtual";
|
||||
import {
|
||||
formatDurationMilliseconds,
|
||||
@@ -17,10 +18,10 @@ import { RuntimeEnvironmentType } from "@trigger.dev/database";
|
||||
import { motion } from "framer-motion";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useHotkeys } from "react-hotkeys-hook";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { ShowParentIcon, ShowParentIconSelected } from "~/assets/icons/ShowParentIcon";
|
||||
import tileBgPath from "~/assets/images/error-banner-tile@2x.png";
|
||||
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
|
||||
import { MainCenteredContainer, PageBody } from "~/components/layout/AppLayout";
|
||||
@@ -32,6 +33,7 @@ import { Input } from "~/components/primitives/Input";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Popover, PopoverArrowTrigger, PopoverContent } from "~/components/primitives/Popover";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
@@ -54,7 +56,7 @@ import { useProject } from "~/hooks/useProject";
|
||||
import { useReplaceLocation } from "~/hooks/useReplaceLocation";
|
||||
import { Shortcut, useShortcutKeys } from "~/hooks/useShortcutKeys";
|
||||
import { useUser } from "~/hooks/useUser";
|
||||
import { RunEvent, RunPresenter } from "~/presenters/v3/RunPresenter.server";
|
||||
import { RunPresenter } from "~/presenters/v3/RunPresenter.server";
|
||||
import { getResizableRunSettings, setResizableRunSettings } from "~/services/resizablePanel";
|
||||
import { requireUserId } from "~/services/session.server";
|
||||
import { cn } from "~/utils/cn";
|
||||
@@ -67,8 +69,10 @@ import {
|
||||
v3RunsPath,
|
||||
} from "~/utils/pathBuilder";
|
||||
import { SpanView } from "../resources.orgs.$organizationSlug.projects.v3.$projectParam.runs.$runParam.spans.$spanParam/route";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import { SimpleTooltip } from "~/components/primitives/Tooltip";
|
||||
import { env } from "~/env.server";
|
||||
|
||||
type TraceEvent = NonNullable<SerializeFrom<typeof loader>["trace"]>["events"][0];
|
||||
|
||||
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
const userId = await requireUserId(request);
|
||||
@@ -85,10 +89,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
//resizable settings
|
||||
const resizeSettings = await getResizableRunSettings(request);
|
||||
|
||||
return typedjson({
|
||||
...result,
|
||||
return {
|
||||
run: result.run,
|
||||
trace: result.trace,
|
||||
maximumLiveReloadingSetting: env.MAXIMUM_LIVE_RELOADING_EVENTS,
|
||||
resizeSettings,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
function getSpanId(location: Location<any>): string | undefined {
|
||||
@@ -97,7 +103,8 @@ function getSpanId(location: Location<any>): string | undefined {
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
const { run, trace, resizeSettings } = useTypedLoaderData<typeof loader>();
|
||||
const { run, trace, resizeSettings, maximumLiveReloadingSetting } =
|
||||
useLoaderData<typeof loader>();
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const user = useUser();
|
||||
@@ -167,6 +174,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const { events, parentRunFriendlyId, duration, rootSpanStatus, rootStartedAt } = trace;
|
||||
const shouldLiveReload = events.length <= maximumLiveReloadingSetting;
|
||||
|
||||
const changeToSpan = useDebounce((selectedSpan: string) => {
|
||||
replaceSearchParam("span", selectedSpan);
|
||||
@@ -175,6 +183,7 @@ export default function Page() {
|
||||
const revalidator = useRevalidator();
|
||||
const streamedEvents = useEventSource(v3RunStreamingPath(organization, project, run), {
|
||||
event: "message",
|
||||
disabled: !shouldLiveReload,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (streamedEvents !== null) {
|
||||
@@ -252,8 +261,10 @@ export default function Page() {
|
||||
}}
|
||||
totalDuration={duration}
|
||||
rootSpanStatus={rootSpanStatus}
|
||||
rootStartedAt={rootStartedAt}
|
||||
rootStartedAt={rootStartedAt ? new Date(rootStartedAt) : undefined}
|
||||
environmentType={run.environment.type}
|
||||
shouldLiveReload={shouldLiveReload}
|
||||
maximumLiveReloadingSetting={maximumLiveReloadingSetting}
|
||||
/>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
@@ -274,7 +285,7 @@ export default function Page() {
|
||||
}
|
||||
|
||||
type TasksTreeViewProps = {
|
||||
events: RunEvent[];
|
||||
events: TraceEvent[];
|
||||
selectedId?: string;
|
||||
parentRunFriendlyId?: string;
|
||||
onSelectedIdChanged: (selectedId: string | undefined) => void;
|
||||
@@ -282,6 +293,8 @@ type TasksTreeViewProps = {
|
||||
rootSpanStatus: "executing" | "completed" | "failed";
|
||||
rootStartedAt: Date | undefined;
|
||||
environmentType: RuntimeEnvironmentType;
|
||||
shouldLiveReload: boolean;
|
||||
maximumLiveReloadingSetting: number;
|
||||
};
|
||||
|
||||
function TasksTreeView({
|
||||
@@ -293,6 +306,8 @@ function TasksTreeView({
|
||||
rootSpanStatus,
|
||||
rootStartedAt,
|
||||
environmentType,
|
||||
shouldLiveReload,
|
||||
maximumLiveReloadingSetting,
|
||||
}: TasksTreeViewProps) {
|
||||
const [filterText, setFilterText] = useState("");
|
||||
const [errorsOnly, setErrorsOnly] = useState(false);
|
||||
@@ -367,7 +382,11 @@ function TasksTreeView({
|
||||
This is the root task
|
||||
</Paragraph>
|
||||
)}
|
||||
<LiveReloadingStatus rootSpanCompleted={rootSpanStatus !== "executing"} />
|
||||
<LiveReloadingStatus
|
||||
rootSpanCompleted={rootSpanStatus !== "executing"}
|
||||
isLiveReloading={shouldLiveReload}
|
||||
settingValue={maximumLiveReloadingSetting}
|
||||
/>
|
||||
</div>
|
||||
<TreeView
|
||||
parentRef={parentRef}
|
||||
@@ -750,7 +769,7 @@ function TimelineView({
|
||||
);
|
||||
}
|
||||
|
||||
function NodeText({ node }: { node: RunEvent }) {
|
||||
function NodeText({ node }: { node: TraceEvent }) {
|
||||
const className = "truncate";
|
||||
return (
|
||||
<Paragraph variant="small" className={cn(className)}>
|
||||
@@ -759,7 +778,7 @@ function NodeText({ node }: { node: RunEvent }) {
|
||||
);
|
||||
}
|
||||
|
||||
function NodeStatusIcon({ node }: { node: RunEvent }) {
|
||||
function NodeStatusIcon({ node }: { node: TraceEvent }) {
|
||||
if (node.data.level !== "TRACE") return null;
|
||||
if (node.data.style.variant !== "primary") return null;
|
||||
|
||||
@@ -834,16 +853,40 @@ function ShowParentLink({ runFriendlyId }: { runFriendlyId: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function LiveReloadingStatus({ rootSpanCompleted }: { rootSpanCompleted: boolean }) {
|
||||
function LiveReloadingStatus({
|
||||
rootSpanCompleted,
|
||||
isLiveReloading,
|
||||
settingValue,
|
||||
}: {
|
||||
rootSpanCompleted: boolean;
|
||||
isLiveReloading: boolean;
|
||||
settingValue: number;
|
||||
}) {
|
||||
if (rootSpanCompleted) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<PulsingDot />
|
||||
<Paragraph variant="extra-small" className="whitespace-nowrap text-blue-500">
|
||||
Live reloading
|
||||
</Paragraph>
|
||||
</div>
|
||||
<>
|
||||
{isLiveReloading ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<PulsingDot />
|
||||
<Paragraph variant="extra-small" className="whitespace-nowrap text-blue-500">
|
||||
Live reloading
|
||||
</Paragraph>
|
||||
</div>
|
||||
) : (
|
||||
<SimpleTooltip
|
||||
content={`Live reloading is disabled because you've exceeded ${settingValue} logs.`}
|
||||
button={
|
||||
<div className="flex items-center gap-1">
|
||||
<BoltSlashIcon className="size-3.5 text-text-dimmed" />
|
||||
<Paragraph variant="extra-small" className="whitespace-nowrap text-text-dimmed">
|
||||
Live reloading disabled
|
||||
</Paragraph>
|
||||
</div>
|
||||
}
|
||||
></SimpleTooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -862,7 +905,7 @@ function SpanWithDuration({
|
||||
showDuration,
|
||||
node,
|
||||
...props
|
||||
}: Timeline.SpanProps & { node: RunEvent; showDuration: boolean }) {
|
||||
}: Timeline.SpanProps & { node: TraceEvent; showDuration: boolean }) {
|
||||
return (
|
||||
<Timeline.Span {...props}>
|
||||
<motion.div
|
||||
|
||||
+37
-12
@@ -23,6 +23,7 @@ import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { Property, PropertyTable } from "~/components/primitives/PropertyTable";
|
||||
import {
|
||||
Table,
|
||||
TableBlankRow,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHeader,
|
||||
@@ -180,12 +181,21 @@ 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();
|
||||
const organization = useOrganization();
|
||||
const project = useProject();
|
||||
const user = useUser();
|
||||
|
||||
const isUtc = schedule.timezone === "UTC";
|
||||
|
||||
return (
|
||||
<div className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden bg-background-bright">
|
||||
@@ -210,6 +220,7 @@ export default function Page() {
|
||||
<Paragraph variant="small">{schedule.cronDescription}</Paragraph>
|
||||
</div>
|
||||
</Property>
|
||||
<Property label="Timezone">{schedule.timezone}</Property>
|
||||
<Property label="Environments">
|
||||
<EnvironmentLabels size="small" environments={schedule.environments} />
|
||||
</Property>
|
||||
@@ -245,21 +256,35 @@ export default function Page() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{!isUtc && <TableHeaderCell>{schedule.timezone}</TableHeaderCell>}
|
||||
<TableHeaderCell>UTC</TableHeaderCell>
|
||||
<TableHeaderCell>Local time</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{schedule.nextRuns.map((run, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<DateTime date={run} timeZone="UTC" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DateTime date={run} />
|
||||
</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>
|
||||
|
||||
+2
-1
@@ -21,7 +21,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { schedule, possibleTasks, possibleEnvironments, showGenerateField } =
|
||||
const { schedule, possibleTasks, possibleEnvironments, possibleTimezones, showGenerateField } =
|
||||
useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
@@ -29,6 +29,7 @@ export default function Page() {
|
||||
schedule={schedule}
|
||||
possibleTasks={possibleTasks}
|
||||
possibleEnvironments={possibleEnvironments}
|
||||
possibleTimezones={possibleTimezones}
|
||||
showGenerateField={showGenerateField}
|
||||
/>
|
||||
);
|
||||
|
||||
+2
-1
@@ -20,7 +20,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { schedule, possibleTasks, possibleEnvironments, showGenerateField } =
|
||||
const { schedule, possibleTasks, possibleEnvironments, possibleTimezones, showGenerateField } =
|
||||
useTypedLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
@@ -29,6 +29,7 @@ export default function Page() {
|
||||
possibleTasks={possibleTasks}
|
||||
possibleEnvironments={possibleEnvironments}
|
||||
showGenerateField={showGenerateField}
|
||||
possibleTimezones={possibleTimezones}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+82
-22
@@ -4,12 +4,21 @@ import { Outlet, useLocation, useParams } from "@remix-run/react";
|
||||
import { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { BlankstateInstructions } from "~/components/BlankstateInstructions";
|
||||
import { Feedback } from "~/components/Feedback";
|
||||
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
|
||||
import { InlineCode } from "~/components/code/InlineCode";
|
||||
import { EnvironmentLabel, EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
|
||||
import { EnvironmentLabels } from "~/components/environments/EnvironmentLabel";
|
||||
import { MainCenteredContainer, PageBody, PageContainer } from "~/components/layout/AppLayout";
|
||||
import { LinkButton } from "~/components/primitives/Buttons";
|
||||
import { Button, LinkButton } from "~/components/primitives/Buttons";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTrigger,
|
||||
} from "~/components/primitives/Dialog";
|
||||
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { PaginationControls } from "~/components/primitives/Pagination";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
@@ -78,6 +87,7 @@ export default function Page() {
|
||||
possibleEnvironments,
|
||||
hasFilters,
|
||||
filters,
|
||||
limits,
|
||||
currentPage,
|
||||
totalPages,
|
||||
} = useTypedLoaderData<typeof loader>();
|
||||
@@ -107,15 +117,43 @@ export default function Page() {
|
||||
</PropertyTable>
|
||||
</AdminDebugTooltip>
|
||||
|
||||
<LinkButton
|
||||
LeadingIcon={PlusIcon}
|
||||
to={`${v3NewSchedulePath(organization, project)}${location.search}`}
|
||||
variant="primary/small"
|
||||
shortcut={{ key: "n" }}
|
||||
disabled={possibleTasks.length === 0 || isShowingNewPane}
|
||||
>
|
||||
New schedule
|
||||
</LinkButton>
|
||||
{limits.used >= limits.limit ? (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
LeadingIcon={PlusIcon}
|
||||
variant="primary/small"
|
||||
shortcut={{ key: "n" }}
|
||||
disabled={possibleTasks.length === 0 || isShowingNewPane}
|
||||
>
|
||||
New schedule
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>You've exceeded your limit</DialogHeader>
|
||||
<DialogDescription>
|
||||
You've used {limits.used}/{limits.limit} of your schedules. You can request more
|
||||
schedules.
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Feedback
|
||||
button={<Button variant="primary/medium">Request more</Button>}
|
||||
defaultValue="help"
|
||||
/>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : (
|
||||
<LinkButton
|
||||
LeadingIcon={PlusIcon}
|
||||
to={`${v3NewSchedulePath(organization, project)}${location.search}`}
|
||||
variant="primary/small"
|
||||
shortcut={{ key: "n" }}
|
||||
disabled={possibleTasks.length === 0 || isShowingNewPane}
|
||||
>
|
||||
New schedule
|
||||
</LinkButton>
|
||||
)}
|
||||
</PageAccessories>
|
||||
</NavBar>
|
||||
<PageBody scrollable={false}>
|
||||
@@ -142,7 +180,21 @@ export default function Page() {
|
||||
</div>
|
||||
|
||||
<SchedulesTable schedules={schedules} hasFilters={hasFilters} />
|
||||
<div className="mt-2 justify-end">
|
||||
<div className="mt-2 justify-between">
|
||||
<Paragraph variant="extra-small" className="mt-3">
|
||||
<span className={limits.used >= limits.limit ? "text-warning" : ""}>
|
||||
You've used {limits.used}/{limits.limit} of your schedules.
|
||||
</span>{" "}
|
||||
<Feedback
|
||||
button={
|
||||
<button className=" text-secondary transition hover:text-indigo-400">
|
||||
Request more
|
||||
</button>
|
||||
}
|
||||
defaultValue="help"
|
||||
/>
|
||||
.
|
||||
</Paragraph>
|
||||
<PaginationControls currentPage={currentPage} totalPages={totalPages} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,12 +288,13 @@ function SchedulesTable({
|
||||
<TableRow>
|
||||
<TableHeaderCell>ID</TableHeaderCell>
|
||||
<TableHeaderCell>Task ID</TableHeaderCell>
|
||||
<TableHeaderCell>External ID</TableHeaderCell>
|
||||
<TableHeaderCell>CRON</TableHeaderCell>
|
||||
<TableHeaderCell hiddenLabel>CRON description</TableHeaderCell>
|
||||
<TableHeaderCell>External ID</TableHeaderCell>
|
||||
<TableHeaderCell>Timezone</TableHeaderCell>
|
||||
<TableHeaderCell>Next run</TableHeaderCell>
|
||||
<TableHeaderCell>Last run</TableHeaderCell>
|
||||
<TableHeaderCell>Deduplication key</TableHeaderCell>
|
||||
<TableHeaderCell>Next run (UTC)</TableHeaderCell>
|
||||
<TableHeaderCell>Last run (UTC)</TableHeaderCell>
|
||||
<TableHeaderCell>Environments</TableHeaderCell>
|
||||
<TableHeaderCell>Enabled</TableHeaderCell>
|
||||
</TableRow>
|
||||
@@ -262,6 +315,9 @@ function SchedulesTable({
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.taskIdentifier}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.externalId ? schedule.externalId : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.cron}
|
||||
</TableCell>
|
||||
@@ -269,17 +325,21 @@ function SchedulesTable({
|
||||
{schedule.cronDescription}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.externalId ? schedule.externalId : "–"}
|
||||
{schedule.timezone}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
<DateTime date={schedule.nextRun} timeZone={schedule.timezone} />
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.lastRun ? (
|
||||
<DateTime date={schedule.lastRun} timeZone={schedule.timezone} />
|
||||
) : (
|
||||
"–"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.userProvidedDeduplicationKey ? schedule.deduplicationKey : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
<DateTime date={schedule.nextRun} timeZone="utc" />
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
{schedule.lastRun ? <DateTime date={schedule.lastRun} timeZone="utc" /> : "–"}
|
||||
</TableCell>
|
||||
<TableCell to={path} className={cellClass}>
|
||||
<EnvironmentLabels environments={schedule.environments} size="small" />
|
||||
</TableCell>
|
||||
|
||||
+53
-3
@@ -26,8 +26,10 @@ import {
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "~/components/primitives/Resizable";
|
||||
import { Select } from "~/components/primitives/Select";
|
||||
import { TextLink } from "~/components/primitives/TextLink";
|
||||
import { TaskRunStatusCombo } from "~/components/runs/v3/TaskRunStatus";
|
||||
import { TimezoneList } from "~/components/scheduled/timezones";
|
||||
import { redirectBackWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import {
|
||||
ScheduledRun,
|
||||
@@ -95,7 +97,13 @@ export default function Page() {
|
||||
return <StandardTaskForm task={result.task} runs={result.runs} />;
|
||||
}
|
||||
case "SCHEDULED": {
|
||||
return <ScheduledTaskForm task={result.task} runs={result.runs} />;
|
||||
return (
|
||||
<ScheduledTaskForm
|
||||
task={result.task}
|
||||
runs={result.runs}
|
||||
possibleTimezones={result.possibleTimezones}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,12 +223,21 @@ function StandardTaskForm({ task, runs }: { task: TestTask["task"]; runs: Standa
|
||||
);
|
||||
}
|
||||
|
||||
function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: ScheduledRun[] }) {
|
||||
function ScheduledTaskForm({
|
||||
task,
|
||||
runs,
|
||||
possibleTimezones,
|
||||
}: {
|
||||
task: TestTask["task"];
|
||||
runs: ScheduledRun[];
|
||||
possibleTimezones: string[];
|
||||
}) {
|
||||
const lastSubmission = useActionData();
|
||||
const [selectedCodeSampleId, setSelectedCodeSampleId] = useState(runs.at(0)?.id);
|
||||
const [timestampValue, setTimestampValue] = useState<Date | undefined>();
|
||||
const [lastTimestampValue, setLastTimestampValue] = useState<Date | undefined>();
|
||||
const [externalIdValue, setExternalIdValue] = useState<string | undefined>();
|
||||
const [timezoneValue, setTimezoneValue] = useState<string>("UTC");
|
||||
|
||||
//set initial values
|
||||
useEffect(() => {
|
||||
@@ -233,11 +250,20 @@ function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: Sched
|
||||
setTimestampValue(initialRun.payload.timestamp);
|
||||
setLastTimestampValue(initialRun.payload.lastTimestamp);
|
||||
setExternalIdValue(initialRun.payload.externalId);
|
||||
setTimezoneValue(initialRun.payload.timezone);
|
||||
}, [selectedCodeSampleId]);
|
||||
|
||||
const [
|
||||
form,
|
||||
{ timestamp, lastTimestamp, externalId, triggerSource, taskIdentifier, environmentId },
|
||||
{
|
||||
timestamp,
|
||||
lastTimestamp,
|
||||
externalId,
|
||||
triggerSource,
|
||||
taskIdentifier,
|
||||
environmentId,
|
||||
timezone,
|
||||
},
|
||||
] = useForm({
|
||||
id: "test-task-scheduled",
|
||||
// TODO: type this
|
||||
@@ -314,6 +340,30 @@ function ScheduledTaskForm({ task, runs }: { task: TestTask["task"]; runs: Sched
|
||||
</Hint>
|
||||
<FormError id={lastTimestamp.errorId}>{lastTimestamp.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label htmlFor={timezone.id}>Timezone</Label>
|
||||
<Select
|
||||
{...conform.select(timezone)}
|
||||
placeholder="Select a timezone"
|
||||
defaultValue={timezoneValue}
|
||||
value={timezoneValue}
|
||||
setValue={(e) => {
|
||||
if (Array.isArray(e)) return;
|
||||
setTimezoneValue(e);
|
||||
}}
|
||||
items={possibleTimezones}
|
||||
filter={{ keys: [(item) => item.replace(/\//g, " ").replace(/_/g, " ")] }}
|
||||
dropdownIcon
|
||||
variant="tertiary/medium"
|
||||
>
|
||||
{(matches) => <TimezoneList timezones={matches} />}
|
||||
</Select>
|
||||
<Hint>
|
||||
The Timestamp and Last timestamp are in UTC so this just changes the timezone
|
||||
string that comes through in the payload.
|
||||
</Hint>
|
||||
<FormError id={timezone.errorId}>{timezone.error}</FormError>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label required={false} htmlFor={externalId.id}>
|
||||
External ID
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Link } from "@remix-run/react";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
import { LogoIcon } from "~/components/LogoIcon";
|
||||
import { Header1 } from "~/components/primitives/Headers";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import { getTimezones } from "~/utils/timezones.server";
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
return typedjson({
|
||||
timezones: getTimezones(),
|
||||
});
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
const { timezones } = useTypedLoaderData<typeof loader>();
|
||||
return (
|
||||
<div className="grid grid-rows-[2.5rem,1fr]">
|
||||
<div className="flex items-center border-b border-b-grid-dimmed px-3">
|
||||
<Link to="/">
|
||||
<LogoIcon className="relative -top-px mr-2 h-4 w-4 min-w-[1rem]" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="overflow-y-auto p-8 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600">
|
||||
<Header1 spacing>Supported timezones</Header1>
|
||||
<Paragraph spacing>We support these timezones when creating a schedule.</Paragraph>
|
||||
<ul className="">
|
||||
{timezones.map((timezone) => (
|
||||
<li key={timezone}>{timezone}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { InitializeDeploymentRequestBody, InitializeDeploymentResponseBody } from "@trigger.dev/core/v3";
|
||||
import {
|
||||
InitializeDeploymentRequestBody,
|
||||
InitializeDeploymentResponseBody,
|
||||
} from "@trigger.dev/core/v3";
|
||||
import { env } from "~/env.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
@@ -37,13 +40,11 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
contentHash: deployment.contentHash,
|
||||
shortCode: deployment.shortCode,
|
||||
version: deployment.version,
|
||||
externalBuildData: deployment.externalBuildData as InitializeDeploymentResponseBody["externalBuildData"],
|
||||
externalBuildData:
|
||||
deployment.externalBuildData as InitializeDeploymentResponseBody["externalBuildData"],
|
||||
imageTag,
|
||||
registryHost: env.DEPLOY_REGISTRY_HOST
|
||||
}
|
||||
registryHost: env.DEPLOY_REGISTRY_HOST,
|
||||
};
|
||||
|
||||
return json(
|
||||
responseBody,
|
||||
{ status: 200 }
|
||||
);
|
||||
return json(responseBody, { status: 200 });
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
|
||||
const repository = new EnvironmentVariablesRepository();
|
||||
|
||||
const variables = await repository.getEnvironment(environment.project.id, environment.id, true);
|
||||
const variables = await repository.getEnvironment(environment.project.id, environment.id);
|
||||
|
||||
const environmentVariable = variables.find((v) => v.key === parsedParams.data.name);
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||
|
||||
const repository = new EnvironmentVariablesRepository();
|
||||
|
||||
const variables = await repository.getEnvironment(environment.project.id, environment.id, true);
|
||||
const variables = await repository.getEnvironment(environment.project.id, environment.id);
|
||||
|
||||
return json(variables.map((variable) => ({ name: variable.key, value: variable.value })));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
|
||||
import { resolveVariablesForEnvironment } from "~/v3/environmentVariables/environmentVariablesRepository.server";
|
||||
|
||||
const ParamsSchema = z.object({
|
||||
projectRef: z.string(),
|
||||
@@ -41,9 +41,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
return json({ error: "Project not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const repository = new EnvironmentVariablesRepository();
|
||||
|
||||
const variables = await repository.getEnvironmentVariables(project.id, authenticatedEnv.id);
|
||||
const variables = await resolveVariablesForEnvironment(authenticatedEnv);
|
||||
|
||||
return json({
|
||||
variables: variables.reduce((acc: Record<string, string>, variable) => {
|
||||
|
||||
+65
-59
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,9 +79,9 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
friendlyId: parsedParams.data.scheduleId,
|
||||
taskIdentifier: body.data.task,
|
||||
cron: body.data.cron,
|
||||
timezone: body.data.timezone,
|
||||
environments: [authenticationResult.environment.id],
|
||||
externalId: body.data.externalId,
|
||||
deduplicationKey: body.data.deduplicationKey,
|
||||
};
|
||||
|
||||
const schedule = await service.call(authenticationResult.environment.projectId, options);
|
||||
@@ -95,6 +95,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
expression: schedule.cron,
|
||||
description: schedule.cronDescription,
|
||||
},
|
||||
timezone: schedule.timezone,
|
||||
externalId: schedule.externalId ?? undefined,
|
||||
deduplicationKey: schedule.deduplicationKey,
|
||||
environments: schedule.environments,
|
||||
|
||||
@@ -43,6 +43,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
environments: [authenticationResult.environment.id],
|
||||
externalId: body.data.externalId,
|
||||
deduplicationKey: body.data.deduplicationKey,
|
||||
timezone: body.data.timezone,
|
||||
};
|
||||
|
||||
const schedule = await service.call(authenticationResult.environment.projectId, options);
|
||||
@@ -56,6 +57,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
expression: schedule.cron,
|
||||
description: schedule.cronDescription,
|
||||
},
|
||||
timezone: schedule.timezone,
|
||||
externalId: schedule.externalId ?? undefined,
|
||||
deduplicationKey: schedule.deduplicationKey,
|
||||
environments: schedule.environments,
|
||||
@@ -111,6 +113,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
expression: schedule.cron,
|
||||
description: schedule.cronDescription,
|
||||
},
|
||||
timezone: schedule.timezone,
|
||||
deduplicationKey: schedule.userProvidedDeduplicationKey
|
||||
? schedule.deduplicationKey
|
||||
: undefined,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { z } from "zod";
|
||||
import { apiCors } from "~/utils/apiCors";
|
||||
import { getTimezones } from "~/utils/timezones.server";
|
||||
|
||||
const SearchParamsSchema = z.object({
|
||||
excludeUtc: z.preprocess((value) => value === "true", z.boolean()).default(false),
|
||||
});
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
if (request.method.toUpperCase() === "OPTIONS") {
|
||||
return apiCors(request, json({}));
|
||||
}
|
||||
|
||||
const rawSearchParams = new URL(request.url).searchParams;
|
||||
const params = SearchParamsSchema.safeParse(Object.fromEntries(rawSearchParams.entries()));
|
||||
|
||||
if (!params.success) {
|
||||
return apiCors(
|
||||
request,
|
||||
json({ error: "Invalid request parameters", issues: params.error.issues }, { status: 400 })
|
||||
);
|
||||
}
|
||||
|
||||
const timezones = getTimezones(!params.data.excludeUtc);
|
||||
return apiCors(request, json({ timezones }));
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||
import { MachinePresetName } from "@trigger.dev/core/v3";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { validateJWTTokenAndRenew } from "~/services/apiAuth.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
import { machinePresetFromName } from "~/v3/machinePresets.server";
|
||||
import { reportUsageEvent } from "~/v3/openMeter.server";
|
||||
|
||||
const JWTPayloadSchema = z.object({
|
||||
environment_id: z.string(),
|
||||
org_id: z.string(),
|
||||
project_id: z.string(),
|
||||
run_id: z.string(),
|
||||
machine_preset: z.string(),
|
||||
});
|
||||
|
||||
const BodySchema = z.object({
|
||||
durationMs: z.number(),
|
||||
});
|
||||
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
// Ensure this is a POST request
|
||||
if (request.method.toUpperCase() !== "POST") {
|
||||
return { status: 405, body: "Method Not Allowed" };
|
||||
}
|
||||
|
||||
const jwtResult = await validateJWTTokenAndRenew(request, JWTPayloadSchema);
|
||||
|
||||
if (!jwtResult) {
|
||||
return { status: 401, body: "Unauthorized" };
|
||||
}
|
||||
|
||||
const rawJson = await request.json();
|
||||
|
||||
const json = BodySchema.safeParse(rawJson);
|
||||
|
||||
if (!json.success) {
|
||||
logger.error("Failed to parse request body", { rawJson });
|
||||
|
||||
return { status: 400, body: "Bad Request" };
|
||||
}
|
||||
|
||||
const preset = machinePresetFromName(jwtResult.payload.machine_preset as MachinePresetName);
|
||||
|
||||
logger.debug("[/api/v1/usage/ingest] Reporting usage", { jwtResult, json: json.data, preset });
|
||||
|
||||
if (json.data.durationMs > 0) {
|
||||
const costInCents = json.data.durationMs * preset.centsPerMs;
|
||||
|
||||
await prisma.taskRun.update({
|
||||
where: {
|
||||
id: jwtResult.payload.run_id,
|
||||
},
|
||||
data: {
|
||||
usageDurationMs: {
|
||||
increment: json.data.durationMs,
|
||||
},
|
||||
costInCents: {
|
||||
increment: json.data.durationMs * preset.centsPerMs,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await reportUsageEvent({
|
||||
source: "webapp",
|
||||
type: "usage",
|
||||
subject: jwtResult.payload.org_id,
|
||||
data: {
|
||||
durationMs: json.data.durationMs,
|
||||
costInCents: String(costInCents),
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error("Failed to report usage event, enqueing v3.reportUsage", { error: e });
|
||||
|
||||
await workerQueue.enqueue("v3.reportUsage", {
|
||||
orgId: jwtResult.payload.org_id,
|
||||
data: {
|
||||
costInCents: String(costInCents),
|
||||
},
|
||||
additionalData: {
|
||||
durationMs: json.data.durationMs,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(null, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"x-trigger-jwt": jwtResult.jwt,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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}`
|
||||
);
|
||||
}
|
||||
+53
-15
@@ -3,9 +3,10 @@ import { parse } from "@conform-to/zod";
|
||||
import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
||||
import { Form, useActionData, useLocation, useNavigation } from "@remix-run/react";
|
||||
import { ActionFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { parseExpression } from "cron-parser";
|
||||
import cronstrue from "cronstrue";
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import {
|
||||
environmentTextClassName,
|
||||
environmentTitle,
|
||||
@@ -42,6 +43,7 @@ import { ProjectParamSchema, docsPath, v3SchedulesPath } from "~/utils/pathBuild
|
||||
import { CronPattern, UpsertSchedule } from "~/v3/schedules";
|
||||
import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server";
|
||||
import { AIGeneratedCronField } from "../resources.orgs.$organizationSlug.projects.$projectParam.schedules.new.natural-language";
|
||||
import { TimezoneList } from "~/components/scheduled/timezones";
|
||||
|
||||
const cronFormat = `* * * * *
|
||||
┬ ┬ ┬ ┬ ┬
|
||||
@@ -117,9 +119,12 @@ export function UpsertScheduleForm({
|
||||
schedule,
|
||||
possibleTasks,
|
||||
possibleEnvironments,
|
||||
possibleTimezones,
|
||||
showGenerateField,
|
||||
}: EditableScheduleElements & { showGenerateField: boolean }) {
|
||||
const lastSubmission = useActionData();
|
||||
const [selectedTimezone, setSelectedTimezone] = useState<string>(schedule?.timezone ?? "UTC");
|
||||
const isUtc = selectedTimezone === "UTC";
|
||||
const [cronPattern, setCronPattern] = useState<string>(schedule?.cron ?? "");
|
||||
const navigation = useNavigation();
|
||||
const isLoading = navigation.state !== "idle";
|
||||
@@ -127,18 +132,20 @@ export function UpsertScheduleForm({
|
||||
const project = useProject();
|
||||
const location = useLocation();
|
||||
|
||||
const [form, { taskIdentifier, cron, externalId, environments, deduplicationKey }] = useForm({
|
||||
id: "create-schedule",
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
shouldRevalidate: "onSubmit",
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: UpsertSchedule });
|
||||
},
|
||||
});
|
||||
const [form, { taskIdentifier, cron, timezone, externalId, environments, deduplicationKey }] =
|
||||
useForm({
|
||||
id: "create-schedule",
|
||||
// TODO: type this
|
||||
lastSubmission: lastSubmission as any,
|
||||
shouldRevalidate: "onSubmit",
|
||||
onValidate({ formData }) {
|
||||
return parse(formData, { schema: UpsertSchedule });
|
||||
},
|
||||
});
|
||||
|
||||
let cronPatternResult: CronPatternResult | undefined = undefined;
|
||||
let nextRuns: Date[] | undefined = undefined;
|
||||
|
||||
if (cronPattern !== "") {
|
||||
const result = CronPattern.safeParse(cronPattern);
|
||||
|
||||
@@ -149,7 +156,10 @@ export function UpsertScheduleForm({
|
||||
};
|
||||
} else {
|
||||
try {
|
||||
const expression = parseExpression(cronPattern, { utc: true });
|
||||
const expression = parseExpression(
|
||||
cronPattern,
|
||||
isUtc ? { utc: true } : { tz: selectedTimezone }
|
||||
);
|
||||
cronPatternResult = {
|
||||
isValid: true,
|
||||
description: cronstrue.toString(cronPattern),
|
||||
@@ -195,6 +205,7 @@ export function UpsertScheduleForm({
|
||||
items={possibleTasks}
|
||||
filter={(task, search) => task.toLowerCase().includes(search.toLowerCase())}
|
||||
dropdownIcon
|
||||
variant="tertiary/medium"
|
||||
>
|
||||
{(matches) => (
|
||||
<>
|
||||
@@ -241,25 +252,52 @@ export function UpsertScheduleForm({
|
||||
<ValidCronMessage isValid={false} message={cronPatternResult.error} />
|
||||
)}
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<Label htmlFor={timezone.id}>Timezone</Label>
|
||||
<Select
|
||||
{...conform.select(timezone)}
|
||||
placeholder="Select a timezone"
|
||||
defaultValue={selectedTimezone}
|
||||
value={selectedTimezone}
|
||||
setValue={(e) => {
|
||||
if (Array.isArray(e)) return;
|
||||
setSelectedTimezone(e);
|
||||
}}
|
||||
items={possibleTimezones}
|
||||
filter={{ keys: [(item) => item.replace(/\//g, " ").replace(/_/g, " ")] }}
|
||||
dropdownIcon
|
||||
variant="tertiary/medium"
|
||||
>
|
||||
{(matches) => <TimezoneList timezones={matches} />}
|
||||
</Select>
|
||||
<Hint>
|
||||
{isUtc
|
||||
? "UTC will not change with daylight savings time."
|
||||
: "This will automatically adjust for daylight savings time."}
|
||||
</Hint>
|
||||
<FormError id={timezone.errorId}>{timezone.error}</FormError>
|
||||
</InputGroup>
|
||||
{nextRuns !== undefined && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Header3>Next 5 runs</Header3>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{!isUtc && <TableHeaderCell>{selectedTimezone}</TableHeaderCell>}
|
||||
<TableHeaderCell>UTC</TableHeaderCell>
|
||||
<TableHeaderCell>Local time</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{nextRuns.map((run, index) => (
|
||||
<TableRow key={index}>
|
||||
{!isUtc && (
|
||||
<TableCell>
|
||||
<DateTime date={run} timeZone={selectedTimezone} />
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>
|
||||
<DateTime date={run} timeZone="UTC" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DateTime date={run} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
+25
-1
@@ -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");
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Prettify } from "@trigger.dev/core";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
RuntimeEnvironment,
|
||||
findEnvironmentByApiKey,
|
||||
findEnvironmentByPublicApiKey,
|
||||
} from "~/models/runtimeEnvironment.server";
|
||||
@@ -12,6 +13,9 @@ import {
|
||||
import { prisma } from "~/db.server";
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { findProjectByRef } from "~/models/project.server";
|
||||
import { SignJWT, jwtVerify, errors } from "jose";
|
||||
import { env } from "~/env.server";
|
||||
import { logger } from "./logger.server";
|
||||
|
||||
type Optional<T, K extends keyof T> = Prettify<Omit<T, K> & Partial<Pick<T, K>>>;
|
||||
|
||||
@@ -209,3 +213,127 @@ export async function authenticatedEnvironmentForAuthentication(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const JWT_SECRET = new TextEncoder().encode(env.SESSION_SECRET);
|
||||
const JWT_ALGORITHM = "HS256";
|
||||
const DEFAULT_JWT_EXPIRATION_IN_MS = 1000 * 60 * 60; // 1 hour
|
||||
|
||||
export async function generateJWTTokenForEnvironment(
|
||||
environment: RuntimeEnvironment,
|
||||
payload: Record<string, string>
|
||||
) {
|
||||
const jwt = await new SignJWT({
|
||||
environment_id: environment.id,
|
||||
org_id: environment.organizationId,
|
||||
project_id: environment.projectId,
|
||||
...payload,
|
||||
})
|
||||
.setProtectedHeader({ alg: JWT_ALGORITHM })
|
||||
.setIssuedAt()
|
||||
.setIssuer("https://id.trigger.dev")
|
||||
.setAudience("https://api.trigger.dev")
|
||||
.setExpirationTime(calculateJWTExpiration())
|
||||
.sign(JWT_SECRET);
|
||||
|
||||
return jwt;
|
||||
}
|
||||
|
||||
export async function validateJWTTokenAndRenew<T extends z.ZodTypeAny>(
|
||||
request: Request,
|
||||
payloadSchema: T
|
||||
): Promise<{ payload: z.infer<T>; jwt: string } | undefined> {
|
||||
try {
|
||||
const jwt = request.headers.get("x-trigger-jwt");
|
||||
|
||||
if (!jwt) {
|
||||
logger.debug("Missing JWT token in request", {
|
||||
headers: Object.fromEntries(request.headers),
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { payload: rawPayload } = await jwtVerify(jwt, JWT_SECRET, {
|
||||
issuer: "https://id.trigger.dev",
|
||||
audience: "https://api.trigger.dev",
|
||||
});
|
||||
|
||||
const payload = payloadSchema.safeParse(rawPayload);
|
||||
|
||||
if (!payload.success) {
|
||||
logger.error("Failed to validate JWT", { payload: rawPayload, issues: payload.error.issues });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const renewedJwt = await renewJWTToken(payload.data);
|
||||
|
||||
return {
|
||||
payload: payload.data,
|
||||
jwt: renewedJwt,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof errors.JWTExpired) {
|
||||
// Now we need to try and renew the token using the API key auth
|
||||
const authenticatedEnv = await authenticateApiRequest(request);
|
||||
|
||||
if (!authenticatedEnv) {
|
||||
logger.error("Failed to renew JWT token, missing or invalid Authorization header", {
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = payloadSchema.safeParse(error.payload);
|
||||
|
||||
if (!payload.success) {
|
||||
logger.error("Failed to parse jwt payload after expired", {
|
||||
payload: error.payload,
|
||||
issues: payload.error.issues,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const renewedJwt = await generateJWTTokenForEnvironment(authenticatedEnv.environment, {
|
||||
...payload.data,
|
||||
});
|
||||
|
||||
logger.debug("Renewed JWT token from Authorization header API Key", {
|
||||
environment: authenticatedEnv.environment,
|
||||
payload: payload.data,
|
||||
});
|
||||
|
||||
return {
|
||||
payload: payload.data,
|
||||
jwt: renewedJwt,
|
||||
};
|
||||
}
|
||||
|
||||
logger.error("Failed to validate JWT token", { error });
|
||||
}
|
||||
}
|
||||
|
||||
async function renewJWTToken(payload: Record<string, string>) {
|
||||
const jwt = await new SignJWT(payload)
|
||||
.setProtectedHeader({ alg: JWT_ALGORITHM })
|
||||
.setIssuedAt()
|
||||
.setIssuer("https://id.trigger.dev")
|
||||
.setAudience("https://api.trigger.dev")
|
||||
.setExpirationTime(calculateJWTExpiration())
|
||||
.sign(JWT_SECRET);
|
||||
|
||||
return jwt;
|
||||
}
|
||||
|
||||
function calculateJWTExpiration() {
|
||||
if (env.PROD_USAGE_HEARTBEAT_INTERVAL_MS) {
|
||||
return (
|
||||
(Date.now() + Math.max(DEFAULT_JWT_EXPIRATION_IN_MS, env.PROD_USAGE_HEARTBEAT_INTERVAL_MS)) /
|
||||
1000
|
||||
);
|
||||
}
|
||||
|
||||
return (Date.now() + DEFAULT_JWT_EXPIRATION_IN_MS) / 1000;
|
||||
}
|
||||
|
||||
@@ -145,11 +145,13 @@ export const apiRateLimiter = authorizationRateLimitMiddleware({
|
||||
"/api/internal/stripe_webhooks",
|
||||
"/api/v1/authorization-code",
|
||||
"/api/v1/token",
|
||||
"/api/v1/usage/ingest",
|
||||
/^\/api\/v1\/tasks\/[^\/]+\/callback\/[^\/]+$/, // /api/v1/tasks/$id/callback/$secret
|
||||
/^\/api\/v1\/runs\/[^\/]+\/tasks\/[^\/]+\/callback\/[^\/]+$/, // /api/v1/runs/$runId/tasks/$id/callback/$secret
|
||||
/^\/api\/v1\/http-endpoints\/[^\/]+\/env\/[^\/]+\/[^\/]+$/, // /api/v1/http-endpoints/$httpEndpointId/env/$envType/$shortcode
|
||||
/^\/api\/v1\/sources\/http\/[^\/]+$/, // /api/v1/sources/http/$id
|
||||
/^\/api\/v1\/endpoints\/[^\/]+\/[^\/]+\/index\/[^\/]+$/, // /api/v1/endpoints/$environmentId/$endpointSlug/index/$indexHookIdentifier
|
||||
"/api/v1/timezones",
|
||||
],
|
||||
log: {
|
||||
rejections: env.API_RATE_LIMIT_REJECTION_LOGS_ENABLED === "1",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -46,6 +46,9 @@ import { ProcessCallbackTimeoutService } from "./tasks/processCallbackTimeout.se
|
||||
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({
|
||||
@@ -169,6 +172,19 @@ const workerCatalog = {
|
||||
"v2.requeueMessage": z.object({
|
||||
runId: z.string(),
|
||||
}),
|
||||
"v3.reportUsage": z.object({
|
||||
orgId: z.string(),
|
||||
data: z.object({
|
||||
costInCents: z.string(),
|
||||
}),
|
||||
additionalData: z.record(z.any()).optional(),
|
||||
}),
|
||||
"v3.enqueueDelayedRun": z.object({
|
||||
runId: z.string(),
|
||||
}),
|
||||
"v3.expireRun": z.object({
|
||||
runId: z.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
const executionWorkerCatalog = {
|
||||
@@ -649,6 +665,39 @@ function getWorkerQueue() {
|
||||
await service.call(payload.runId);
|
||||
},
|
||||
},
|
||||
"v3.reportUsage": {
|
||||
priority: 0,
|
||||
maxAttempts: 8,
|
||||
handler: async (payload, job) => {
|
||||
await reportUsageEvent({
|
||||
source: "webapp",
|
||||
type: "usage",
|
||||
subject: payload.orgId,
|
||||
data: {
|
||||
costInCents: payload.data.costInCents,
|
||||
...payload.additionalData,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
"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,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user