v3: Refactor attempt creation to be worker requested (#1077)
* WIP worker TaskRunAttempt creation
* Handling failing task runs that cannot create an attempt for whatever reason
* Move the visibility queue stuff into a graphile job
* Fixed task runs with unsanitized queue names
* “Borrow” the code from alerts PR to get self hosted deployments working
* Add an admin API endpoint to get info about the shared marqs queue
* Allow admins to view any project metrics
* start adding lazy attempts to prod
* lazy attempt creation for prod workers
* resurrect prod stack traces
* add exception event to failed run spans
* simplify dependency resumes
* fix typecheck
* fix merge
* fresh process for all attempts
* always try sigterm first
* stop heartbeat timeout on non-inplace replace message
* add missing ack on checkpoint creation service failure
* bypass dequeue for retries with running worker
* respect retry delays
* crash runs with invalid run status for execution
* remove debug logs
* fix nack message
* fix version locking
* fresh attempt processes in dev and prod
* improve handling of ipc timeouts
* consider checkpoint failures on cancellation
* add basic chaos monkey to checkpointer
* changeset
* control forced checkpoint simulation via env var
* fix merge
* kill old attempt processes before checkpointing
* detailed perf logging for checkpointing
* add coordinator otlp endpoint example
* improve prod run cancellation
* rename supports lazy attempts migration
* fix graceful exit
* fix retry mechanics
* clear paused state before retry
* remove checkpoint image after push
* crash worker on unrecoverable errors
* refactor unrecoverable error emit
* switch to do hosted busybox image
* increase wait for duration ipc timeout
* add changeset for misc fixes
* fix merge
* fix retry delay span runId
* fix dev retries
* improve prod worker logging
* log checkpoint sizes
* add lazy attempts catalog entries
* Fixed merge issue: use zodFetch, not wrapZodFetch
* Revert "Fixed merge issue: use zodFetch, not wrapZodFetch"
This reverts commit d137e4e1fe.
* importEnvVars uses wrapZodFetch now
* add backwards compat for retries without checkpoints
* handle more cases of unrecoverable runs
* don't kill the child process if it shouldn't be killed
---------
Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com>
Co-authored-by: Matt Aitken <matt@mattaitken.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
"trigger.dev": patch
|
||||||
|
"@trigger.dev/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
- Clear paused states before retry
|
||||||
|
- Detect and handle unrecoverable worker errors
|
||||||
|
- Remove checkpoints after successful push
|
||||||
|
- Permanently switch to DO hosted busybox image
|
||||||
|
- Fix IPC timeout issue, or at least handle it more gracefully
|
||||||
|
- Handle checkpoint failures
|
||||||
|
- Basic chaos monkey for checkpoint testing
|
||||||
|
- Stack traces are back in the dashboard
|
||||||
|
- Display final errors on root span
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@trigger.dev/core": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Improve handling of IPC timeouts and fix checkpoint cancellation after failures
|
||||||
+296
-50
@@ -1,5 +1,6 @@
|
|||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
import { $ } from "execa";
|
import fs from "node:fs/promises";
|
||||||
|
import { $, type ExecaChildProcess } from "execa";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { Server } from "socket.io";
|
import { Server } from "socket.io";
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +20,11 @@ collectDefaultMetrics();
|
|||||||
const HTTP_SERVER_PORT = Number(process.env.HTTP_SERVER_PORT || 8020);
|
const HTTP_SERVER_PORT = Number(process.env.HTTP_SERVER_PORT || 8020);
|
||||||
const NODE_NAME = process.env.NODE_NAME || "coordinator";
|
const NODE_NAME = process.env.NODE_NAME || "coordinator";
|
||||||
const DEFAULT_RETRY_DELAY_THRESHOLD_IN_MS = 30_000;
|
const DEFAULT_RETRY_DELAY_THRESHOLD_IN_MS = 30_000;
|
||||||
|
const CHAOS_MONKEY_ENABLED = !!process.env.CHAOS_MONKEY_ENABLED;
|
||||||
|
|
||||||
|
const FORCE_CHECKPOINT_SIMULATION = ["1", "true"].includes(
|
||||||
|
process.env.FORCE_CHECKPOINT_SIMULATION ?? "true"
|
||||||
|
);
|
||||||
|
|
||||||
const REGISTRY_HOST = process.env.REGISTRY_HOST || "localhost:5000";
|
const REGISTRY_HOST = process.env.REGISTRY_HOST || "localhost:5000";
|
||||||
const CHECKPOINT_PATH = process.env.CHECKPOINT_PATH || "/checkpoints";
|
const CHECKPOINT_PATH = process.env.CHECKPOINT_PATH || "/checkpoints";
|
||||||
@@ -32,6 +38,10 @@ const SECURE_CONNECTION = ["1", "true"].includes(process.env.SECURE_CONNECTION ?
|
|||||||
|
|
||||||
const logger = new SimpleLogger(`[${NODE_NAME}]`);
|
const logger = new SimpleLogger(`[${NODE_NAME}]`);
|
||||||
|
|
||||||
|
if (CHAOS_MONKEY_ENABLED) {
|
||||||
|
logger.log("🍌 Chaos monkey enabled");
|
||||||
|
}
|
||||||
|
|
||||||
type CheckpointerInitializeReturn = {
|
type CheckpointerInitializeReturn = {
|
||||||
canCheckpoint: boolean;
|
canCheckpoint: boolean;
|
||||||
willSimulate: boolean;
|
willSimulate: boolean;
|
||||||
@@ -49,6 +59,40 @@ type CheckpointData = {
|
|||||||
docker: boolean;
|
docker: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function isExecaChildProcess(maybeExeca: unknown): maybeExeca is Awaited<ExecaChildProcess> {
|
||||||
|
return typeof maybeExeca === "object" && maybeExeca !== null && "escapedCommand" in maybeExeca;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFileSize(filePath: string): Promise<number> {
|
||||||
|
try {
|
||||||
|
const stats = await fs.stat(filePath);
|
||||||
|
return stats.size;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting file size:", error);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getParsedFileSize(filePath: string) {
|
||||||
|
const sizeInBytes = await getFileSize(filePath);
|
||||||
|
|
||||||
|
let message = `Size in bytes: ${sizeInBytes}`;
|
||||||
|
|
||||||
|
if (sizeInBytes > 1024 * 1024) {
|
||||||
|
const sizeInMB = (sizeInBytes / 1024 / 1024).toFixed(2);
|
||||||
|
message = `Size in MB (rounded): ${sizeInMB}`;
|
||||||
|
} else if (sizeInBytes > 1024) {
|
||||||
|
const sizeInKB = (sizeInBytes / 1024).toFixed(2);
|
||||||
|
message = `Size in KB (rounded): ${sizeInKB}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: filePath,
|
||||||
|
sizeInBytes,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class Checkpointer {
|
class Checkpointer {
|
||||||
#initialized = false;
|
#initialized = false;
|
||||||
#canCheckpoint = false;
|
#canCheckpoint = false;
|
||||||
@@ -56,6 +100,7 @@ class Checkpointer {
|
|||||||
|
|
||||||
#logger = new SimpleLogger("[checkptr]");
|
#logger = new SimpleLogger("[checkptr]");
|
||||||
#abortControllers = new Map<string, AbortController>();
|
#abortControllers = new Map<string, AbortController>();
|
||||||
|
#failedCheckpoints = new Map<string, unknown>();
|
||||||
|
|
||||||
constructor(private opts = { forceSimulate: false }) {}
|
constructor(private opts = { forceSimulate: false }) {}
|
||||||
|
|
||||||
@@ -150,7 +195,11 @@ class Checkpointer {
|
|||||||
success: !!result,
|
success: !!result,
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
if (!result.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.checkpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
isCheckpointing(runId: string) {
|
isCheckpointing(runId: string) {
|
||||||
@@ -158,6 +207,13 @@ class Checkpointer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cancelCheckpoint(runId: string): boolean {
|
cancelCheckpoint(runId: string): boolean {
|
||||||
|
// If the last checkpoint failed, pretend we canceled it
|
||||||
|
// This ensures tasks don't wait for external resume messages to continue
|
||||||
|
if (this.#hasFailedCheckpoint(runId)) {
|
||||||
|
this.#clearFailedCheckpoint(runId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const controller = this.#abortControllers.get(runId);
|
const controller = this.#abortControllers.get(runId);
|
||||||
|
|
||||||
if (!controller) {
|
if (!controller) {
|
||||||
@@ -176,44 +232,58 @@ class Checkpointer {
|
|||||||
leaveRunning = true, // This mirrors kubernetes behaviour more accurately
|
leaveRunning = true, // This mirrors kubernetes behaviour more accurately
|
||||||
projectRef,
|
projectRef,
|
||||||
deploymentVersion,
|
deploymentVersion,
|
||||||
}: CheckpointAndPushOptions): Promise<CheckpointData | undefined> {
|
}: CheckpointAndPushOptions): Promise<
|
||||||
|
{ success: true; checkpoint: CheckpointData } | { success: false; reason?: "CANCELED" }
|
||||||
|
> {
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
runId,
|
||||||
|
leaveRunning,
|
||||||
|
projectRef,
|
||||||
|
deploymentVersion,
|
||||||
|
};
|
||||||
|
|
||||||
if (!this.#dockerMode && !this.#canCheckpoint) {
|
if (!this.#dockerMode && !this.#canCheckpoint) {
|
||||||
this.#logger.error("No checkpoint support. Simulation requires docker.");
|
this.#logger.error("No checkpoint support. Simulation requires docker.");
|
||||||
return;
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.#abortControllers.has(runId)) {
|
if (this.#abortControllers.has(runId)) {
|
||||||
logger.error("Checkpoint procedure already in progress", {
|
logger.error("Checkpoint procedure already in progress", { options });
|
||||||
options: {
|
return { success: false };
|
||||||
runId,
|
|
||||||
leaveRunning,
|
|
||||||
projectRef,
|
|
||||||
deploymentVersion,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is a new checkpoint, clear any last failure for this run
|
||||||
|
this.#clearFailedCheckpoint(runId);
|
||||||
|
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
this.#abortControllers.set(runId, controller);
|
this.#abortControllers.set(runId, controller);
|
||||||
|
|
||||||
const $$ = $({ signal: controller.signal });
|
const $$ = $({ signal: controller.signal });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (CHAOS_MONKEY_ENABLED) {
|
||||||
|
console.log("🍌 Chaos monkey wreaking havoc");
|
||||||
|
|
||||||
|
const random = Math.random();
|
||||||
|
|
||||||
|
if (random < 0.33) {
|
||||||
|
// Fake long checkpoint duration
|
||||||
|
await $$`sleep 300`;
|
||||||
|
} else if (random < 0.66) {
|
||||||
|
// Fake checkpoint error
|
||||||
|
await $$`false`;
|
||||||
|
} else {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const shortCode = nanoid(8);
|
const shortCode = nanoid(8);
|
||||||
const imageRef = this.#getImageRef(projectRef, deploymentVersion, shortCode);
|
const imageRef = this.#getImageRef(projectRef, deploymentVersion, shortCode);
|
||||||
const exportLocation = this.#getExportLocation(projectRef, deploymentVersion, shortCode);
|
const exportLocation = this.#getExportLocation(projectRef, deploymentVersion, shortCode);
|
||||||
|
|
||||||
this.#logger.log("Checkpointing:", {
|
this.#logger.log("Checkpointing:", { options });
|
||||||
options: {
|
|
||||||
runId,
|
|
||||||
leaveRunning,
|
|
||||||
projectRef,
|
|
||||||
deploymentVersion,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const containterName = this.#getRunContainerName(runId);
|
const containterName = this.#getRunContainerName(runId);
|
||||||
|
|
||||||
@@ -234,9 +304,9 @@ class Checkpointer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
this.#logger.error(error.stderr);
|
this.#logger.error("Failed while creating docker checkpoint", { exportLocation });
|
||||||
return;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#logger.log("checkpoint created:", {
|
this.#logger.log("checkpoint created:", {
|
||||||
@@ -245,8 +315,11 @@ class Checkpointer {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
location: exportLocation,
|
success: true,
|
||||||
docker: true,
|
checkpoint: {
|
||||||
|
location: exportLocation,
|
||||||
|
docker: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,54 +339,104 @@ class Checkpointer {
|
|||||||
throw new Error("could not find container id");
|
throw new Error("could not find container id");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const start = performance.now();
|
||||||
|
|
||||||
|
// Create checkpoint
|
||||||
this.#logger.debug(await $$`crictl checkpoint --export=${exportLocation} ${containerId}`);
|
this.#logger.debug(await $$`crictl checkpoint --export=${exportLocation} ${containerId}`);
|
||||||
|
const postCheckpoint = performance.now();
|
||||||
|
|
||||||
|
// Print checkpoint size
|
||||||
|
const size = await getParsedFileSize(exportLocation);
|
||||||
|
this.#logger.log("checkpoint archive created", { size, options });
|
||||||
|
|
||||||
// Create image from checkpoint
|
// Create image from checkpoint
|
||||||
const container = this.#logger.debug(await $$`buildah from scratch`);
|
const container = this.#logger.debug(await $$`buildah from scratch`);
|
||||||
|
const postFrom = performance.now();
|
||||||
|
|
||||||
this.#logger.debug(await $$`buildah add ${container} ${exportLocation} /`);
|
this.#logger.debug(await $$`buildah add ${container} ${exportLocation} /`);
|
||||||
|
const postAdd = performance.now();
|
||||||
|
|
||||||
this.#logger.debug(
|
this.#logger.debug(
|
||||||
await $$`buildah config --annotation=io.kubernetes.cri-o.annotations.checkpoint.name=counter ${container}`
|
await $$`buildah config --annotation=io.kubernetes.cri-o.annotations.checkpoint.name=counter ${container}`
|
||||||
);
|
);
|
||||||
|
const postConfig = performance.now();
|
||||||
|
|
||||||
this.#logger.debug(await $$`buildah commit ${container} ${imageRef}`);
|
this.#logger.debug(await $$`buildah commit ${container} ${imageRef}`);
|
||||||
|
const postCommit = performance.now();
|
||||||
|
|
||||||
this.#logger.debug(await $$`buildah rm ${container}`);
|
this.#logger.debug(await $$`buildah rm ${container}`);
|
||||||
|
const postRm = performance.now();
|
||||||
|
|
||||||
// Push checkpoint image
|
// Push checkpoint image
|
||||||
this.#logger.debug(await $$`buildah push --tls-verify=${REGISTRY_TLS_VERIFY} ${imageRef}`);
|
this.#logger.debug(await $$`buildah push --tls-verify=${REGISTRY_TLS_VERIFY} ${imageRef}`);
|
||||||
|
const postPush = performance.now();
|
||||||
|
|
||||||
this.#logger.log("Checkpointed and pushed image to:", { location: imageRef });
|
const perf = {
|
||||||
|
"crictl checkpoint": postCheckpoint - start,
|
||||||
|
"buildah from": postFrom - postCheckpoint,
|
||||||
|
"buildah add": postAdd - postFrom,
|
||||||
|
"buildah config": postConfig - postAdd,
|
||||||
|
"buildah commit": postCommit - postConfig,
|
||||||
|
"buildah rm": postRm - postCommit,
|
||||||
|
"buildah push": postPush - postRm,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#logger.log("Checkpointed and pushed image to:", { location: imageRef, perf });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await $$`rm ${exportLocation}`;
|
await $$`rm ${exportLocation}`;
|
||||||
this.#logger.log("Deleted checkpoint archive", { exportLocation });
|
this.#logger.log("Deleted checkpoint archive", { exportLocation });
|
||||||
|
|
||||||
// Disabled for now as this will increase restore time by having to pull the image again
|
await $`buildah rmi ${imageRef}`;
|
||||||
// await $`buildah rmi ${imageRef}`;
|
this.#logger.log("Deleted checkpoint image", { imageRef });
|
||||||
// this.#logger.log("Deleted checkpoint image", { imageRef });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.#logger.error("Failed during checkpoint cleanup", { exportLocation });
|
this.#logger.error("Failed during checkpoint cleanup", { exportLocation });
|
||||||
this.#logger.debug(error);
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
location: imageRef,
|
success: true,
|
||||||
docker: false,
|
checkpoint: {
|
||||||
|
location: imageRef,
|
||||||
|
docker: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.#logger.error("checkpoint failed", {
|
if (isExecaChildProcess(error)) {
|
||||||
options: {
|
if (error.isCanceled) {
|
||||||
runId,
|
this.#logger.error("Checkpoint canceled", { options, error });
|
||||||
leaveRunning,
|
|
||||||
projectRef,
|
return { success: false, reason: "CANCELED" };
|
||||||
deploymentVersion,
|
}
|
||||||
},
|
|
||||||
error,
|
// Everything that's not a cancellation is a failure
|
||||||
});
|
this.#failCheckpoint(runId, error);
|
||||||
return;
|
this.#logger.error("Checkpoint command error", { options, error });
|
||||||
|
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#failCheckpoint(runId, error);
|
||||||
|
this.#logger.error("Unhandled checkpoint error", { options, error });
|
||||||
|
|
||||||
|
return { success: false };
|
||||||
} finally {
|
} finally {
|
||||||
this.#abortControllers.delete(runId);
|
this.#abortControllers.delete(runId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#failCheckpoint(runId: string, error: unknown) {
|
||||||
|
this.#failedCheckpoints.set(runId, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
#clearFailedCheckpoint(runId: string) {
|
||||||
|
this.#failedCheckpoints.delete(runId);
|
||||||
|
}
|
||||||
|
|
||||||
|
#hasFailedCheckpoint(runId: string) {
|
||||||
|
return this.#failedCheckpoints.has(runId);
|
||||||
|
}
|
||||||
|
|
||||||
#getRunContainerName(suffix: string) {
|
#getRunContainerName(suffix: string) {
|
||||||
return `task-run-${suffix}`;
|
return `task-run-${suffix}`;
|
||||||
}
|
}
|
||||||
@@ -321,7 +444,7 @@ class Checkpointer {
|
|||||||
|
|
||||||
class TaskCoordinator {
|
class TaskCoordinator {
|
||||||
#httpServer: ReturnType<typeof createServer>;
|
#httpServer: ReturnType<typeof createServer>;
|
||||||
#checkpointer = new Checkpointer({ forceSimulate: true });
|
#checkpointer = new Checkpointer({ forceSimulate: FORCE_CHECKPOINT_SIMULATION });
|
||||||
|
|
||||||
#prodWorkerNamespace: ZodNamespace<
|
#prodWorkerNamespace: ZodNamespace<
|
||||||
typeof ProdWorkerToCoordinatorMessages,
|
typeof ProdWorkerToCoordinatorMessages,
|
||||||
@@ -442,6 +565,28 @@ class TaskCoordinator {
|
|||||||
|
|
||||||
taskSocket.emit("REQUEST_ATTEMPT_CANCELLATION", message);
|
taskSocket.emit("REQUEST_ATTEMPT_CANCELLATION", message);
|
||||||
},
|
},
|
||||||
|
REQUEST_RUN_CANCELLATION: async (message) => {
|
||||||
|
const taskSocket = await this.#getRunSocket(message.runId);
|
||||||
|
|
||||||
|
if (!taskSocket) {
|
||||||
|
logger.log("Socket for run not found", {
|
||||||
|
runId: message.runId,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.delayInMs) {
|
||||||
|
taskSocket.emit("REQUEST_EXIT", {
|
||||||
|
version: "v2",
|
||||||
|
delayInMs: message.delayInMs,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If there's no delay, assume the worker doesn't support non-v1 messages
|
||||||
|
taskSocket.emit("REQUEST_EXIT", {
|
||||||
|
version: "v1",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
READY_FOR_RETRY: async (message) => {
|
READY_FOR_RETRY: async (message) => {
|
||||||
const taskSocket = await this.#getRunSocket(message.runId);
|
const taskSocket = await this.#getRunSocket(message.runId);
|
||||||
|
|
||||||
@@ -528,6 +673,20 @@ class TaskCoordinator {
|
|||||||
onConnection: async (socket, handler, sender) => {
|
onConnection: async (socket, handler, sender) => {
|
||||||
const logger = new SimpleLogger(`[prod-worker][${socket.id}]`);
|
const logger = new SimpleLogger(`[prod-worker][${socket.id}]`);
|
||||||
|
|
||||||
|
const crashRun = async (error: { name: string; message: string; stack?: string }) => {
|
||||||
|
try {
|
||||||
|
this.#platformSocket?.send("RUN_CRASHED", {
|
||||||
|
version: "v1",
|
||||||
|
runId: socket.data.runId,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
socket.emit("REQUEST_EXIT", {
|
||||||
|
version: "v1",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const checkpointInProgress = () => {
|
const checkpointInProgress = () => {
|
||||||
return this.#checkpointableTasks.has(socket.data.runId);
|
return this.#checkpointableTasks.has(socket.data.runId);
|
||||||
};
|
};
|
||||||
@@ -596,8 +755,9 @@ class TaskCoordinator {
|
|||||||
if (!executionAck) {
|
if (!executionAck) {
|
||||||
logger.error("no execution ack", { runId: socket.data.runId });
|
logger.error("no execution ack", { runId: socket.data.runId });
|
||||||
|
|
||||||
socket.emit("REQUEST_EXIT", {
|
await crashRun({
|
||||||
version: "v1",
|
name: "ReadyForExecutionError",
|
||||||
|
message: "No execution ack",
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -606,8 +766,9 @@ class TaskCoordinator {
|
|||||||
if (!executionAck.success) {
|
if (!executionAck.success) {
|
||||||
logger.error("failed to get execution payload", { runId: socket.data.runId });
|
logger.error("failed to get execution payload", { runId: socket.data.runId });
|
||||||
|
|
||||||
socket.emit("REQUEST_EXIT", {
|
await crashRun({
|
||||||
version: "v1",
|
name: "ReadyForExecutionError",
|
||||||
|
message: "Failed to get execution payload",
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -624,6 +785,46 @@ class TaskCoordinator {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("READY_FOR_LAZY_ATTEMPT", async (message) => {
|
||||||
|
logger.log("[READY_FOR_LAZY_ATTEMPT]", message);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const lazyAttempt = await this.#platformSocket?.sendWithAck("READY_FOR_LAZY_ATTEMPT", {
|
||||||
|
...message,
|
||||||
|
envId: socket.data.envId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!lazyAttempt) {
|
||||||
|
logger.error("no lazy attempt ack", { runId: socket.data.runId });
|
||||||
|
|
||||||
|
await crashRun({
|
||||||
|
name: "ReadyForLazyAttemptError",
|
||||||
|
message: "No lazy attempt ack",
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lazyAttempt.success) {
|
||||||
|
logger.error("failed to get lazy attempt payload", { runId: socket.data.runId });
|
||||||
|
|
||||||
|
await crashRun({
|
||||||
|
name: "ReadyForLazyAttemptError",
|
||||||
|
message: "Failed to get lazy attempt payload",
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.emit("EXECUTE_TASK_RUN_LAZY_ATTEMPT", {
|
||||||
|
version: "v1",
|
||||||
|
lazyPayload: lazyAttempt.lazyPayload,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error", { error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("READY_FOR_RESUME", async (message) => {
|
socket.on("READY_FOR_RESUME", async (message) => {
|
||||||
logger.log("[READY_FOR_RESUME]", message);
|
logger.log("[READY_FOR_RESUME]", message);
|
||||||
|
|
||||||
@@ -714,6 +915,19 @@ class TaskCoordinator {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("TASK_RUN_FAILED_TO_RUN", async ({ completion }) => {
|
||||||
|
logger.log("completed task", { completionId: completion.id });
|
||||||
|
|
||||||
|
this.#platformSocket?.send("TASK_RUN_FAILED_TO_RUN", {
|
||||||
|
version: "v1",
|
||||||
|
completion,
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.emit("REQUEST_EXIT", {
|
||||||
|
version: "v1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("READY_FOR_CHECKPOINT", async (message) => {
|
socket.on("READY_FOR_CHECKPOINT", async (message) => {
|
||||||
logger.log("[READY_FOR_CHECKPOINT]", message);
|
logger.log("[READY_FOR_CHECKPOINT]", message);
|
||||||
|
|
||||||
@@ -890,7 +1104,7 @@ class TaskCoordinator {
|
|||||||
logger.log("[INDEX_TASKS]", message);
|
logger.log("[INDEX_TASKS]", message);
|
||||||
|
|
||||||
const workerAck = await this.#platformSocket?.sendWithAck("CREATE_WORKER", {
|
const workerAck = await this.#platformSocket?.sendWithAck("CREATE_WORKER", {
|
||||||
version: "v1",
|
version: "v2",
|
||||||
projectRef: socket.data.projectRef,
|
projectRef: socket.data.projectRef,
|
||||||
envId: socket.data.envId,
|
envId: socket.data.envId,
|
||||||
deploymentId: message.deploymentId,
|
deploymentId: message.deploymentId,
|
||||||
@@ -899,6 +1113,7 @@ class TaskCoordinator {
|
|||||||
packageVersion: message.packageVersion,
|
packageVersion: message.packageVersion,
|
||||||
tasks: message.tasks,
|
tasks: message.tasks,
|
||||||
},
|
},
|
||||||
|
supportsLazyAttempts: message.version !== "v1" && message.supportsLazyAttempts,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!workerAck) {
|
if (!workerAck) {
|
||||||
@@ -917,6 +1132,34 @@ class TaskCoordinator {
|
|||||||
error: message.error,
|
error: message.error,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("CREATE_TASK_RUN_ATTEMPT", async (message, callback) => {
|
||||||
|
logger.log("[CREATE_TASK_RUN_ATTEMPT]", message);
|
||||||
|
|
||||||
|
const createAttempt = await this.#platformSocket?.sendWithAck("CREATE_TASK_RUN_ATTEMPT", {
|
||||||
|
runId: message.runId,
|
||||||
|
envId: socket.data.envId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createAttempt?.success) {
|
||||||
|
logger.debug("no ack while creating attempt", message);
|
||||||
|
callback({ success: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket.data.attemptFriendlyId = createAttempt.executionPayload.execution.attempt.id;
|
||||||
|
|
||||||
|
callback({
|
||||||
|
success: true,
|
||||||
|
executionPayload: createAttempt.executionPayload,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("UNRECOVERABLE_ERROR", async (message) => {
|
||||||
|
logger.log("[UNRECOVERABLE_ERROR]", message);
|
||||||
|
|
||||||
|
await crashRun(message.error);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onDisconnect: async (socket, handler, sender, logger) => {
|
onDisconnect: async (socket, handler, sender, logger) => {
|
||||||
this.#platformSocket?.send("LOG", {
|
this.#platformSocket?.send("LOG", {
|
||||||
@@ -928,13 +1171,16 @@ class TaskCoordinator {
|
|||||||
TASK_HEARTBEAT: async (message) => {
|
TASK_HEARTBEAT: async (message) => {
|
||||||
this.#platformSocket?.send("TASK_HEARTBEAT", message);
|
this.#platformSocket?.send("TASK_HEARTBEAT", message);
|
||||||
},
|
},
|
||||||
|
TASK_RUN_HEARTBEAT: async (message) => {
|
||||||
|
this.#platformSocket?.send("TASK_RUN_HEARTBEAT", message);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
#cancelCheckpoint(runId: string) {
|
#cancelCheckpoint(runId: string): boolean {
|
||||||
const checkpointWait = this.#checkpointableTasks.get(runId);
|
const checkpointWait = this.#checkpointableTasks.get(runId);
|
||||||
|
|
||||||
if (checkpointWait) {
|
if (checkpointWait) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ PLATFORM_WS_PORT=3030
|
|||||||
PLATFORM_SECRET=provider-secret
|
PLATFORM_SECRET=provider-secret
|
||||||
SECURE_CONNECTION=false
|
SECURE_CONNECTION=false
|
||||||
|
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT=http://0.0.0.0:3030/otel
|
||||||
|
|
||||||
# Use this if you are on macOS
|
# Use this if you are on macOS
|
||||||
# COORDINATOR_HOST="host.docker.internal"
|
# COORDINATOR_HOST="host.docker.internal"
|
||||||
# OTEL_EXPORTER_OTLP_ENDPOINT="http://host.docker.internal:4318"
|
# OTEL_EXPORTER_OTLP_ENDPOINT="http://host.docker.internal:4318"
|
||||||
@@ -13,9 +13,14 @@ import { PostStartCauses, PreStopCauses } from "@trigger.dev/core/v3";
|
|||||||
const MACHINE_NAME = process.env.MACHINE_NAME || "local";
|
const MACHINE_NAME = process.env.MACHINE_NAME || "local";
|
||||||
const COORDINATOR_PORT = process.env.COORDINATOR_PORT || 8020;
|
const COORDINATOR_PORT = process.env.COORDINATOR_PORT || 8020;
|
||||||
const COORDINATOR_HOST = process.env.COORDINATOR_HOST || "127.0.0.1";
|
const COORDINATOR_HOST = process.env.COORDINATOR_HOST || "127.0.0.1";
|
||||||
|
|
||||||
const OTEL_EXPORTER_OTLP_ENDPOINT =
|
const OTEL_EXPORTER_OTLP_ENDPOINT =
|
||||||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://0.0.0.0:4318";
|
process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://0.0.0.0:4318";
|
||||||
|
|
||||||
|
const FORCE_CHECKPOINT_SIMULATION = ["1", "true"].includes(
|
||||||
|
process.env.FORCE_CHECKPOINT_SIMULATION ?? "true"
|
||||||
|
);
|
||||||
|
|
||||||
const logger = new SimpleLogger(`[${MACHINE_NAME}]`);
|
const logger = new SimpleLogger(`[${MACHINE_NAME}]`);
|
||||||
|
|
||||||
type InitializeReturn = {
|
type InitializeReturn = {
|
||||||
@@ -278,7 +283,7 @@ class DockerTaskOperations implements TaskOperations {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const provider = new ProviderShell({
|
const provider = new ProviderShell({
|
||||||
tasks: new DockerTaskOperations({ forceSimulate: true }),
|
tasks: new DockerTaskOperations({ forceSimulate: FORCE_CHECKPOINT_SIMULATION }),
|
||||||
type: "docker",
|
type: "docker",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ class KubernetesTaskOperations implements TaskOperations {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "populate-taskinfo",
|
name: "populate-taskinfo",
|
||||||
image: "docker.io/library/busybox",
|
image: "registry.digitalocean.com/trigger/busybox",
|
||||||
imagePullPolicy: "IfNotPresent",
|
imagePullPolicy: "IfNotPresent",
|
||||||
command: ["/bin/sh", "-c"],
|
command: ["/bin/sh", "-c"],
|
||||||
args: ["printenv COORDINATOR_HOST | tee /etc/taskinfo/coordinator-host"],
|
args: ["printenv COORDINATOR_HOST | tee /etc/taskinfo/coordinator-host"],
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { LoaderFunctionArgs, json } from "@remix-run/server-runtime";
|
||||||
|
import { prisma } from "~/db.server";
|
||||||
|
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
|
||||||
|
import { marqs } from "~/v3/marqs/index.server";
|
||||||
|
|
||||||
|
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||||
|
// Next authenticate the request
|
||||||
|
const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request);
|
||||||
|
|
||||||
|
if (!authenticationResult) {
|
||||||
|
return json({ error: "Invalid or Missing API key" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: authenticationResult.userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return json({ error: "Invalid or Missing API key" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user.admin) {
|
||||||
|
return json({ error: "You must be an admin to perform this action" }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const details = await marqs?.getSharedQueueDetails();
|
||||||
|
|
||||||
|
return json(details);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
|
||||||
|
import { json } from "@remix-run/server-runtime";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { authenticateApiRequest } from "~/services/apiAuth.server";
|
||||||
|
import { ServiceValidationError } from "~/v3/services/baseService.server";
|
||||||
|
import { CreateTaskRunAttemptService } from "~/v3/services/createTaskRunAttempt.server";
|
||||||
|
|
||||||
|
const ParamsSchema = z.object({
|
||||||
|
/* This is the run friendly ID */
|
||||||
|
runParam: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function action({ request, params }: ActionFunctionArgs) {
|
||||||
|
// 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 service = new CreateTaskRunAttemptService();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { execution } = await service.call(runParam, authenticationResult.environment);
|
||||||
|
|
||||||
|
return json(execution, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ServiceValidationError) {
|
||||||
|
return json({ error: error.message }, { status: error.status ?? 422 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(
|
||||||
|
{ error: error instanceof Error ? error.message : "Internal Server Error" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,6 +44,8 @@ import { GraphileMigrationHelperService } from "./db/graphileMigrationHelper.ser
|
|||||||
import { PerformBulkActionService } from "~/v3/services/bulk/performBulkAction.server";
|
import { PerformBulkActionService } from "~/v3/services/bulk/performBulkAction.server";
|
||||||
import { CancelTaskRunService } from "~/v3/services/cancelTaskRun.server";
|
import { CancelTaskRunService } from "~/v3/services/cancelTaskRun.server";
|
||||||
import { ReplayTaskRunService } from "~/v3/services/replayTaskRun.server";
|
import { ReplayTaskRunService } from "~/v3/services/replayTaskRun.server";
|
||||||
|
import { RequeueTaskRunService } from "~/v3/requeueTaskRun.server";
|
||||||
|
import { RetryAttemptService } from "~/v3/services/retryAttempt.server";
|
||||||
|
|
||||||
const workerCatalog = {
|
const workerCatalog = {
|
||||||
indexEndpoint: z.object({
|
indexEndpoint: z.object({
|
||||||
@@ -158,6 +160,12 @@ const workerCatalog = {
|
|||||||
"v3.performBulkActionItem": z.object({
|
"v3.performBulkActionItem": z.object({
|
||||||
bulkActionItemId: z.string(),
|
bulkActionItemId: z.string(),
|
||||||
}),
|
}),
|
||||||
|
"v3.requeueTaskRun": z.object({
|
||||||
|
runId: z.string(),
|
||||||
|
}),
|
||||||
|
"v3.retryAttempt": z.object({
|
||||||
|
runId: z.string(),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const executionWorkerCatalog = {
|
const executionWorkerCatalog = {
|
||||||
@@ -600,6 +608,24 @@ function getWorkerQueue() {
|
|||||||
await service.performBulkActionItem(payload.bulkActionItemId);
|
await service.performBulkActionItem(payload.bulkActionItemId);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"v3.requeueTaskRun": {
|
||||||
|
priority: 0,
|
||||||
|
maxAttempts: 3,
|
||||||
|
handler: async (payload, job) => {
|
||||||
|
const service = new RequeueTaskRunService();
|
||||||
|
|
||||||
|
await service.call(payload.runId);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"v3.retryAttempt": {
|
||||||
|
priority: 0,
|
||||||
|
maxAttempts: 3,
|
||||||
|
handler: async (payload, job) => {
|
||||||
|
const service = new RetryAttemptService();
|
||||||
|
|
||||||
|
return await service.call(payload.runId);
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ export class AuthenticatedSocketConnection {
|
|||||||
schema: clientWebsocketMessages,
|
schema: clientWebsocketMessages,
|
||||||
messages: {
|
messages: {
|
||||||
READY_FOR_TASKS: async (payload) => {
|
READY_FOR_TASKS: async (payload) => {
|
||||||
await this._consumer.registerBackgroundWorker(payload.backgroundWorkerId);
|
await this._consumer.registerBackgroundWorker(
|
||||||
|
payload.backgroundWorkerId,
|
||||||
|
payload.inProgressRuns ?? []
|
||||||
|
);
|
||||||
},
|
},
|
||||||
BACKGROUND_WORKER_DEPRECATED: async (payload) => {
|
BACKGROUND_WORKER_DEPRECATED: async (payload) => {
|
||||||
await this._consumer.deprecateBackgroundWorker(payload.backgroundWorkerId);
|
await this._consumer.deprecateBackgroundWorker(payload.backgroundWorkerId);
|
||||||
@@ -69,10 +72,22 @@ export class AuthenticatedSocketConnection {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "TASK_RUN_FAILED_TO_RUN": {
|
||||||
|
await this._consumer.taskRunFailed(
|
||||||
|
payload.backgroundWorkerId,
|
||||||
|
payload.data.completion
|
||||||
|
);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "TASK_HEARTBEAT": {
|
case "TASK_HEARTBEAT": {
|
||||||
await this._consumer.taskHeartbeat(payload.backgroundWorkerId, payload.data.id);
|
await this._consumer.taskHeartbeat(payload.backgroundWorkerId, payload.data.id);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "TASK_RUN_HEARTBEAT": {
|
||||||
|
await this._consumer.taskRunHeartbeat(payload.backgroundWorkerId, payload.data.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
SpanEvents,
|
SpanEvents,
|
||||||
SpanMessagingEvent,
|
SpanMessagingEvent,
|
||||||
TaskEventStyle,
|
TaskEventStyle,
|
||||||
|
TaskRunError,
|
||||||
correctErrorStackTrace,
|
correctErrorStackTrace,
|
||||||
createPacketAttributesAsJson,
|
createPacketAttributesAsJson,
|
||||||
flattenAttributes,
|
flattenAttributes,
|
||||||
@@ -117,6 +118,7 @@ export type QueriedEvent = Prisma.TaskEventGetPayload<{
|
|||||||
isCancelled: true;
|
isCancelled: true;
|
||||||
level: true;
|
level: true;
|
||||||
events: true;
|
events: true;
|
||||||
|
environmentType: true;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@@ -156,6 +158,7 @@ export type SpanSummary = {
|
|||||||
isPartial: boolean;
|
isPartial: boolean;
|
||||||
isCancelled: boolean;
|
isCancelled: boolean;
|
||||||
level: NonNullable<CreatableEvent["level"]>;
|
level: NonNullable<CreatableEvent["level"]>;
|
||||||
|
environmentType: CreatableEventEnvironmentType;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,6 +168,7 @@ export type UpdateEventOptions = {
|
|||||||
attributes: TraceAttributes;
|
attributes: TraceAttributes;
|
||||||
endTime?: Date;
|
endTime?: Date;
|
||||||
immediate?: boolean;
|
immediate?: boolean;
|
||||||
|
events?: SpanEvents;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class EventRepository {
|
export class EventRepository {
|
||||||
@@ -239,7 +243,7 @@ export class EventRepository {
|
|||||||
isCancelled: false,
|
isCancelled: false,
|
||||||
status: options?.attributes.isError ? "ERROR" : "OK",
|
status: options?.attributes.isError ? "ERROR" : "OK",
|
||||||
links: event.links ?? [],
|
links: event.links ?? [],
|
||||||
events: event.events ?? [],
|
events: event.events ?? (options?.events as any) ?? [],
|
||||||
duration: calculateDurationFromStart(event.startTime, options?.endTime),
|
duration: calculateDurationFromStart(event.startTime, options?.endTime),
|
||||||
properties: event.properties as Attributes,
|
properties: event.properties as Attributes,
|
||||||
metadata: event.metadata as Attributes,
|
metadata: event.metadata as Attributes,
|
||||||
@@ -386,6 +390,7 @@ export class EventRepository {
|
|||||||
isCancelled: true,
|
isCancelled: true,
|
||||||
level: true,
|
level: true,
|
||||||
events: true,
|
events: true,
|
||||||
|
environmentType: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
traceId,
|
traceId,
|
||||||
@@ -421,6 +426,7 @@ export class EventRepository {
|
|||||||
startTime: getDateFromNanoseconds(event.startTime),
|
startTime: getDateFromNanoseconds(event.startTime),
|
||||||
level: event.level,
|
level: event.level,
|
||||||
events: event.events,
|
events: event.events,
|
||||||
|
environmentType: event.environmentType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -505,7 +511,11 @@ export class EventRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const events = transformEvents(span.data.events, fullEvent.metadata as Attributes);
|
const events = transformEvents(
|
||||||
|
span.data.events,
|
||||||
|
fullEvent.metadata as Attributes,
|
||||||
|
traceSummary?.rootSpan.data.environmentType === "DEVELOPMENT"
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...fullEvent,
|
...fullEvent,
|
||||||
@@ -877,6 +887,36 @@ export function stripAttributePrefix(attributes: Attributes, prefix: string) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createExceptionPropertiesFromError(error: TaskRunError): ExceptionEventProperties {
|
||||||
|
switch (error.type) {
|
||||||
|
case "BUILT_IN_ERROR": {
|
||||||
|
return {
|
||||||
|
type: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stacktrace: error.stackTrace,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "CUSTOM_ERROR": {
|
||||||
|
return {
|
||||||
|
type: "Error",
|
||||||
|
message: error.raw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "INTERNAL_ERROR": {
|
||||||
|
return {
|
||||||
|
type: "Internal error",
|
||||||
|
message: [error.code, error.message].filter(Boolean).join(": "),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "STRING_ERROR": {
|
||||||
|
return {
|
||||||
|
type: "Error",
|
||||||
|
message: error.raw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters out partial events from a batch of creatable events, excluding those that have a corresponding full event.
|
* Filters out partial events from a batch of creatable events, excluding those that have a corresponding full event.
|
||||||
* @param batch - The batch of creatable events to filter.
|
* @param batch - The batch of creatable events to filter.
|
||||||
@@ -1097,16 +1137,16 @@ function removePrivateProperties(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformEvents(events: SpanEvents, properties: Attributes): SpanEvents {
|
function transformEvents(events: SpanEvents, properties: Attributes, isDev: boolean): SpanEvents {
|
||||||
return (events ?? []).map((event) => transformEvent(event, properties));
|
return (events ?? []).map((event) => transformEvent(event, properties, isDev));
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformEvent(event: SpanEvent, properties: Attributes): SpanEvent {
|
function transformEvent(event: SpanEvent, properties: Attributes, isDev: boolean): SpanEvent {
|
||||||
if (isExceptionSpanEvent(event)) {
|
if (isExceptionSpanEvent(event)) {
|
||||||
return {
|
return {
|
||||||
...event,
|
...event,
|
||||||
properties: {
|
properties: {
|
||||||
exception: transformException(event.properties.exception, properties),
|
exception: transformException(event.properties.exception, properties, isDev),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1116,11 +1156,12 @@ function transformEvent(event: SpanEvent, properties: Attributes): SpanEvent {
|
|||||||
|
|
||||||
function transformException(
|
function transformException(
|
||||||
exception: ExceptionEventProperties,
|
exception: ExceptionEventProperties,
|
||||||
properties: Attributes
|
properties: Attributes,
|
||||||
|
isDev: boolean
|
||||||
): ExceptionEventProperties {
|
): ExceptionEventProperties {
|
||||||
const projectDirAttributeValue = properties[SemanticInternalAttributes.PROJECT_DIR];
|
const projectDirAttributeValue = properties[SemanticInternalAttributes.PROJECT_DIR];
|
||||||
|
|
||||||
if (typeof projectDirAttributeValue !== "string") {
|
if (projectDirAttributeValue !== undefined && typeof projectDirAttributeValue !== "string") {
|
||||||
return exception;
|
return exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1129,6 +1170,7 @@ function transformException(
|
|||||||
stacktrace: exception.stacktrace
|
stacktrace: exception.stacktrace
|
||||||
? correctErrorStackTrace(exception.stacktrace, projectDirAttributeValue, {
|
? correctErrorStackTrace(exception.stacktrace, projectDirAttributeValue, {
|
||||||
removeFirstLine: true,
|
removeFirstLine: true,
|
||||||
|
isDev,
|
||||||
})
|
})
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { TaskRunFailedExecutionResult } from "@trigger.dev/core/v3";
|
||||||
|
import { logger } from "~/services/logger.server";
|
||||||
|
import { marqs } from "~/v3/marqs/index.server";
|
||||||
|
|
||||||
|
import { TaskRunStatus } from "@trigger.dev/database";
|
||||||
|
import { createExceptionPropertiesFromError, eventRepository } from "./eventRepository.server";
|
||||||
|
import { BaseService } from "./services/baseService.server";
|
||||||
|
|
||||||
|
const FAILABLE_TASK_RUN_STATUSES: TaskRunStatus[] = ["EXECUTING", "PENDING", "WAITING_FOR_DEPLOY"];
|
||||||
|
|
||||||
|
export class FailedTaskRunService extends BaseService {
|
||||||
|
public async call(runFriendlyId: string, completion: TaskRunFailedExecutionResult) {
|
||||||
|
const taskRun = await this._prisma.taskRun.findUnique({
|
||||||
|
where: { friendlyId: runFriendlyId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!taskRun) {
|
||||||
|
logger.error("[FailedTaskRunService] Task run not found", {
|
||||||
|
runFriendlyId,
|
||||||
|
completion,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FAILABLE_TASK_RUN_STATUSES.includes(taskRun.status)) {
|
||||||
|
logger.error("[FailedTaskRunService] Task run is not in a failable state", {
|
||||||
|
taskRun,
|
||||||
|
completion,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No more retries, we need to fail the task run
|
||||||
|
logger.debug("[FailedTaskRunService] Failing task run", { taskRun, completion });
|
||||||
|
|
||||||
|
await marqs?.acknowledgeMessage(taskRun.id);
|
||||||
|
|
||||||
|
// Now we need to "complete" the task run event/span
|
||||||
|
await eventRepository.completeEvent(taskRun.spanId, {
|
||||||
|
endTime: new Date(),
|
||||||
|
attributes: {
|
||||||
|
isError: true,
|
||||||
|
},
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
name: "exception",
|
||||||
|
time: new Date(),
|
||||||
|
properties: {
|
||||||
|
exception: createExceptionPropertiesFromError(completion.error),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await this._prisma.taskRun.update({
|
||||||
|
where: {
|
||||||
|
id: taskRun.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: "SYSTEM_FAILURE",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ import { DeploymentIndexFailed } from "./services/deploymentIndexFailed.server";
|
|||||||
import { Redis } from "ioredis";
|
import { Redis } from "ioredis";
|
||||||
import { createAdapter } from "@socket.io/redis-adapter";
|
import { createAdapter } from "@socket.io/redis-adapter";
|
||||||
import { CrashTaskRunService } from "./services/crashTaskRun.server";
|
import { CrashTaskRunService } from "./services/crashTaskRun.server";
|
||||||
|
import { CreateTaskRunAttemptService } from "./services/createTaskRunAttempt.server";
|
||||||
|
|
||||||
export const socketIo = singleton("socketIo", initalizeIoServer);
|
export const socketIo = singleton("socketIo", initalizeIoServer);
|
||||||
|
|
||||||
@@ -91,6 +92,23 @@ function createCoordinatorNamespace(io: Server) {
|
|||||||
return { success: true, payload };
|
return { success: true, payload };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
READY_FOR_LAZY_ATTEMPT: async (message) => {
|
||||||
|
try {
|
||||||
|
const payload = await sharedQueueTasks.getLazyAttemptPayload(
|
||||||
|
message.envId,
|
||||||
|
message.runId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
logger.error("Failed to retrieve lazy attempt payload", message);
|
||||||
|
return { success: false, reason: "Failed to retrieve payload" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, lazyPayload: payload };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
},
|
||||||
READY_FOR_RESUME: async (message) => {
|
READY_FOR_RESUME: async (message) => {
|
||||||
const resumeAttempt = new ResumeAttemptService();
|
const resumeAttempt = new ResumeAttemptService();
|
||||||
await resumeAttempt.call(message);
|
await resumeAttempt.call(message);
|
||||||
@@ -103,9 +121,15 @@ function createCoordinatorNamespace(io: Server) {
|
|||||||
checkpoint: message.checkpoint,
|
checkpoint: message.checkpoint,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
TASK_RUN_FAILED_TO_RUN: async (message) => {
|
||||||
|
await sharedQueueTasks.taskRunFailed(message.completion);
|
||||||
|
},
|
||||||
TASK_HEARTBEAT: async (message) => {
|
TASK_HEARTBEAT: async (message) => {
|
||||||
await sharedQueueTasks.taskHeartbeat(message.attemptFriendlyId);
|
await sharedQueueTasks.taskHeartbeat(message.attemptFriendlyId);
|
||||||
},
|
},
|
||||||
|
TASK_RUN_HEARTBEAT: async (message) => {
|
||||||
|
await sharedQueueTasks.taskRunHeartbeat(message.runId);
|
||||||
|
},
|
||||||
CHECKPOINT_CREATED: async (message) => {
|
CHECKPOINT_CREATED: async (message) => {
|
||||||
const createCheckpoint = new CreateCheckpointService();
|
const createCheckpoint = new CreateCheckpointService();
|
||||||
await createCheckpoint.call(message);
|
await createCheckpoint.call(message);
|
||||||
@@ -123,6 +147,7 @@ function createCoordinatorNamespace(io: Server) {
|
|||||||
const worker = await service.call(message.projectRef, environment, message.deploymentId, {
|
const worker = await service.call(message.projectRef, environment, message.deploymentId, {
|
||||||
localOnly: false,
|
localOnly: false,
|
||||||
metadata: message.metadata,
|
metadata: message.metadata,
|
||||||
|
supportsLazyAttempts: message.version !== "v1" && message.supportsLazyAttempts,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { success: !!worker };
|
return { success: !!worker };
|
||||||
@@ -131,13 +156,52 @@ function createCoordinatorNamespace(io: Server) {
|
|||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
CREATE_TASK_RUN_ATTEMPT: async (message) => {
|
||||||
|
try {
|
||||||
|
const environment = await findEnvironmentById(message.envId);
|
||||||
|
|
||||||
|
if (!environment) {
|
||||||
|
logger.error("Environment not found", { id: message.envId });
|
||||||
|
return { success: false, reason: "Environment not found" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = new CreateTaskRunAttemptService();
|
||||||
|
const { attempt } = await service.call(message.runId, environment, false);
|
||||||
|
|
||||||
|
const payload = await sharedQueueTasks.getExecutionPayloadFromAttempt(attempt.id, true);
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
logger.error("Failed to retrieve payload after attempt creation", {
|
||||||
|
id: message.envId,
|
||||||
|
});
|
||||||
|
return { success: false, reason: "Failed to retrieve payload" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, executionPayload: payload };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error while creating attempt", { error });
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
},
|
||||||
INDEXING_FAILED: async (message) => {
|
INDEXING_FAILED: async (message) => {
|
||||||
try {
|
try {
|
||||||
const service = new DeploymentIndexFailed();
|
const service = new DeploymentIndexFailed();
|
||||||
|
|
||||||
await service.call(message.deploymentId, message.error);
|
await service.call(message.deploymentId, message.error);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Error while indexing", { error: e });
|
logger.error("Error while processing index failure", { error: e });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
RUN_CRASHED: async (message) => {
|
||||||
|
try {
|
||||||
|
const service = new CrashTaskRunService();
|
||||||
|
|
||||||
|
await service.call(message.runId, {
|
||||||
|
reason: `${message.error.name}: ${message.error.message}`,
|
||||||
|
logs: message.error.stack,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Error while processing run failure", { error: e });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { Context, ROOT_CONTEXT, Span, SpanKind, context, trace } from "@opentelemetry/api";
|
import { Context, ROOT_CONTEXT, Span, SpanKind, context, trace } from "@opentelemetry/api";
|
||||||
import {
|
import {
|
||||||
TaskRunExecution,
|
TaskRunExecution,
|
||||||
|
TaskRunExecutionLazyAttemptPayload,
|
||||||
TaskRunExecutionPayload,
|
TaskRunExecutionPayload,
|
||||||
TaskRunExecutionResult,
|
TaskRunExecutionResult,
|
||||||
|
TaskRunFailedExecutionResult,
|
||||||
serverWebsocketMessages,
|
serverWebsocketMessages,
|
||||||
} from "@trigger.dev/core/v3";
|
} from "@trigger.dev/core/v3";
|
||||||
import { ZodMessageSender } from "@trigger.dev/core/v3/zodMessageHandler";
|
import { ZodMessageSender } from "@trigger.dev/core/v3/zodMessageHandler";
|
||||||
@@ -14,16 +16,16 @@ import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
|||||||
import { logger } from "~/services/logger.server";
|
import { logger } from "~/services/logger.server";
|
||||||
import { marqs, sanitizeQueueName } from "~/v3/marqs/index.server";
|
import { marqs, sanitizeQueueName } from "~/v3/marqs/index.server";
|
||||||
import { EnvironmentVariablesRepository } from "../environmentVariables/environmentVariablesRepository.server";
|
import { EnvironmentVariablesRepository } from "../environmentVariables/environmentVariablesRepository.server";
|
||||||
import { generateFriendlyId } from "../friendlyIdentifiers";
|
|
||||||
import { CancelAttemptService } from "../services/cancelAttempt.server";
|
|
||||||
import { CancelTaskRunService } from "../services/cancelTaskRun.server";
|
import { CancelTaskRunService } from "../services/cancelTaskRun.server";
|
||||||
import { CompleteAttemptService } from "../services/completeAttempt.server";
|
import { CompleteAttemptService } from "../services/completeAttempt.server";
|
||||||
|
import { CreateTaskRunAttemptService } from "../services/createTaskRunAttempt.server";
|
||||||
import {
|
import {
|
||||||
SEMINTATTRS_FORCE_RECORDING,
|
SEMINTATTRS_FORCE_RECORDING,
|
||||||
attributesFromAuthenticatedEnv,
|
attributesFromAuthenticatedEnv,
|
||||||
tracer,
|
tracer,
|
||||||
} from "../tracer.server";
|
} from "../tracer.server";
|
||||||
import { DevSubscriber, devPubSub } from "./devPubSub.server";
|
import { DevSubscriber, devPubSub } from "./devPubSub.server";
|
||||||
|
import { FailedTaskRunService } from "../failedTaskRun.server";
|
||||||
|
|
||||||
const MessageBody = z.discriminatedUnion("type", [
|
const MessageBody = z.discriminatedUnion("type", [
|
||||||
z.object({
|
z.object({
|
||||||
@@ -54,7 +56,6 @@ export class DevQueueConsumer {
|
|||||||
private _taskSuccesses: number = 0;
|
private _taskSuccesses: number = 0;
|
||||||
private _currentSpan: Span | undefined;
|
private _currentSpan: Span | undefined;
|
||||||
private _endSpanInNextIteration = false;
|
private _endSpanInNextIteration = false;
|
||||||
private _inProgressAttempts: Map<string, string> = new Map(); // Keys are task attempt friendly IDs, values are TaskRun ids/queue message ids
|
|
||||||
private _inProgressRuns: Map<string, string> = new Map(); // Keys are task run friendly IDs, values are TaskRun internal ids/queue message ids
|
private _inProgressRuns: Map<string, string> = new Map(); // Keys are task run friendly IDs, values are TaskRun internal ids/queue message ids
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -78,7 +79,7 @@ export class DevQueueConsumer {
|
|||||||
this._backgroundWorkers.delete(id);
|
this._backgroundWorkers.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async registerBackgroundWorker(id: string) {
|
public async registerBackgroundWorker(id: string, inProgressRuns: string[] = []) {
|
||||||
const backgroundWorker = await prisma.backgroundWorker.findUnique({
|
const backgroundWorker = await prisma.backgroundWorker.findUnique({
|
||||||
where: { friendlyId: id, runtimeEnvironmentId: this.env.id },
|
where: { friendlyId: id, runtimeEnvironmentId: this.env.id },
|
||||||
include: {
|
include: {
|
||||||
@@ -96,7 +97,10 @@ export class DevQueueConsumer {
|
|||||||
|
|
||||||
this._backgroundWorkers.set(backgroundWorker.id, backgroundWorker);
|
this._backgroundWorkers.set(backgroundWorker.id, backgroundWorker);
|
||||||
|
|
||||||
logger.debug("Registered background worker", { backgroundWorker: backgroundWorker.id });
|
logger.debug("Registered background worker", {
|
||||||
|
backgroundWorker: backgroundWorker.id,
|
||||||
|
inProgressRuns,
|
||||||
|
});
|
||||||
|
|
||||||
const subscriber = await devPubSub.subscribe(`backgroundWorker:${backgroundWorker.id}:*`);
|
const subscriber = await devPubSub.subscribe(`backgroundWorker:${backgroundWorker.id}:*`);
|
||||||
|
|
||||||
@@ -113,6 +117,10 @@ export class DevQueueConsumer {
|
|||||||
|
|
||||||
this._backgroundWorkerSubscriber.set(backgroundWorker.id, subscriber);
|
this._backgroundWorkerSubscriber.set(backgroundWorker.id, subscriber);
|
||||||
|
|
||||||
|
for (const runId of inProgressRuns) {
|
||||||
|
this._inProgressRuns.set(runId, runId);
|
||||||
|
}
|
||||||
|
|
||||||
// Start reading from the queue if we haven't already
|
// Start reading from the queue if we haven't already
|
||||||
await this.#enable();
|
await this.#enable();
|
||||||
}
|
}
|
||||||
@@ -122,15 +130,16 @@ export class DevQueueConsumer {
|
|||||||
completion: TaskRunExecutionResult,
|
completion: TaskRunExecutionResult,
|
||||||
execution: TaskRunExecution
|
execution: TaskRunExecution
|
||||||
) {
|
) {
|
||||||
this._inProgressAttempts.delete(execution.attempt.id);
|
|
||||||
|
|
||||||
if (completion.ok) {
|
if (completion.ok) {
|
||||||
this._taskSuccesses++;
|
this._taskSuccesses++;
|
||||||
} else {
|
} else {
|
||||||
this._taskFailures++;
|
this._taskFailures++;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("Task run completed", { taskRunCompletion: completion, execution });
|
logger.debug("[DevQueueConsumer] taskAttemptCompleted()", {
|
||||||
|
taskRunCompletion: completion,
|
||||||
|
execution,
|
||||||
|
});
|
||||||
|
|
||||||
const service = new CompleteAttemptService();
|
const service = new CompleteAttemptService();
|
||||||
const result = await service.call({ completion, execution, env: this.env });
|
const result = await service.call({ completion, execution, env: this.env });
|
||||||
@@ -140,7 +149,24 @@ export class DevQueueConsumer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async taskRunFailed(workerId: string, completion: TaskRunFailedExecutionResult) {
|
||||||
|
this._taskFailures++;
|
||||||
|
|
||||||
|
logger.debug("[DevQueueConsumer] taskRunFailed()", { completion });
|
||||||
|
|
||||||
|
this._inProgressRuns.delete(completion.id);
|
||||||
|
|
||||||
|
const service = new FailedTaskRunService();
|
||||||
|
|
||||||
|
await service.call(completion.id, completion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use `taskRunHeartbeat` instead
|
||||||
|
*/
|
||||||
public async taskHeartbeat(workerId: string, id: string, seconds: number = 60) {
|
public async taskHeartbeat(workerId: string, id: string, seconds: number = 60) {
|
||||||
|
logger.debug("[DevQueueConsumer] taskHeartbeat()", { id, seconds });
|
||||||
|
|
||||||
const taskRunAttempt = await prisma.taskRunAttempt.findUnique({
|
const taskRunAttempt = await prisma.taskRunAttempt.findUnique({
|
||||||
where: { friendlyId: id },
|
where: { friendlyId: id },
|
||||||
});
|
});
|
||||||
@@ -152,6 +178,12 @@ export class DevQueueConsumer {
|
|||||||
await marqs?.heartbeatMessage(taskRunAttempt.taskRunId, seconds);
|
await marqs?.heartbeatMessage(taskRunAttempt.taskRunId, seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async taskRunHeartbeat(workerId: string, id: string, seconds: number = 60) {
|
||||||
|
logger.debug("[DevQueueConsumer] taskRunHeartbeat()", { id, seconds });
|
||||||
|
|
||||||
|
await marqs?.heartbeatMessage(id, seconds);
|
||||||
|
}
|
||||||
|
|
||||||
public async stop(reason: string = "CLI disconnected") {
|
public async stop(reason: string = "CLI disconnected") {
|
||||||
if (!this._enabled) {
|
if (!this._enabled) {
|
||||||
return;
|
return;
|
||||||
@@ -184,66 +216,23 @@ export class DevQueueConsumer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async #cancelInProgressRunsAndAttempts(reason: string) {
|
async #cancelInProgressRunsAndAttempts(reason: string) {
|
||||||
const cancelAttemptService = new CancelAttemptService();
|
|
||||||
const cancelTaskRunService = new CancelTaskRunService();
|
const cancelTaskRunService = new CancelTaskRunService();
|
||||||
|
|
||||||
const cancelledAt = new Date();
|
const cancelledAt = new Date();
|
||||||
|
|
||||||
const inProgressAttempts = new Map(this._inProgressAttempts);
|
|
||||||
const inProgressRuns = new Map(this._inProgressRuns);
|
const inProgressRuns = new Map(this._inProgressRuns);
|
||||||
|
|
||||||
this._inProgressAttempts.clear();
|
|
||||||
this._inProgressRuns.clear();
|
this._inProgressRuns.clear();
|
||||||
|
|
||||||
const inProgressRunsWithNoInProgressAttempts: string[] = [];
|
|
||||||
const inProgressAttemptRunIds = new Set(inProgressAttempts.values());
|
|
||||||
|
|
||||||
for (const [runId, messageId] of inProgressRuns) {
|
|
||||||
if (!inProgressAttemptRunIds.has(messageId)) {
|
|
||||||
inProgressRunsWithNoInProgressAttempts.push(messageId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("Cancelling in progress runs and attempts", {
|
logger.debug("Cancelling in progress runs and attempts", {
|
||||||
attempts: Array.from(inProgressAttempts.keys()),
|
|
||||||
runs: Array.from(inProgressRuns.keys()),
|
runs: Array.from(inProgressRuns.keys()),
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const [attemptId, messageId] of inProgressAttempts) {
|
for (const [_, runId] of inProgressRuns) {
|
||||||
await this.#cancelInProgressAttempt(
|
|
||||||
attemptId,
|
|
||||||
messageId,
|
|
||||||
cancelAttemptService,
|
|
||||||
cancelledAt,
|
|
||||||
reason
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const runId of inProgressRunsWithNoInProgressAttempts) {
|
|
||||||
await this.#cancelInProgressRun(runId, cancelTaskRunService, cancelledAt, reason);
|
await this.#cancelInProgressRun(runId, cancelTaskRunService, cancelledAt, reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #cancelInProgressAttempt(
|
|
||||||
attemptId: string,
|
|
||||||
messageId: string,
|
|
||||||
cancelAttemptService: CancelAttemptService,
|
|
||||||
cancelledAt: Date,
|
|
||||||
reason: string
|
|
||||||
) {
|
|
||||||
logger.debug("Cancelling in progress attempt", { attemptId, messageId });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await cancelAttemptService.call(attemptId, messageId, cancelledAt, reason, this.env);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Failed to cancel in progress attempt", {
|
|
||||||
attemptId,
|
|
||||||
messageId,
|
|
||||||
error: e,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async #cancelInProgressRun(
|
async #cancelInProgressRun(
|
||||||
runId: string,
|
runId: string,
|
||||||
service: CancelTaskRunService,
|
service: CancelTaskRunService,
|
||||||
@@ -252,16 +241,20 @@ export class DevQueueConsumer {
|
|||||||
) {
|
) {
|
||||||
logger.debug("Cancelling in progress run", { runId });
|
logger.debug("Cancelling in progress run", { runId });
|
||||||
|
|
||||||
const taskRun = await prisma.taskRun.findUnique({
|
const taskRun = runId.startsWith("run_")
|
||||||
where: { id: runId },
|
? await prisma.taskRun.findUnique({
|
||||||
});
|
where: { friendlyId: runId },
|
||||||
|
})
|
||||||
|
: await prisma.taskRun.findUnique({
|
||||||
|
where: { id: runId },
|
||||||
|
});
|
||||||
|
|
||||||
if (!taskRun) {
|
if (!taskRun) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await service.call(taskRun, { reason, cancelAttempts: false, cancelledAt });
|
await service.call(taskRun, { reason, cancelAttempts: true, cancelledAt });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("Failed to cancel in progress run", {
|
logger.error("Failed to cancel in progress run", {
|
||||||
runId,
|
runId,
|
||||||
@@ -474,141 +467,131 @@ export class DevQueueConsumer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!this._enabled) {
|
if (!this._enabled) {
|
||||||
|
logger.debug("Dev queue consumer is disabled", { env: this.env, queueMessage: message });
|
||||||
|
|
||||||
await marqs?.nackMessage(message.messageId);
|
await marqs?.nackMessage(message.messageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskRunAttempt = await prisma.taskRunAttempt.create({
|
|
||||||
data: {
|
|
||||||
number: lockedTaskRun.attempts[0] ? lockedTaskRun.attempts[0].number + 1 : 1,
|
|
||||||
friendlyId: generateFriendlyId("attempt"),
|
|
||||||
taskRunId: lockedTaskRun.id,
|
|
||||||
startedAt: new Date(),
|
|
||||||
backgroundWorkerId: backgroundTask.workerId,
|
|
||||||
backgroundWorkerTaskId: backgroundTask.id,
|
|
||||||
status: "EXECUTING" as const,
|
|
||||||
queueId: queue.id,
|
|
||||||
runtimeEnvironmentId: this.env.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const execution: TaskRunExecution = {
|
|
||||||
task: {
|
|
||||||
id: backgroundTask.slug,
|
|
||||||
filePath: backgroundTask.filePath,
|
|
||||||
exportName: backgroundTask.exportName,
|
|
||||||
},
|
|
||||||
attempt: {
|
|
||||||
id: taskRunAttempt.friendlyId,
|
|
||||||
number: taskRunAttempt.number,
|
|
||||||
startedAt: taskRunAttempt.startedAt ?? taskRunAttempt.createdAt,
|
|
||||||
backgroundWorkerId: backgroundWorker.id,
|
|
||||||
backgroundWorkerTaskId: backgroundTask.id,
|
|
||||||
status: "EXECUTING" as const,
|
|
||||||
},
|
|
||||||
run: {
|
|
||||||
id: lockedTaskRun.friendlyId,
|
|
||||||
payload: lockedTaskRun.payload,
|
|
||||||
payloadType: lockedTaskRun.payloadType,
|
|
||||||
context: lockedTaskRun.context,
|
|
||||||
createdAt: lockedTaskRun.createdAt,
|
|
||||||
tags: lockedTaskRun.tags.map((tag) => tag.name),
|
|
||||||
isTest: lockedTaskRun.isTest,
|
|
||||||
idempotencyKey: lockedTaskRun.idempotencyKey ?? undefined,
|
|
||||||
},
|
|
||||||
queue: {
|
|
||||||
id: queue.friendlyId,
|
|
||||||
name: queue.name,
|
|
||||||
},
|
|
||||||
environment: {
|
|
||||||
id: this.env.id,
|
|
||||||
slug: this.env.slug,
|
|
||||||
type: this.env.type,
|
|
||||||
},
|
|
||||||
organization: {
|
|
||||||
id: this.env.organization.id,
|
|
||||||
slug: this.env.organization.slug,
|
|
||||||
name: this.env.organization.title,
|
|
||||||
},
|
|
||||||
project: {
|
|
||||||
id: this.env.project.id,
|
|
||||||
ref: this.env.project.externalRef,
|
|
||||||
slug: this.env.project.slug,
|
|
||||||
name: this.env.project.name,
|
|
||||||
},
|
|
||||||
batch:
|
|
||||||
lockedTaskRun.batchItems[0] && lockedTaskRun.batchItems[0].batchTaskRun
|
|
||||||
? { id: lockedTaskRun.batchItems[0].batchTaskRun.friendlyId }
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const environmentRepository = new EnvironmentVariablesRepository();
|
const environmentRepository = new EnvironmentVariablesRepository();
|
||||||
const variables = await environmentRepository.getEnvironmentVariables(
|
const variables = await environmentRepository.getEnvironmentVariables(
|
||||||
this.env.project.id,
|
this.env.project.id,
|
||||||
this.env.id
|
this.env.id
|
||||||
);
|
);
|
||||||
|
|
||||||
const payload: TaskRunExecutionPayload = {
|
if (backgroundWorker.supportsLazyAttempts) {
|
||||||
execution,
|
const payload: TaskRunExecutionLazyAttemptPayload = {
|
||||||
traceContext: lockedTaskRun.traceContext as Record<string, unknown>,
|
traceContext: lockedTaskRun.traceContext as Record<string, unknown>,
|
||||||
environment: variables.reduce((acc: Record<string, string>, curr) => {
|
environment: variables.reduce((acc: Record<string, string>, curr) => {
|
||||||
acc[curr.key] = curr.value;
|
acc[curr.key] = curr.value;
|
||||||
return acc;
|
return acc;
|
||||||
}, {}),
|
}, {}),
|
||||||
};
|
runId: lockedTaskRun.friendlyId,
|
||||||
|
messageId: lockedTaskRun.id,
|
||||||
|
isTest: lockedTaskRun.isTest,
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: send trace context down to the CLI
|
await this._sender.send("BACKGROUND_WORKER_MESSAGE", {
|
||||||
await this._sender.send("BACKGROUND_WORKER_MESSAGE", {
|
backgroundWorkerId: backgroundWorker.friendlyId,
|
||||||
backgroundWorkerId: backgroundWorker.friendlyId,
|
|
||||||
data: {
|
|
||||||
type: "EXECUTE_RUNS",
|
|
||||||
payloads: [payload],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.debug("Saving the in progress attempt", {
|
|
||||||
taskRunAttempt: taskRunAttempt.id,
|
|
||||||
messageId: message.messageId,
|
|
||||||
});
|
|
||||||
|
|
||||||
this._inProgressAttempts.set(taskRunAttempt.friendlyId, message.messageId);
|
|
||||||
this._inProgressRuns.set(lockedTaskRun.friendlyId, message.messageId);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof Error) {
|
|
||||||
this._currentSpan?.recordException(e);
|
|
||||||
} else {
|
|
||||||
this._currentSpan?.recordException(new Error(String(e)));
|
|
||||||
}
|
|
||||||
|
|
||||||
this._endSpanInNextIteration = true;
|
|
||||||
|
|
||||||
// We now need to unlock the task run and delete the task run attempt
|
|
||||||
await prisma.$transaction([
|
|
||||||
prisma.taskRun.update({
|
|
||||||
where: {
|
|
||||||
id: lockedTaskRun.id,
|
|
||||||
},
|
|
||||||
data: {
|
data: {
|
||||||
lockedAt: null,
|
type: "EXECUTE_RUN_LAZY_ATTEMPT",
|
||||||
lockedById: null,
|
payload,
|
||||||
status: "PENDING",
|
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
prisma.taskRunAttempt.delete({
|
|
||||||
where: {
|
logger.debug("Executing the run", {
|
||||||
id: taskRunAttempt.id,
|
messageId: message.messageId,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._inProgressRuns.set(lockedTaskRun.friendlyId, message.messageId);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
this._currentSpan?.recordException(e);
|
||||||
|
} else {
|
||||||
|
this._currentSpan?.recordException(new Error(String(e)));
|
||||||
|
}
|
||||||
|
|
||||||
|
this._endSpanInNextIteration = true;
|
||||||
|
|
||||||
|
// We now need to unlock the task run and delete the task run attempt
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.taskRun.update({
|
||||||
|
where: {
|
||||||
|
id: lockedTaskRun.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
lockedAt: null,
|
||||||
|
lockedById: null,
|
||||||
|
status: "PENDING",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this._inProgressRuns.delete(lockedTaskRun.friendlyId);
|
||||||
|
|
||||||
|
// Finally we need to nack the message so it can be retried
|
||||||
|
await marqs?.nackMessage(message.messageId);
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => this.#doWork(), 100);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const service = new CreateTaskRunAttemptService();
|
||||||
|
const { execution } = await service.call(lockedTaskRun.friendlyId, this.env);
|
||||||
|
|
||||||
|
const payload: TaskRunExecutionPayload = {
|
||||||
|
traceContext: lockedTaskRun.traceContext as Record<string, unknown>,
|
||||||
|
environment: variables.reduce((acc: Record<string, string>, curr) => {
|
||||||
|
acc[curr.key] = curr.value;
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
execution,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this._sender.send("BACKGROUND_WORKER_MESSAGE", {
|
||||||
|
backgroundWorkerId: backgroundWorker.friendlyId,
|
||||||
|
data: {
|
||||||
|
type: "EXECUTE_RUNS",
|
||||||
|
payloads: [payload],
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
]);
|
|
||||||
|
|
||||||
this._inProgressAttempts.delete(taskRunAttempt.friendlyId);
|
logger.debug("Executing the run", {
|
||||||
this._inProgressRuns.delete(lockedTaskRun.friendlyId);
|
messageId: message.messageId,
|
||||||
|
});
|
||||||
|
|
||||||
// Finally we need to nack the message so it can be retried
|
this._inProgressRuns.set(lockedTaskRun.friendlyId, message.messageId);
|
||||||
await marqs?.nackMessage(message.messageId);
|
} catch (e) {
|
||||||
} finally {
|
if (e instanceof Error) {
|
||||||
setTimeout(() => this.#doWork(), 100);
|
this._currentSpan?.recordException(e);
|
||||||
|
} else {
|
||||||
|
this._currentSpan?.recordException(new Error(String(e)));
|
||||||
|
}
|
||||||
|
|
||||||
|
this._endSpanInNextIteration = true;
|
||||||
|
|
||||||
|
// We now need to unlock the task run and delete the task run attempt
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.taskRun.update({
|
||||||
|
where: {
|
||||||
|
id: lockedTaskRun.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
lockedAt: null,
|
||||||
|
lockedById: null,
|
||||||
|
status: "PENDING",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
this._inProgressRuns.delete(lockedTaskRun.friendlyId);
|
||||||
|
|
||||||
|
// Finally we need to nack the message so it can be retried
|
||||||
|
await marqs?.nackMessage(message.messageId);
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => this.#doWork(), 100);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
QueueCapacities,
|
QueueCapacities,
|
||||||
QueueRange,
|
QueueRange,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import { RequeueTaskRunService } from "../requeueTaskRun.server";
|
||||||
|
|
||||||
const tracer = trace.getTracer("marqs");
|
const tracer = trace.getTracer("marqs");
|
||||||
|
|
||||||
@@ -259,6 +260,11 @@ export class MarQS {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await RequeueTaskRunService.enqueue(
|
||||||
|
messageData.messageId,
|
||||||
|
new Date(Date.now() + this.visibilityTimeoutInMs)
|
||||||
|
);
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -272,6 +278,35 @@ export class MarQS {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getSharedQueueDetails() {
|
||||||
|
const parentQueue = constants.SHARED_QUEUE;
|
||||||
|
|
||||||
|
const { range, selectionId } = await this.queuePriorityStrategy.nextCandidateSelection(
|
||||||
|
parentQueue
|
||||||
|
);
|
||||||
|
const queues = await this.#getChildQueuesWithScores(parentQueue, range);
|
||||||
|
|
||||||
|
const queuesWithScores = await this.#calculateQueueScores(queues, (queue) =>
|
||||||
|
this.#calculateMessageQueueCapacities(queue)
|
||||||
|
);
|
||||||
|
|
||||||
|
// We need to priority shuffle here to ensure all workers aren't just working on the highest priority queue
|
||||||
|
const choice = this.queuePriorityStrategy.chooseQueue(
|
||||||
|
queuesWithScores,
|
||||||
|
parentQueue,
|
||||||
|
selectionId
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectionId,
|
||||||
|
queues,
|
||||||
|
queuesWithScores,
|
||||||
|
nextRange: range,
|
||||||
|
queueCount: queues.length,
|
||||||
|
queueChoice: choice,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dequeue a message from the shared queue (this should be used in production environments)
|
* Dequeue a message from the shared queue (this should be used in production environments)
|
||||||
*/
|
*/
|
||||||
@@ -350,6 +385,8 @@ export class MarQS {
|
|||||||
[SemanticAttributes.PARENT_QUEUE]: message.parentQueue,
|
[SemanticAttributes.PARENT_QUEUE]: message.parentQueue,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await RequeueTaskRunService.dequeue(messageId);
|
||||||
|
|
||||||
await this.#callAcknowledgeMessage({
|
await this.#callAcknowledgeMessage({
|
||||||
parentQueue: message.parentQueue,
|
parentQueue: message.parentQueue,
|
||||||
messageKey: this.keys.messageKey(messageId),
|
messageKey: this.keys.messageKey(messageId),
|
||||||
@@ -415,6 +452,8 @@ export class MarQS {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await RequeueTaskRunService.dequeue(messageId);
|
||||||
|
|
||||||
await this.#callAcknowledgeMessage({
|
await this.#callAcknowledgeMessage({
|
||||||
parentQueue: oldMessage.parentQueue,
|
parentQueue: oldMessage.parentQueue,
|
||||||
messageKey: this.keys.messageKey(messageId),
|
messageKey: this.keys.messageKey(messageId),
|
||||||
@@ -481,6 +520,8 @@ export class MarQS {
|
|||||||
[SemanticAttributes.PARENT_QUEUE]: message.parentQueue,
|
[SemanticAttributes.PARENT_QUEUE]: message.parentQueue,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await RequeueTaskRunService.dequeue(messageId);
|
||||||
|
|
||||||
await this.#callNackMessage({
|
await this.#callNackMessage({
|
||||||
messageKey: this.keys.messageKey(messageId),
|
messageKey: this.keys.messageKey(messageId),
|
||||||
messageQueue: message.queue,
|
messageQueue: message.queue,
|
||||||
@@ -506,16 +547,19 @@ export class MarQS {
|
|||||||
|
|
||||||
// This should increment by the number of seconds, but with a max value of Date.now() + visibilityTimeoutInMs
|
// This should increment by the number of seconds, but with a max value of Date.now() + visibilityTimeoutInMs
|
||||||
public async heartbeatMessage(messageId: string, seconds: number = 30) {
|
public async heartbeatMessage(messageId: string, seconds: number = 30) {
|
||||||
|
// We are still calling this for backwards compatibility, but we should be using the v3.requeueTaskRun job
|
||||||
await this.#callHeartbeatMessage({
|
await this.#callHeartbeatMessage({
|
||||||
visibilityQueue: constants.MESSAGE_VISIBILITY_TIMEOUT_QUEUE,
|
visibilityQueue: constants.MESSAGE_VISIBILITY_TIMEOUT_QUEUE,
|
||||||
messageId,
|
messageId,
|
||||||
milliseconds: seconds * 1000,
|
milliseconds: seconds * 1000,
|
||||||
maxVisibilityTimeout: Date.now() + this.visibilityTimeoutInMs,
|
maxVisibilityTimeout: Date.now() + this.visibilityTimeoutInMs,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await RequeueTaskRunService.enqueue(messageId, new Date(Date.now() + seconds * 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
get visibilityTimeoutInMs() {
|
get visibilityTimeoutInMs() {
|
||||||
return this.options.visibilityTimeoutInMs ?? 300000;
|
return this.options.visibilityTimeoutInMs ?? 300000; // 5 minutes
|
||||||
}
|
}
|
||||||
|
|
||||||
async readMessage(messageId: string) {
|
async readMessage(messageId: string) {
|
||||||
@@ -873,7 +917,6 @@ export class MarQS {
|
|||||||
const result = await this.redis.dequeueMessage(
|
const result = await this.redis.dequeueMessage(
|
||||||
messageQueue,
|
messageQueue,
|
||||||
parentQueue,
|
parentQueue,
|
||||||
visibilityQueue,
|
|
||||||
concurrencyLimitKey,
|
concurrencyLimitKey,
|
||||||
envConcurrencyLimitKey,
|
envConcurrencyLimitKey,
|
||||||
orgConcurrencyLimitKey,
|
orgConcurrencyLimitKey,
|
||||||
@@ -881,7 +924,6 @@ export class MarQS {
|
|||||||
envCurrentConcurrencyKey,
|
envCurrentConcurrencyKey,
|
||||||
orgCurrentConcurrencyKey,
|
orgCurrentConcurrencyKey,
|
||||||
messageQueue,
|
messageQueue,
|
||||||
String(this.options.visibilityTimeoutInMs ?? 300000), // 5 minutes
|
|
||||||
String(Date.now()),
|
String(Date.now()),
|
||||||
String(this.options.defaultEnvConcurrency),
|
String(this.options.defaultEnvConcurrency),
|
||||||
String(this.options.defaultOrgConcurrency)
|
String(this.options.defaultOrgConcurrency)
|
||||||
@@ -1007,6 +1049,9 @@ export class MarQS {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This is being replaced by the v3.requeueTaskRun graphile worker job
|
||||||
|
*/
|
||||||
#callHeartbeatMessage({
|
#callHeartbeatMessage({
|
||||||
visibilityQueue,
|
visibilityQueue,
|
||||||
messageId,
|
messageId,
|
||||||
@@ -1145,25 +1190,23 @@ end
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.redis.defineCommand("dequeueMessage", {
|
this.redis.defineCommand("dequeueMessage", {
|
||||||
numberOfKeys: 9,
|
numberOfKeys: 8,
|
||||||
lua: `
|
lua: `
|
||||||
-- Keys: childQueue, parentQueue, visibilityQueue, concurrencyLimitKey, envConcurrencyLimitKey, orgConcurrencyLimitKey, currentConcurrencyKey, envCurrentConcurrencyKey, orgCurrentConcurrencyKey
|
-- Keys: childQueue, parentQueue, concurrencyLimitKey, envConcurrencyLimitKey, orgConcurrencyLimitKey, currentConcurrencyKey, envCurrentConcurrencyKey, orgCurrentConcurrencyKey
|
||||||
local childQueue = KEYS[1]
|
local childQueue = KEYS[1]
|
||||||
local parentQueue = KEYS[2]
|
local parentQueue = KEYS[2]
|
||||||
local visibilityQueue = KEYS[3]
|
local concurrencyLimitKey = KEYS[3]
|
||||||
local concurrencyLimitKey = KEYS[4]
|
local envConcurrencyLimitKey = KEYS[4]
|
||||||
local envConcurrencyLimitKey = KEYS[5]
|
local orgConcurrencyLimitKey = KEYS[5]
|
||||||
local orgConcurrencyLimitKey = KEYS[6]
|
local currentConcurrencyKey = KEYS[6]
|
||||||
local currentConcurrencyKey = KEYS[7]
|
local envCurrentConcurrencyKey = KEYS[7]
|
||||||
local envCurrentConcurrencyKey = KEYS[8]
|
local orgCurrentConcurrencyKey = KEYS[8]
|
||||||
local orgCurrentConcurrencyKey = KEYS[9]
|
|
||||||
|
|
||||||
-- Args: childQueueName, visibilityQueue, currentTime, defaultEnvConcurrencyLimit, defaultOrgConcurrencyLimit
|
-- Args: childQueueName, currentTime, defaultEnvConcurrencyLimit, defaultOrgConcurrencyLimit
|
||||||
local childQueueName = ARGV[1]
|
local childQueueName = ARGV[1]
|
||||||
local visibilityTimeout = tonumber(ARGV[2])
|
local currentTime = tonumber(ARGV[2])
|
||||||
local currentTime = tonumber(ARGV[3])
|
local defaultEnvConcurrencyLimit = ARGV[3]
|
||||||
local defaultEnvConcurrencyLimit = ARGV[4]
|
local defaultOrgConcurrencyLimit = ARGV[4]
|
||||||
local defaultOrgConcurrencyLimit = ARGV[5]
|
|
||||||
|
|
||||||
-- Check current org concurrency against the limit
|
-- Check current org concurrency against the limit
|
||||||
local orgCurrentConcurrency = tonumber(redis.call('SCARD', orgCurrentConcurrencyKey) or '0')
|
local orgCurrentConcurrency = tonumber(redis.call('SCARD', orgCurrentConcurrencyKey) or '0')
|
||||||
@@ -1199,11 +1242,9 @@ end
|
|||||||
|
|
||||||
local messageId = messages[1]
|
local messageId = messages[1]
|
||||||
local messageScore = tonumber(messages[2])
|
local messageScore = tonumber(messages[2])
|
||||||
local timeoutScore = currentTime + visibilityTimeout
|
|
||||||
|
|
||||||
-- Move message to timeout queue and update concurrency
|
-- Move message to timeout queue and update concurrency
|
||||||
redis.call('ZREM', childQueue, messageId)
|
redis.call('ZREM', childQueue, messageId)
|
||||||
redis.call('ZADD', visibilityQueue, timeoutScore, messageId)
|
|
||||||
redis.call('SADD', currentConcurrencyKey, messageId)
|
redis.call('SADD', currentConcurrencyKey, messageId)
|
||||||
redis.call('SADD', envCurrentConcurrencyKey, messageId)
|
redis.call('SADD', envCurrentConcurrencyKey, messageId)
|
||||||
redis.call('SADD', orgCurrentConcurrencyKey, messageId)
|
redis.call('SADD', orgCurrentConcurrencyKey, messageId)
|
||||||
@@ -1269,7 +1310,7 @@ else
|
|||||||
redis.call('ZADD', parentQueue, earliestMessage[2], messageQueueName)
|
redis.call('ZADD', parentQueue, earliestMessage[2], messageQueueName)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Remove the message from the timeout queue
|
-- Remove the message from the timeout queue (deprecated, will eventually remove this)
|
||||||
redis.call('ZREM', visibilityQueue, messageId)
|
redis.call('ZREM', visibilityQueue, messageId)
|
||||||
|
|
||||||
-- Update the concurrency keys
|
-- Update the concurrency keys
|
||||||
@@ -1297,20 +1338,18 @@ local messageId = ARGV[2]
|
|||||||
local currentTime = tonumber(ARGV[3])
|
local currentTime = tonumber(ARGV[3])
|
||||||
local messageScore = tonumber(ARGV[4])
|
local messageScore = tonumber(ARGV[4])
|
||||||
|
|
||||||
-- Check to see if the message is still in the visibilityQueue
|
|
||||||
local messageVisibility = tonumber(redis.call('ZSCORE', visibilityQueue, messageId)) or 0
|
|
||||||
|
|
||||||
if messageVisibility == 0 then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Update the concurrency keys
|
-- Update the concurrency keys
|
||||||
redis.call('SREM', concurrencyKey, messageId)
|
redis.call('SREM', concurrencyKey, messageId)
|
||||||
redis.call('SREM', envConcurrencyKey, messageId)
|
redis.call('SREM', envConcurrencyKey, messageId)
|
||||||
redis.call('SREM', orgConcurrencyKey, messageId)
|
redis.call('SREM', orgConcurrencyKey, messageId)
|
||||||
|
|
||||||
-- Remove the message from the timeout queue
|
-- Check to see if the message is still in the visibilityQueue
|
||||||
redis.call('ZREM', visibilityQueue, messageId)
|
local messageVisibility = tonumber(redis.call('ZSCORE', visibilityQueue, messageId)) or 0
|
||||||
|
|
||||||
|
if messageVisibility > 0 then
|
||||||
|
-- Remove the message from the timeout queue (deprecated, will eventually remove this)
|
||||||
|
redis.call('ZREM', visibilityQueue, messageId)
|
||||||
|
end
|
||||||
|
|
||||||
-- Enqueue the message into the queue
|
-- Enqueue the message into the queue
|
||||||
redis.call('ZADD', childQueueKey, messageScore, messageId)
|
redis.call('ZADD', childQueueKey, messageScore, messageId)
|
||||||
@@ -1337,12 +1376,16 @@ local milliseconds = tonumber(ARGV[2])
|
|||||||
local maxVisibilityTimeout = tonumber(ARGV[3])
|
local maxVisibilityTimeout = tonumber(ARGV[3])
|
||||||
|
|
||||||
-- Get the current visibility timeout
|
-- Get the current visibility timeout
|
||||||
local currentVisibilityTimeout = tonumber(redis.call('ZSCORE', visibilityQueue, messageId)) or 0
|
local zscoreResult = redis.call('ZSCORE', visibilityQueue, messageId)
|
||||||
|
|
||||||
if currentVisibilityTimeout == 0 then
|
-- If there's no currentVisibilityTimeout, return and do not execute ZADD
|
||||||
|
if zscoreResult == false then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local currentVisibilityTimeout = tonumber(zscoreResult)
|
||||||
|
|
||||||
|
|
||||||
-- Calculate the new visibility timeout
|
-- Calculate the new visibility timeout
|
||||||
local newVisibilityTimeout = math.min(currentVisibilityTimeout + milliseconds * 1000, maxVisibilityTimeout)
|
local newVisibilityTimeout = math.min(currentVisibilityTimeout + milliseconds * 1000, maxVisibilityTimeout)
|
||||||
|
|
||||||
@@ -1445,7 +1488,6 @@ declare module "ioredis" {
|
|||||||
dequeueMessage(
|
dequeueMessage(
|
||||||
childQueue: string,
|
childQueue: string,
|
||||||
parentQueue: string,
|
parentQueue: string,
|
||||||
visibilityQueue: string,
|
|
||||||
concurrencyLimitKey: string,
|
concurrencyLimitKey: string,
|
||||||
envConcurrencyLimitKey: string,
|
envConcurrencyLimitKey: string,
|
||||||
orgConcurrencyLimitKey: string,
|
orgConcurrencyLimitKey: string,
|
||||||
@@ -1453,7 +1495,6 @@ declare module "ioredis" {
|
|||||||
envCurrentConcurrencyKey: string,
|
envCurrentConcurrencyKey: string,
|
||||||
orgCurrentConcurrencyKey: string,
|
orgCurrentConcurrencyKey: string,
|
||||||
childQueueName: string,
|
childQueueName: string,
|
||||||
visibilityTimeout: string,
|
|
||||||
currentTime: string,
|
currentTime: string,
|
||||||
defaultEnvConcurrencyLimit: string,
|
defaultEnvConcurrencyLimit: string,
|
||||||
defaultOrgConcurrencyLimit: string,
|
defaultOrgConcurrencyLimit: string,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
ProdTaskRunExecutionPayload,
|
ProdTaskRunExecutionPayload,
|
||||||
TaskRunError,
|
TaskRunError,
|
||||||
TaskRunExecution,
|
TaskRunExecution,
|
||||||
|
TaskRunExecutionLazyAttemptPayload,
|
||||||
TaskRunExecutionResult,
|
TaskRunExecutionResult,
|
||||||
TaskRunFailedExecutionResult,
|
TaskRunFailedExecutionResult,
|
||||||
TaskRunSuccessfulExecutionResult,
|
TaskRunSuccessfulExecutionResult,
|
||||||
@@ -33,6 +34,9 @@ import {
|
|||||||
import { RestoreCheckpointService } from "../services/restoreCheckpoint.server";
|
import { RestoreCheckpointService } from "../services/restoreCheckpoint.server";
|
||||||
import { SEMINTATTRS_FORCE_RECORDING, tracer } from "../tracer.server";
|
import { SEMINTATTRS_FORCE_RECORDING, tracer } from "../tracer.server";
|
||||||
import { CrashTaskRunService } from "../services/crashTaskRun.server";
|
import { CrashTaskRunService } from "../services/crashTaskRun.server";
|
||||||
|
import { FailedTaskRunService } from "../failedTaskRun.server";
|
||||||
|
import { CreateTaskRunAttemptService } from "../services/createTaskRunAttempt.server";
|
||||||
|
import { findEnvironmentById } from "~/models/runtimeEnvironment.server";
|
||||||
|
|
||||||
const WithTraceContext = z.object({
|
const WithTraceContext = z.object({
|
||||||
traceparent: z.string().optional(),
|
traceparent: z.string().optional(),
|
||||||
@@ -260,6 +264,14 @@ export class SharedQueueConsumer {
|
|||||||
where: {
|
where: {
|
||||||
id: message.messageId,
|
id: message.messageId,
|
||||||
},
|
},
|
||||||
|
include: {
|
||||||
|
lockedToVersion: {
|
||||||
|
include: {
|
||||||
|
deployment: true,
|
||||||
|
tasks: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!existingTaskRun) {
|
if (!existingTaskRun) {
|
||||||
@@ -291,7 +303,7 @@ export class SharedQueueConsumer {
|
|||||||
(!retryingFromCheckpoint &&
|
(!retryingFromCheckpoint &&
|
||||||
!EXECUTABLE_RUN_STATUSES.withoutCheckpoint.includes(existingTaskRun.status))
|
!EXECUTABLE_RUN_STATUSES.withoutCheckpoint.includes(existingTaskRun.status))
|
||||||
) {
|
) {
|
||||||
logger.debug("Task run has invalid status for execution", {
|
logger.error("Task run has invalid status for execution", {
|
||||||
queueMessage: message.data,
|
queueMessage: message.data,
|
||||||
messageId: message.messageId,
|
messageId: message.messageId,
|
||||||
taskRun: existingTaskRun.id,
|
taskRun: existingTaskRun.id,
|
||||||
@@ -299,6 +311,12 @@ export class SharedQueueConsumer {
|
|||||||
retryingFromCheckpoint,
|
retryingFromCheckpoint,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const service = new CrashTaskRunService();
|
||||||
|
await service.call(existingTaskRun.id, {
|
||||||
|
crashAttempts: true,
|
||||||
|
reason: `Invalid run status for execution: ${existingTaskRun.status}`,
|
||||||
|
});
|
||||||
|
|
||||||
await this.#ackAndDoMoreWork(message.messageId);
|
await this.#ackAndDoMoreWork(message.messageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -398,6 +416,7 @@ export class SharedQueueConsumer {
|
|||||||
createdAt: "desc",
|
createdAt: "desc",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
lockedBy: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -443,39 +462,12 @@ export class SharedQueueConsumer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const taskRunAttempt = await prisma.taskRunAttempt.create({
|
const nextAttemptNumber = lockedTaskRun.attempts[0]
|
||||||
data: {
|
? lockedTaskRun.attempts[0].number + 1
|
||||||
number: lockedTaskRun.attempts[0] ? lockedTaskRun.attempts[0].number + 1 : 1,
|
: 1;
|
||||||
friendlyId: generateFriendlyId("attempt"),
|
|
||||||
taskRunId: lockedTaskRun.id,
|
|
||||||
startedAt: new Date(),
|
|
||||||
backgroundWorkerId: backgroundTask.workerId,
|
|
||||||
backgroundWorkerTaskId: backgroundTask.id,
|
|
||||||
status: "PENDING" as const,
|
|
||||||
queueId: queue.id,
|
|
||||||
runtimeEnvironmentId: lockedTaskRun.runtimeEnvironmentId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
backgroundWorkerTask: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const isRetry = taskRunAttempt.number > 1;
|
const isRetry = nextAttemptNumber > 1;
|
||||||
|
|
||||||
const { machineConfig } = taskRunAttempt.backgroundWorkerTask;
|
|
||||||
const machine = Machine.safeParse(machineConfig ?? {});
|
|
||||||
|
|
||||||
if (!machine.success) {
|
|
||||||
logger.error("Failed to parse machine config", {
|
|
||||||
queueMessage: message.data,
|
|
||||||
messageId: message.messageId,
|
|
||||||
attemptId: taskRunAttempt.id,
|
|
||||||
machineConfig,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.#ackAndDoMoreWork(message.messageId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
if (messageBody.data.checkpointEventId) {
|
if (messageBody.data.checkpointEventId) {
|
||||||
const restoreService = new RestoreCheckpointService();
|
const restoreService = new RestoreCheckpointService();
|
||||||
@@ -494,12 +486,35 @@ export class SharedQueueConsumer {
|
|||||||
await this.#ackAndDoMoreWork(message.messageId);
|
await this.#ackAndDoMoreWork(message.messageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (isRetry) {
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deployment.worker.supportsLazyAttempts) {
|
||||||
|
const service = new CreateTaskRunAttemptService();
|
||||||
|
await service.call(lockedTaskRun.friendlyId, undefined, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRetry) {
|
||||||
socketIo.coordinatorNamespace.emit("READY_FOR_RETRY", {
|
socketIo.coordinatorNamespace.emit("READY_FOR_RETRY", {
|
||||||
version: "v1",
|
version: "v1",
|
||||||
runId: taskRunAttempt.taskRunId,
|
runId: lockedTaskRun.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const machineConfig = lockedTaskRun.lockedBy?.machineConfig;
|
||||||
|
const machine = Machine.safeParse(machineConfig ?? {});
|
||||||
|
|
||||||
|
if (!machine.success) {
|
||||||
|
logger.error("Failed to parse machine config", {
|
||||||
|
queueMessage: message.data,
|
||||||
|
messageId: message.messageId,
|
||||||
|
machineConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.#ackAndDoMoreWork(message.messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this._sender.send("BACKGROUND_WORKER_MESSAGE", {
|
await this._sender.send("BACKGROUND_WORKER_MESSAGE", {
|
||||||
backgroundWorkerId: deployment.worker.friendlyId,
|
backgroundWorkerId: deployment.worker.friendlyId,
|
||||||
data: {
|
data: {
|
||||||
@@ -508,12 +523,12 @@ export class SharedQueueConsumer {
|
|||||||
version: deployment.version,
|
version: deployment.version,
|
||||||
machine: machine.data,
|
machine: machine.data,
|
||||||
// identifiers
|
// identifiers
|
||||||
id: taskRunAttempt.id,
|
id: "placeholder", // TODO: Remove this completely in a future release
|
||||||
envId: lockedTaskRun.runtimeEnvironment.id,
|
envId: lockedTaskRun.runtimeEnvironment.id,
|
||||||
envType: lockedTaskRun.runtimeEnvironment.type,
|
envType: lockedTaskRun.runtimeEnvironment.type,
|
||||||
orgId: lockedTaskRun.runtimeEnvironment.organizationId,
|
orgId: lockedTaskRun.runtimeEnvironment.organizationId,
|
||||||
projectId: lockedTaskRun.runtimeEnvironment.projectId,
|
projectId: lockedTaskRun.runtimeEnvironment.projectId,
|
||||||
runId: taskRunAttempt.taskRunId,
|
runId: lockedTaskRun.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -535,11 +550,7 @@ export class SharedQueueConsumer {
|
|||||||
data: {
|
data: {
|
||||||
lockedAt: null,
|
lockedAt: null,
|
||||||
lockedById: null,
|
lockedById: null,
|
||||||
},
|
status: lockedTaskRun.status,
|
||||||
}),
|
|
||||||
prisma.taskRunAttempt.delete({
|
|
||||||
where: {
|
|
||||||
id: taskRunAttempt.id,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
@@ -1096,7 +1107,50 @@ class SharedQueueTasks {
|
|||||||
return this.getExecutionPayloadFromAttempt(latestAttempt.id, setToExecuting, isRetrying);
|
return this.getExecutionPayloadFromAttempt(latestAttempt.id, setToExecuting, isRetrying);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getLazyAttemptPayload(
|
||||||
|
envId: string,
|
||||||
|
runId: string
|
||||||
|
): Promise<TaskRunExecutionLazyAttemptPayload | undefined> {
|
||||||
|
const environment = await findEnvironmentById(envId);
|
||||||
|
|
||||||
|
if (!environment) {
|
||||||
|
logger.error("Environment not found", { id: envId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const run = await prisma.taskRun.findUnique({
|
||||||
|
where: {
|
||||||
|
id: runId,
|
||||||
|
runtimeEnvironmentId: environment.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!run) {
|
||||||
|
logger.error("Run not found", { id: runId, envId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const environmentRepository = new EnvironmentVariablesRepository();
|
||||||
|
const variables = await environmentRepository.getEnvironmentVariables(
|
||||||
|
environment.projectId,
|
||||||
|
environment.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
traceContext: run.traceContext as Record<string, unknown>,
|
||||||
|
environment: variables.reduce((acc: Record<string, string>, curr) => {
|
||||||
|
acc[curr.key] = curr.value;
|
||||||
|
return acc;
|
||||||
|
}, {}),
|
||||||
|
runId: run.friendlyId,
|
||||||
|
messageId: run.id,
|
||||||
|
isTest: run.isTest,
|
||||||
|
} satisfies TaskRunExecutionLazyAttemptPayload;
|
||||||
|
}
|
||||||
|
|
||||||
async taskHeartbeat(attemptFriendlyId: string, seconds: number = 60) {
|
async taskHeartbeat(attemptFriendlyId: string, seconds: number = 60) {
|
||||||
|
logger.debug("[SharedQueueConsumer] taskHeartbeat()", { id: attemptFriendlyId, seconds });
|
||||||
|
|
||||||
const taskRunAttempt = await prisma.taskRunAttempt.findUnique({
|
const taskRunAttempt = await prisma.taskRunAttempt.findUnique({
|
||||||
where: { friendlyId: attemptFriendlyId },
|
where: { friendlyId: attemptFriendlyId },
|
||||||
});
|
});
|
||||||
@@ -1107,6 +1161,20 @@ class SharedQueueTasks {
|
|||||||
|
|
||||||
await marqs?.heartbeatMessage(taskRunAttempt.taskRunId, seconds);
|
await marqs?.heartbeatMessage(taskRunAttempt.taskRunId, seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async taskRunHeartbeat(runId: string, seconds: number = 60) {
|
||||||
|
logger.debug("[SharedQueueConsumer] taskRunHeartbeat()", { runId, seconds });
|
||||||
|
|
||||||
|
await marqs?.heartbeatMessage(runId, seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async taskRunFailed(completion: TaskRunFailedExecutionResult) {
|
||||||
|
logger.debug("[SharedQueueConsumer] taskRunFailed()", { completion });
|
||||||
|
|
||||||
|
const service = new FailedTaskRunService();
|
||||||
|
|
||||||
|
await service.call(completion.id, completion);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sharedQueueTasks = singleton("sharedQueueTasks", () => new SharedQueueTasks());
|
export const sharedQueueTasks = singleton("sharedQueueTasks", () => new SharedQueueTasks());
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { logger } from "~/services/logger.server";
|
||||||
|
import { marqs } from "~/v3/marqs/index.server";
|
||||||
|
|
||||||
|
import assertNever from "assert-never";
|
||||||
|
import { FailedTaskRunService } from "./failedTaskRun.server";
|
||||||
|
import { BaseService } from "./services/baseService.server";
|
||||||
|
import { PrismaClientOrTransaction } from "~/db.server";
|
||||||
|
import { workerQueue } from "~/services/worker.server";
|
||||||
|
|
||||||
|
export class RequeueTaskRunService extends BaseService {
|
||||||
|
public async call(runId: string) {
|
||||||
|
const taskRun = await this._prisma.taskRun.findUnique({
|
||||||
|
where: { id: runId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!taskRun) {
|
||||||
|
logger.error("[RequeueTaskRunService] Task run not found", {
|
||||||
|
runId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (taskRun.status) {
|
||||||
|
case "PENDING": {
|
||||||
|
logger.debug("[RequeueTaskRunService] Requeueing task run", { taskRun });
|
||||||
|
|
||||||
|
await marqs?.nackMessage(taskRun.id);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "EXECUTING":
|
||||||
|
case "RETRYING_AFTER_FAILURE": {
|
||||||
|
logger.debug("[RequeueTaskRunService] Failing task run", { taskRun });
|
||||||
|
|
||||||
|
const service = new FailedTaskRunService();
|
||||||
|
|
||||||
|
await service.call(taskRun.friendlyId, {
|
||||||
|
ok: false,
|
||||||
|
id: taskRun.friendlyId,
|
||||||
|
retry: undefined,
|
||||||
|
error: {
|
||||||
|
type: "INTERNAL_ERROR",
|
||||||
|
code: "TASK_RUN_HEARTBEAT_TIMEOUT",
|
||||||
|
message: "Did not receive a heartbeat from the worker in time",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "WAITING_FOR_DEPLOY": {
|
||||||
|
logger.debug("[RequeueTaskRunService] Removing task run from queue", { taskRun });
|
||||||
|
|
||||||
|
await marqs?.acknowledgeMessage(taskRun.id);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "WAITING_TO_RESUME":
|
||||||
|
case "PAUSED": {
|
||||||
|
logger.debug("[RequeueTaskRunService] Requeueing task run", { taskRun });
|
||||||
|
|
||||||
|
await marqs?.nackMessage(taskRun.id);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "SYSTEM_FAILURE":
|
||||||
|
case "INTERRUPTED":
|
||||||
|
case "CRASHED":
|
||||||
|
case "COMPLETED_WITH_ERRORS":
|
||||||
|
case "COMPLETED_SUCCESSFULLY":
|
||||||
|
case "CANCELED": {
|
||||||
|
logger.debug("[RequeueTaskRunService] Task run is completed", { taskRun });
|
||||||
|
|
||||||
|
await marqs?.acknowledgeMessage(taskRun.id);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
assertNever(taskRun.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async enqueue(runId: string, runAt?: Date, tx?: PrismaClientOrTransaction) {
|
||||||
|
return await workerQueue.enqueue(
|
||||||
|
"v3.requeueTaskRun",
|
||||||
|
{ runId },
|
||||||
|
{ runAt, jobKey: `requeueTaskRun:${runId}` }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async dequeue(runId: string, tx?: PrismaClientOrTransaction) {
|
||||||
|
return await workerQueue.dequeue(`requeueTaskRun:${runId}`, { tx });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ export abstract class BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ServiceValidationError extends Error {
|
export class ServiceValidationError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string, public status?: number) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "ServiceValidationError";
|
this.name = "ServiceValidationError";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,15 @@ const CANCELLABLE_ATTEMPT_STATUSES: Array<TaskRunAttemptStatus> = [
|
|||||||
"PENDING",
|
"PENDING",
|
||||||
];
|
];
|
||||||
|
|
||||||
type ExtendedTaskRunAttempt = Prisma.TaskRunAttemptGetPayload<{
|
type ExtendedTaskRun = Prisma.TaskRunGetPayload<{
|
||||||
include: {
|
include: {
|
||||||
runtimeEnvironment: true;
|
runtimeEnvironment: true;
|
||||||
|
lockedToVersion: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type ExtendedTaskRunAttempt = Prisma.TaskRunAttemptGetPayload<{
|
||||||
|
include: {
|
||||||
backgroundWorker: true;
|
backgroundWorker: true;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
@@ -71,11 +77,10 @@ export class CancelTaskRunService extends BaseService {
|
|||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
backgroundWorker: true,
|
backgroundWorker: true,
|
||||||
runtimeEnvironment: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dependency: true,
|
|
||||||
runtimeEnvironment: true,
|
runtimeEnvironment: true,
|
||||||
|
lockedToVersion: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,6 +101,7 @@ export class CancelTaskRunService extends BaseService {
|
|||||||
// Cancel any in progress attempts
|
// Cancel any in progress attempts
|
||||||
if (opts.cancelAttempts) {
|
if (opts.cancelAttempts) {
|
||||||
await this.#cancelPotentiallyRunningAttempts(cancelledTaskRun, cancelledTaskRun.attempts);
|
await this.#cancelPotentiallyRunningAttempts(cancelledTaskRun, cancelledTaskRun.attempts);
|
||||||
|
await this.#cancelRemainingRunWorkers(cancelledTaskRun);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -103,9 +109,12 @@ export class CancelTaskRunService extends BaseService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async #cancelPotentiallyRunningAttempts(run: TaskRun, attempts: ExtendedTaskRunAttempt[]) {
|
async #cancelPotentiallyRunningAttempts(
|
||||||
|
run: ExtendedTaskRun,
|
||||||
|
attempts: ExtendedTaskRunAttempt[]
|
||||||
|
) {
|
||||||
for (const attempt of attempts) {
|
for (const attempt of attempts) {
|
||||||
if (attempt.runtimeEnvironment.type === "DEVELOPMENT") {
|
if (run.runtimeEnvironment.type === "DEVELOPMENT") {
|
||||||
// Signal the task run attempt to stop
|
// Signal the task run attempt to stop
|
||||||
await devPubSub.publish(
|
await devPubSub.publish(
|
||||||
`backgroundWorker:${attempt.backgroundWorkerId}:${attempt.id}`,
|
`backgroundWorker:${attempt.backgroundWorkerId}:${attempt.id}`,
|
||||||
@@ -158,4 +167,19 @@ export class CancelTaskRunService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async #cancelRemainingRunWorkers(run: ExtendedTaskRun) {
|
||||||
|
if (run.runtimeEnvironment.type === "DEVELOPMENT") {
|
||||||
|
// Nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast cancel message to all coordinators
|
||||||
|
socketIo.coordinatorNamespace.emit("REQUEST_RUN_CANCELLATION", {
|
||||||
|
version: "v1",
|
||||||
|
runId: run.id,
|
||||||
|
// Give the attempts some time to exit gracefully. If the runs supports lazy attempts, it also supports exit delays.
|
||||||
|
delayInMs: run.lockedToVersion?.supportsLazyAttempts ? 5_000 : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { PrismaClientOrTransaction } from "~/db.server";
|
|||||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||||
import { logger } from "~/services/logger.server";
|
import { logger } from "~/services/logger.server";
|
||||||
import { safeJsonParse } from "~/utils/json";
|
import { safeJsonParse } from "~/utils/json";
|
||||||
import { eventRepository } from "../eventRepository.server";
|
import { createExceptionPropertiesFromError, eventRepository } from "../eventRepository.server";
|
||||||
import { marqs } from "~/v3/marqs/index.server";
|
import { marqs } from "~/v3/marqs/index.server";
|
||||||
import { BaseService } from "./baseService.server";
|
import { BaseService } from "./baseService.server";
|
||||||
import { CancelAttemptService } from "./cancelAttempt.server";
|
import { CancelAttemptService } from "./cancelAttempt.server";
|
||||||
@@ -20,6 +20,7 @@ import { MAX_TASK_RUN_ATTEMPTS } from "~/consts";
|
|||||||
import { CreateCheckpointService } from "./createCheckpoint.server";
|
import { CreateCheckpointService } from "./createCheckpoint.server";
|
||||||
import { TaskRun } from "@trigger.dev/database";
|
import { TaskRun } from "@trigger.dev/database";
|
||||||
import { PerformTaskAttemptAlertsService } from "./alerts/performTaskAttemptAlerts.server";
|
import { PerformTaskAttemptAlertsService } from "./alerts/performTaskAttemptAlerts.server";
|
||||||
|
import { RetryAttemptService } from "./retryAttempt.server";
|
||||||
|
|
||||||
type FoundAttempt = Awaited<ReturnType<typeof findAttempt>>;
|
type FoundAttempt = Awaited<ReturnType<typeof findAttempt>>;
|
||||||
|
|
||||||
@@ -57,6 +58,8 @@ export class CompleteAttemptService extends BaseService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// No attempt, so there's no message to ACK
|
||||||
|
|
||||||
return "COMPLETED";
|
return "COMPLETED";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,6 +146,8 @@ export class CompleteAttemptService extends BaseService {
|
|||||||
env
|
env
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// The cancel service handles ACK
|
||||||
|
|
||||||
return "COMPLETED";
|
return "COMPLETED";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,7 +178,7 @@ export class CompleteAttemptService extends BaseService {
|
|||||||
properties: {
|
properties: {
|
||||||
retryAt: retryAt.toISOString(),
|
retryAt: retryAt.toISOString(),
|
||||||
},
|
},
|
||||||
runId: taskRunAttempt.taskRunId,
|
runId: taskRunAttempt.taskRun.friendlyId,
|
||||||
style: {
|
style: {
|
||||||
icon: "schedule-attempt",
|
icon: "schedule-attempt",
|
||||||
},
|
},
|
||||||
@@ -185,7 +190,10 @@ export class CompleteAttemptService extends BaseService {
|
|||||||
endTime: retryAt,
|
endTime: retryAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug("Retrying", { taskRun: taskRunAttempt.taskRun.friendlyId });
|
logger.debug("Retrying", {
|
||||||
|
taskRun: taskRunAttempt.taskRun.friendlyId,
|
||||||
|
retry: completion.retry,
|
||||||
|
});
|
||||||
|
|
||||||
await this._prisma.taskRun.update({
|
await this._prisma.taskRun.update({
|
||||||
where: {
|
where: {
|
||||||
@@ -203,7 +211,12 @@ export class CompleteAttemptService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!checkpoint) {
|
if (!checkpoint) {
|
||||||
await this.#enqueueRetry(taskRunAttempt.taskRun, completion.retry.timestamp);
|
await this.#retryAttempt(
|
||||||
|
taskRunAttempt.taskRun,
|
||||||
|
completion.retry.timestamp,
|
||||||
|
undefined,
|
||||||
|
taskRunAttempt.backgroundWorker.supportsLazyAttempts
|
||||||
|
);
|
||||||
return "RETRIED";
|
return "RETRIED";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,10 +244,12 @@ export class CompleteAttemptService extends BaseService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await marqs?.acknowledgeMessage(taskRunAttempt.taskRunId);
|
||||||
|
|
||||||
return "COMPLETED";
|
return "COMPLETED";
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.#enqueueRetry(
|
await this.#retryAttempt(
|
||||||
taskRunAttempt.taskRun,
|
taskRunAttempt.taskRun,
|
||||||
completion.retry.timestamp,
|
completion.retry.timestamp,
|
||||||
checkpointCreateResult.event.id
|
checkpointCreateResult.event.id
|
||||||
@@ -253,6 +268,15 @@ export class CompleteAttemptService extends BaseService {
|
|||||||
attributes: {
|
attributes: {
|
||||||
isError: true,
|
isError: true,
|
||||||
},
|
},
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
name: "exception",
|
||||||
|
time: new Date(),
|
||||||
|
properties: {
|
||||||
|
exception: createExceptionPropertiesFromError(completion.error),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -310,17 +334,28 @@ export class CompleteAttemptService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #enqueueRetry(run: TaskRun, retryTimestamp: number, checkpointEventId?: string) {
|
async #retryAttempt(
|
||||||
// We have to replace a potential RESUME with EXECUTE to correctly retry the attempt
|
run: TaskRun,
|
||||||
return await marqs?.replaceMessage(
|
retryTimestamp: number,
|
||||||
run.id,
|
checkpointEventId?: string,
|
||||||
{
|
supportsLazyAttempts?: boolean
|
||||||
type: "EXECUTE",
|
) {
|
||||||
taskIdentifier: run.taskIdentifier,
|
if (checkpointEventId || !supportsLazyAttempts) {
|
||||||
checkpointEventId: checkpointEventId,
|
// We have to replace a potential RESUME with EXECUTE to correctly retry the attempt
|
||||||
},
|
return await marqs?.replaceMessage(
|
||||||
retryTimestamp
|
run.id,
|
||||||
);
|
{
|
||||||
|
type: "EXECUTE",
|
||||||
|
taskIdentifier: run.taskIdentifier,
|
||||||
|
checkpointEventId: checkpointEventId,
|
||||||
|
},
|
||||||
|
retryTimestamp
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// There's no checkpoint so the worker is still running and waiting for a retry message
|
||||||
|
// It supports lazy attempts so we can bypass the queue and send the message directly to the worker
|
||||||
|
RetryAttemptService.enqueue(run.id, this._prisma, new Date(retryTimestamp));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#generateMetadataAttributesForNextAttempt(execution: TaskRunExecution) {
|
#generateMetadataAttributesForNextAttempt(execution: TaskRunExecution) {
|
||||||
@@ -353,6 +388,7 @@ async function findAttempt(prismaClient: PrismaClientOrTransaction, friendlyId:
|
|||||||
include: {
|
include: {
|
||||||
taskRun: true,
|
taskRun: true,
|
||||||
backgroundWorkerTask: true,
|
backgroundWorkerTask: true,
|
||||||
|
backgroundWorker: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export class CreateBackgroundWorkerService extends BaseService {
|
|||||||
contentHash: body.metadata.contentHash,
|
contentHash: body.metadata.contentHash,
|
||||||
cliVersion: body.metadata.cliPackageVersion,
|
cliVersion: body.metadata.cliPackageVersion,
|
||||||
sdkVersion: body.metadata.packageVersion,
|
sdkVersion: body.metadata.packageVersion,
|
||||||
|
supportsLazyAttempts: body.supportsLazyAttempts,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export class CreateDeployedBackgroundWorkerService extends BaseService {
|
|||||||
contentHash: body.metadata.contentHash,
|
contentHash: body.metadata.contentHash,
|
||||||
cliVersion: body.metadata.cliPackageVersion,
|
cliVersion: body.metadata.cliPackageVersion,
|
||||||
sdkVersion: body.metadata.packageVersion,
|
sdkVersion: body.metadata.packageVersion,
|
||||||
|
supportsLazyAttempts: body.supportsLazyAttempts,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
import { TaskRunExecution } from "@trigger.dev/core/v3";
|
||||||
|
import { $transaction, PrismaClientOrTransaction, prisma } from "~/db.server";
|
||||||
|
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||||
|
import { logger } from "~/services/logger.server";
|
||||||
|
import { generateFriendlyId } from "../friendlyIdentifiers";
|
||||||
|
import { BaseService, ServiceValidationError } from "./baseService.server";
|
||||||
|
import { TaskRun, TaskRunAttempt } from "@trigger.dev/database";
|
||||||
|
|
||||||
|
export class CreateTaskRunAttemptService extends BaseService {
|
||||||
|
public async call(
|
||||||
|
runId: string,
|
||||||
|
env?: AuthenticatedEnvironment,
|
||||||
|
setToExecuting = true
|
||||||
|
): Promise<{
|
||||||
|
execution: TaskRunExecution;
|
||||||
|
run: TaskRun;
|
||||||
|
attempt: TaskRunAttempt;
|
||||||
|
}> {
|
||||||
|
const environment = env ?? (await getAuthenticatedEnvironmentFromRun(runId, this._prisma));
|
||||||
|
|
||||||
|
if (!environment) {
|
||||||
|
throw new ServiceValidationError("Environment not found", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFriendlyId = runId.startsWith("run_");
|
||||||
|
|
||||||
|
return await this.traceWithEnv("call()", environment, async (span) => {
|
||||||
|
if (isFriendlyId) {
|
||||||
|
span.setAttribute("taskRunFriendlyId", runId);
|
||||||
|
} else {
|
||||||
|
span.setAttribute("taskRunId", runId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskRun = await this._prisma.taskRun.findUnique({
|
||||||
|
where: {
|
||||||
|
id: !isFriendlyId ? runId : undefined,
|
||||||
|
friendlyId: isFriendlyId ? runId : undefined,
|
||||||
|
runtimeEnvironmentId: environment.id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
tags: true,
|
||||||
|
attempts: {
|
||||||
|
take: 1,
|
||||||
|
orderBy: {
|
||||||
|
number: "desc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
lockedBy: {
|
||||||
|
include: {
|
||||||
|
worker: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
batchItems: {
|
||||||
|
include: {
|
||||||
|
batchTaskRun: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug("Creating a task run attempt", { taskRun });
|
||||||
|
|
||||||
|
if (!taskRun) {
|
||||||
|
throw new ServiceValidationError("Task run not found", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
span.setAttribute("taskRunId", taskRun.id);
|
||||||
|
span.setAttribute("taskRunFriendlyId", taskRun.friendlyId);
|
||||||
|
|
||||||
|
if (taskRun.status === "CANCELED") {
|
||||||
|
throw new ServiceValidationError("Task run is cancelled", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!taskRun.lockedBy) {
|
||||||
|
throw new ServiceValidationError("Task run is not locked", 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
const queue = await this._prisma.taskQueue.findUnique({
|
||||||
|
where: {
|
||||||
|
runtimeEnvironmentId_name: {
|
||||||
|
runtimeEnvironmentId: environment.id,
|
||||||
|
name: taskRun.queue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!queue) {
|
||||||
|
throw new ServiceValidationError("Queue not found", 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextAttemptNumber = taskRun.attempts[0] ? taskRun.attempts[0].number + 1 : 1;
|
||||||
|
|
||||||
|
const taskRunAttempt = await $transaction(this._prisma, async (tx) => {
|
||||||
|
const taskRunAttempt = await tx.taskRunAttempt.create({
|
||||||
|
data: {
|
||||||
|
number: nextAttemptNumber,
|
||||||
|
friendlyId: generateFriendlyId("attempt"),
|
||||||
|
taskRunId: taskRun.id,
|
||||||
|
startedAt: new Date(),
|
||||||
|
backgroundWorkerId: taskRun.lockedBy!.worker.id,
|
||||||
|
backgroundWorkerTaskId: taskRun.lockedBy!.id,
|
||||||
|
status: setToExecuting ? "EXECUTING" : "PENDING",
|
||||||
|
queueId: queue.id,
|
||||||
|
runtimeEnvironmentId: environment.id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
backgroundWorker: true,
|
||||||
|
backgroundWorkerTask: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (setToExecuting) {
|
||||||
|
await tx.taskRun.update({
|
||||||
|
where: {
|
||||||
|
id: taskRun.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: "EXECUTING",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return taskRunAttempt;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!taskRunAttempt) {
|
||||||
|
logger.error("Failed to create task run attempt", { runId: taskRun.id, nextAttemptNumber });
|
||||||
|
throw new ServiceValidationError("Failed to create task run attempt", 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
const execution: TaskRunExecution = {
|
||||||
|
task: {
|
||||||
|
id: taskRun.lockedBy.slug,
|
||||||
|
filePath: taskRun.lockedBy.filePath,
|
||||||
|
exportName: taskRun.lockedBy.exportName,
|
||||||
|
},
|
||||||
|
attempt: {
|
||||||
|
id: taskRunAttempt.friendlyId,
|
||||||
|
number: taskRunAttempt.number,
|
||||||
|
startedAt: taskRunAttempt.startedAt ?? taskRunAttempt.createdAt,
|
||||||
|
backgroundWorkerId: taskRun.lockedBy.worker.id,
|
||||||
|
backgroundWorkerTaskId: taskRun.lockedBy.id,
|
||||||
|
status: "EXECUTING" as const,
|
||||||
|
},
|
||||||
|
run: {
|
||||||
|
id: taskRun.friendlyId,
|
||||||
|
payload: taskRun.payload,
|
||||||
|
payloadType: taskRun.payloadType,
|
||||||
|
context: taskRun.context,
|
||||||
|
createdAt: taskRun.createdAt,
|
||||||
|
tags: taskRun.tags.map((tag) => tag.name),
|
||||||
|
isTest: taskRun.isTest,
|
||||||
|
idempotencyKey: taskRun.idempotencyKey ?? undefined,
|
||||||
|
},
|
||||||
|
queue: {
|
||||||
|
id: queue.friendlyId,
|
||||||
|
name: queue.name,
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
id: environment.id,
|
||||||
|
slug: environment.slug,
|
||||||
|
type: environment.type,
|
||||||
|
},
|
||||||
|
organization: {
|
||||||
|
id: environment.organization.id,
|
||||||
|
slug: environment.organization.slug,
|
||||||
|
name: environment.organization.title,
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
id: environment.project.id,
|
||||||
|
ref: environment.project.externalRef,
|
||||||
|
slug: environment.project.slug,
|
||||||
|
name: environment.project.name,
|
||||||
|
},
|
||||||
|
batch:
|
||||||
|
taskRun.batchItems[0] && taskRun.batchItems[0].batchTaskRun
|
||||||
|
? { id: taskRun.batchItems[0].batchTaskRun.friendlyId }
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
execution,
|
||||||
|
run: taskRun,
|
||||||
|
attempt: taskRunAttempt,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAuthenticatedEnvironmentFromRun(
|
||||||
|
friendlyId: string,
|
||||||
|
prismaClient?: PrismaClientOrTransaction
|
||||||
|
) {
|
||||||
|
const taskRun = await (prismaClient ?? prisma).taskRun.findUnique({
|
||||||
|
where: {
|
||||||
|
friendlyId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
runtimeEnvironment: {
|
||||||
|
include: {
|
||||||
|
organization: true,
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!taskRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return taskRun?.runtimeEnvironment;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { BaseService } from "./baseService.server";
|
||||||
|
import { logger } from "~/services/logger.server";
|
||||||
|
import { socketIo } from "../handleSocketIo.server";
|
||||||
|
import { PrismaClientOrTransaction } from "~/db.server";
|
||||||
|
import { workerQueue } from "~/services/worker.server";
|
||||||
|
|
||||||
|
export class RetryAttemptService extends BaseService {
|
||||||
|
public async call(runId: string) {
|
||||||
|
const taskRun = await this._prisma.taskRun.findFirst({
|
||||||
|
where: {
|
||||||
|
id: runId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!taskRun) {
|
||||||
|
logger.error("Task run not found", { runId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socketIo.coordinatorNamespace.emit("READY_FOR_RETRY", {
|
||||||
|
version: "v1",
|
||||||
|
runId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static async enqueue(runId: string, tx: PrismaClientOrTransaction, runAt?: Date) {
|
||||||
|
return await workerQueue.enqueue(
|
||||||
|
"v3.retryAttempt",
|
||||||
|
{
|
||||||
|
runId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tx,
|
||||||
|
runAt,
|
||||||
|
jobKey: `retryAttempt:${runId}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,13 +4,12 @@ import {
|
|||||||
TriggerTaskRequestBody,
|
TriggerTaskRequestBody,
|
||||||
packetRequiresOffloading,
|
packetRequiresOffloading,
|
||||||
} from "@trigger.dev/core/v3";
|
} from "@trigger.dev/core/v3";
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { $transaction } from "~/db.server";
|
import { $transaction } from "~/db.server";
|
||||||
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
import { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||||
import { eventRepository } from "../eventRepository.server";
|
import { eventRepository } from "../eventRepository.server";
|
||||||
import { generateFriendlyId } from "../friendlyIdentifiers";
|
import { generateFriendlyId } from "../friendlyIdentifiers";
|
||||||
import { marqs } from "~/v3/marqs/index.server";
|
import { marqs, sanitizeQueueName } from "~/v3/marqs/index.server";
|
||||||
import { uploadToObjectStore } from "../r2.server";
|
import { uploadToObjectStore } from "../r2.server";
|
||||||
import { BaseService } from "./baseService.server";
|
import { BaseService } from "./baseService.server";
|
||||||
|
|
||||||
@@ -112,7 +111,7 @@ export class TriggerTaskService extends BaseService {
|
|||||||
select: { lastNumber: true },
|
select: { lastNumber: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const queueName = body.options?.queue?.name ?? `task/${taskId}`;
|
const queueName = sanitizeQueueName(body.options?.queue?.name ?? `task/${taskId}`);
|
||||||
|
|
||||||
event.setAttribute("queueName", queueName);
|
event.setAttribute("queueName", queueName);
|
||||||
span.setAttribute("queueName", queueName);
|
span.setAttribute("queueName", queueName);
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ import {
|
|||||||
GetProjectResponseBody,
|
GetProjectResponseBody,
|
||||||
ImportEnvironmentVariablesRequestBody,
|
ImportEnvironmentVariablesRequestBody,
|
||||||
EnvironmentVariableResponseBody,
|
EnvironmentVariableResponseBody,
|
||||||
|
TaskRunExecution,
|
||||||
|
APIError,
|
||||||
} from "@trigger.dev/core/v3";
|
} from "@trigger.dev/core/v3";
|
||||||
|
import { zodfetch } from "@trigger.dev/core/v3/zodfetch";
|
||||||
|
|
||||||
export class CliApiClient {
|
export class CliApiClient {
|
||||||
private readonly apiURL: string;
|
private readonly apiURL: string;
|
||||||
@@ -29,7 +32,7 @@ export class CliApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createAuthorizationCode() {
|
async createAuthorizationCode() {
|
||||||
return zodfetch(
|
return wrapZodFetch(
|
||||||
CreateAuthorizationCodeResponseSchema,
|
CreateAuthorizationCodeResponseSchema,
|
||||||
`${this.apiURL}/api/v1/authorization-code`,
|
`${this.apiURL}/api/v1/authorization-code`,
|
||||||
{
|
{
|
||||||
@@ -39,7 +42,7 @@ export class CliApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPersonalAccessToken(authorizationCode: string) {
|
async getPersonalAccessToken(authorizationCode: string) {
|
||||||
return zodfetch(GetPersonalAccessTokenResponseSchema, `${this.apiURL}/api/v1/token`, {
|
return wrapZodFetch(GetPersonalAccessTokenResponseSchema, `${this.apiURL}/api/v1/token`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
authorizationCode,
|
authorizationCode,
|
||||||
@@ -52,7 +55,7 @@ export class CliApiClient {
|
|||||||
throw new Error("whoAmI: No access token");
|
throw new Error("whoAmI: No access token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return zodfetch(WhoAmIResponseSchema, `${this.apiURL}/api/v2/whoami`, {
|
return wrapZodFetch(WhoAmIResponseSchema, `${this.apiURL}/api/v2/whoami`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.accessToken}`,
|
Authorization: `Bearer ${this.accessToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -65,7 +68,7 @@ export class CliApiClient {
|
|||||||
throw new Error("getProject: No access token");
|
throw new Error("getProject: No access token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return zodfetch(GetProjectResponseBody, `${this.apiURL}/api/v1/projects/${projectRef}`, {
|
return wrapZodFetch(GetProjectResponseBody, `${this.apiURL}/api/v1/projects/${projectRef}`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.accessToken}`,
|
Authorization: `Bearer ${this.accessToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -78,7 +81,7 @@ export class CliApiClient {
|
|||||||
throw new Error("getProjects: No access token");
|
throw new Error("getProjects: No access token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return zodfetch(GetProjectsResponseBody, `${this.apiURL}/api/v1/projects`, {
|
return wrapZodFetch(GetProjectsResponseBody, `${this.apiURL}/api/v1/projects`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.accessToken}`,
|
Authorization: `Bearer ${this.accessToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -91,7 +94,7 @@ export class CliApiClient {
|
|||||||
throw new Error("createBackgroundWorker: No access token");
|
throw new Error("createBackgroundWorker: No access token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return zodfetch(
|
return wrapZodFetch(
|
||||||
CreateBackgroundWorkerResponse,
|
CreateBackgroundWorkerResponse,
|
||||||
`${this.apiURL}/api/v1/projects/${projectRef}/background-workers`,
|
`${this.apiURL}/api/v1/projects/${projectRef}/background-workers`,
|
||||||
{
|
{
|
||||||
@@ -105,6 +108,20 @@ export class CliApiClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createTaskRunAttempt(runFriendlyId: string) {
|
||||||
|
if (!this.accessToken) {
|
||||||
|
throw new Error("creatTaskRunAttempt: No access token");
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapZodFetch(TaskRunExecution, `${this.apiURL}/api/v1/runs/${runFriendlyId}/attempts`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getProjectEnv({
|
async getProjectEnv({
|
||||||
projectRef,
|
projectRef,
|
||||||
env,
|
env,
|
||||||
@@ -116,12 +133,16 @@ export class CliApiClient {
|
|||||||
throw new Error("getProjectDevEnv: No access token");
|
throw new Error("getProjectDevEnv: No access token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return zodfetch(GetProjectEnvResponse, `${this.apiURL}/api/v1/projects/${projectRef}/${env}`, {
|
return wrapZodFetch(
|
||||||
headers: {
|
GetProjectEnvResponse,
|
||||||
Authorization: `Bearer ${this.accessToken}`,
|
`${this.apiURL}/api/v1/projects/${projectRef}/${env}`,
|
||||||
"Content-Type": "application/json",
|
{
|
||||||
},
|
headers: {
|
||||||
});
|
Authorization: `Bearer ${this.accessToken}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEnvironmentVariables(projectRef: string) {
|
async getEnvironmentVariables(projectRef: string) {
|
||||||
@@ -129,7 +150,7 @@ export class CliApiClient {
|
|||||||
throw new Error("getEnvironmentVariables: No access token");
|
throw new Error("getEnvironmentVariables: No access token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return zodfetch(
|
return wrapZodFetch(
|
||||||
GetEnvironmentVariablesResponseBody,
|
GetEnvironmentVariablesResponseBody,
|
||||||
`${this.apiURL}/api/v1/projects/${projectRef}/envvars`,
|
`${this.apiURL}/api/v1/projects/${projectRef}/envvars`,
|
||||||
{
|
{
|
||||||
@@ -150,7 +171,7 @@ export class CliApiClient {
|
|||||||
throw new Error("importEnvVars: No access token");
|
throw new Error("importEnvVars: No access token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return zodfetch(
|
return wrapZodFetch(
|
||||||
EnvironmentVariableResponseBody,
|
EnvironmentVariableResponseBody,
|
||||||
`${this.apiURL}/api/v1/projects/${projectRef}/envvars/${slug}/import`,
|
`${this.apiURL}/api/v1/projects/${projectRef}/envvars/${slug}/import`,
|
||||||
{
|
{
|
||||||
@@ -169,7 +190,7 @@ export class CliApiClient {
|
|||||||
throw new Error("initializeDeployment: No access token");
|
throw new Error("initializeDeployment: No access token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return zodfetch(InitializeDeploymentResponseBody, `${this.apiURL}/api/v1/deployments`, {
|
return wrapZodFetch(InitializeDeploymentResponseBody, `${this.apiURL}/api/v1/deployments`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${this.accessToken}`,
|
Authorization: `Bearer ${this.accessToken}`,
|
||||||
@@ -184,7 +205,7 @@ export class CliApiClient {
|
|||||||
throw new Error("startDeploymentIndexing: No access token");
|
throw new Error("startDeploymentIndexing: No access token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return zodfetch(
|
return wrapZodFetch(
|
||||||
StartDeploymentIndexingResponseBody,
|
StartDeploymentIndexingResponseBody,
|
||||||
`${this.apiURL}/api/v1/deployments/${deploymentId}/start-indexing`,
|
`${this.apiURL}/api/v1/deployments/${deploymentId}/start-indexing`,
|
||||||
{
|
{
|
||||||
@@ -203,7 +224,7 @@ export class CliApiClient {
|
|||||||
throw new Error("getDeployment: No access token");
|
throw new Error("getDeployment: No access token");
|
||||||
}
|
}
|
||||||
|
|
||||||
return zodfetch(
|
return wrapZodFetch(
|
||||||
GetDeploymentResponseBody,
|
GetDeploymentResponseBody,
|
||||||
`${this.apiURL}/api/v1/deployments/${deploymentId}`,
|
`${this.apiURL}/api/v1/deployments/${deploymentId}`,
|
||||||
{
|
{
|
||||||
@@ -223,56 +244,42 @@ type ApiResult<TSuccessResult> =
|
|||||||
error: string;
|
error: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function zodfetch<TResponseBody extends any>(
|
async function wrapZodFetch<T extends z.ZodTypeAny>(
|
||||||
schema: z.Schema<TResponseBody>,
|
schema: T,
|
||||||
url: string,
|
url: string,
|
||||||
requestInit?: RequestInit
|
requestInit?: RequestInit
|
||||||
): Promise<ApiResult<TResponseBody>> {
|
): Promise<ApiResult<z.infer<T>>> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, requestInit);
|
const response = await zodfetch(schema, url, requestInit, {
|
||||||
|
retry: {
|
||||||
|
minTimeoutInMs: 500,
|
||||||
|
maxTimeoutInMs: 5000,
|
||||||
|
maxAttempts: 3,
|
||||||
|
factor: 2,
|
||||||
|
randomize: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if ((!requestInit || requestInit.method === "GET") && response.status === 404) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `404: ${response.statusText}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status >= 400 && response.status < 500) {
|
|
||||||
const body = await response.json();
|
|
||||||
if (!body.error) {
|
|
||||||
return { success: false, error: "Something went wrong" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: false, error: body.error };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `Failed to fetch ${url}, got status code ${response.status}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const jsonBody = await response.json();
|
|
||||||
const parsedResult = schema.safeParse(jsonBody);
|
|
||||||
|
|
||||||
if (parsedResult.success) {
|
|
||||||
return { success: true, data: parsedResult.data };
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("error" in jsonBody) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: typeof jsonBody.error === "string" ? jsonBody.error : JSON.stringify(jsonBody.error),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: false, error: parsedResult.error.message };
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: true,
|
||||||
error: error instanceof Error ? error.message : JSON.stringify(error),
|
data: response,
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof APIError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,7 +199,9 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
|
|||||||
`Failed to connect to ${authorization.auth?.apiUrl}. Are you sure it's the correct URL?`
|
`Failed to connect to ${authorization.auth?.apiUrl}. Are you sure it's the correct URL?`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new Error("You must login first. Use `trigger.dev login` to login.");
|
throw new Error(
|
||||||
|
`You must login first. Use the \`login\` CLI command.\n\n${authorization.error}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,11 @@ export async function devCommand(dir: string, options: DevCommandOptions) {
|
|||||||
)} Connecting to the server failed. Please check your internet connection or contact eric@trigger.dev for help.`
|
)} Connecting to the server failed. Please check your internet connection or contact eric@trigger.dev for help.`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.log(`${chalkError("X Error:")} You must login first. Use the \`login\` CLI command.`);
|
logger.log(
|
||||||
|
`${chalkError("X Error:")} You must login first. Use the \`login\` CLI command.\n\n${
|
||||||
|
authorization.error
|
||||||
|
}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return;
|
||||||
@@ -285,6 +289,7 @@ function useDev({
|
|||||||
websocket.addEventListener("close", (event) => {});
|
websocket.addEventListener("close", (event) => {});
|
||||||
websocket.addEventListener("error", (event) => {});
|
websocket.addEventListener("error", (event) => {});
|
||||||
|
|
||||||
|
// This is the deprecated task heart beat that uses the friendly attempt ID
|
||||||
backgroundWorkerCoordinator.onWorkerTaskHeartbeat.attach(
|
backgroundWorkerCoordinator.onWorkerTaskHeartbeat.attach(
|
||||||
async ({ worker, backgroundWorkerId, id }) => {
|
async ({ worker, backgroundWorkerId, id }) => {
|
||||||
await sender.send("BACKGROUND_WORKER_MESSAGE", {
|
await sender.send("BACKGROUND_WORKER_MESSAGE", {
|
||||||
@@ -297,6 +302,19 @@ function useDev({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// "Task Run Heartbeat" id is the actual run ID that corresponds to the MarQS message ID
|
||||||
|
backgroundWorkerCoordinator.onWorkerTaskRunHeartbeat.attach(
|
||||||
|
async ({ worker, backgroundWorkerId, id }) => {
|
||||||
|
await sender.send("BACKGROUND_WORKER_MESSAGE", {
|
||||||
|
backgroundWorkerId,
|
||||||
|
data: {
|
||||||
|
type: "TASK_RUN_HEARTBEAT",
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
backgroundWorkerCoordinator.onTaskCompleted.attach(
|
backgroundWorkerCoordinator.onTaskCompleted.attach(
|
||||||
async ({ backgroundWorkerId, completion, execution }) => {
|
async ({ backgroundWorkerId, completion, execution }) => {
|
||||||
await sender.send("BACKGROUND_WORKER_MESSAGE", {
|
await sender.send("BACKGROUND_WORKER_MESSAGE", {
|
||||||
@@ -310,6 +328,18 @@ function useDev({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
backgroundWorkerCoordinator.onTaskFailedToRun.attach(
|
||||||
|
async ({ backgroundWorkerId, completion }) => {
|
||||||
|
await sender.send("BACKGROUND_WORKER_MESSAGE", {
|
||||||
|
backgroundWorkerId,
|
||||||
|
data: {
|
||||||
|
type: "TASK_RUN_FAILED_TO_RUN",
|
||||||
|
completion,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
backgroundWorkerCoordinator.onWorkerRegistered.attach(async ({ id, worker, record }) => {
|
backgroundWorkerCoordinator.onWorkerRegistered.attach(async ({ id, worker, record }) => {
|
||||||
await sender.send("READY_FOR_TASKS", {
|
await sender.send("READY_FOR_TASKS", {
|
||||||
backgroundWorkerId: id,
|
backgroundWorkerId: id,
|
||||||
@@ -334,6 +364,7 @@ function useDev({
|
|||||||
for (const worker of backgroundWorkerCoordinator.currentWorkers) {
|
for (const worker of backgroundWorkerCoordinator.currentWorkers) {
|
||||||
await sender.send("READY_FOR_TASKS", {
|
await sender.send("READY_FOR_TASKS", {
|
||||||
backgroundWorkerId: worker.id,
|
backgroundWorkerId: worker.id,
|
||||||
|
inProgressRuns: worker.worker.inProgressRuns,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -505,21 +536,25 @@ function useDev({
|
|||||||
|
|
||||||
const processEnv = await gatherProcessEnv();
|
const processEnv = await gatherProcessEnv();
|
||||||
|
|
||||||
const backgroundWorker = new BackgroundWorker(fullPath, {
|
const backgroundWorker = new BackgroundWorker(
|
||||||
projectConfig: config,
|
fullPath,
|
||||||
dependencies,
|
{
|
||||||
env: {
|
projectConfig: config,
|
||||||
...processEnv,
|
dependencies,
|
||||||
TRIGGER_API_URL: apiUrl,
|
env: {
|
||||||
TRIGGER_SECRET_KEY: apiKey,
|
...processEnv,
|
||||||
...(environmentVariablesResponse.success
|
TRIGGER_API_URL: apiUrl,
|
||||||
? environmentVariablesResponse.data.variables
|
TRIGGER_SECRET_KEY: apiKey,
|
||||||
: {}),
|
...(environmentVariablesResponse.success
|
||||||
|
? environmentVariablesResponse.data.variables
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
debuggerOn,
|
||||||
|
debugOtel,
|
||||||
|
resolveEnvVariables: createResolveEnvironmentVariablesFunction(configModule),
|
||||||
},
|
},
|
||||||
debuggerOn,
|
environmentClient
|
||||||
debugOtel,
|
);
|
||||||
resolveEnvVariables: createResolveEnvironmentVariablesFunction(configModule),
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await backgroundWorker.initialize();
|
await backgroundWorker.initialize();
|
||||||
@@ -576,6 +611,7 @@ function useDev({
|
|||||||
tasks: taskResources,
|
tasks: taskResources,
|
||||||
contentHash: contentHash,
|
contentHash: contentHash,
|
||||||
},
|
},
|
||||||
|
supportsLazyAttempts: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const backgroundWorkerRecord = await environmentClient.createBackgroundWorker(
|
const backgroundWorkerRecord = await environmentClient.createBackgroundWorker(
|
||||||
@@ -827,18 +863,9 @@ function createDuplicateTaskIdOutputErrorMessage(
|
|||||||
|
|
||||||
async function gatherProcessEnv() {
|
async function gatherProcessEnv() {
|
||||||
const env = {
|
const env = {
|
||||||
|
...process.env,
|
||||||
NODE_ENV: process.env.NODE_ENV ?? "development",
|
NODE_ENV: process.env.NODE_ENV ?? "development",
|
||||||
PATH: process.env.PATH,
|
|
||||||
USER: process.env.USER,
|
|
||||||
SHELL: process.env.SHELL,
|
|
||||||
NVM_INC: process.env.NVM_INC,
|
|
||||||
NVM_DIR: process.env.NVM_DIR,
|
|
||||||
NVM_BIN: process.env.NVM_BIN,
|
|
||||||
LANG: process.env.LANG,
|
|
||||||
TERM: process.env.TERM,
|
|
||||||
NODE_PATH: await amendNodePathWithPnpmNodeModules(process.env.NODE_PATH),
|
NODE_PATH: await amendNodePathWithPnpmNodeModules(process.env.NODE_PATH),
|
||||||
HOME: process.env.HOME,
|
|
||||||
BUN_INSTALL: process.env.BUN_INSTALL,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filter out undefined values
|
// Filter out undefined values
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export async function whoAmI(
|
|||||||
options?.profile ?? "default"
|
options?.profile ?? "default"
|
||||||
}\` to login.`
|
}\` to login.`
|
||||||
);
|
);
|
||||||
outro("Whoami failed");
|
outro(`Whoami failed: ${authentication.error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,43 @@ export class TaskMetadataParseError extends Error {
|
|||||||
this.name = "TaskMetadataParseError";
|
this.name = "TaskMetadataParseError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UnexpectedExitError extends Error {
|
||||||
|
constructor(public code: number) {
|
||||||
|
super(`Unexpected exit with code ${code}`);
|
||||||
|
|
||||||
|
this.name = "UnexpectedExitError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CleanupProcessError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super("Cancelled");
|
||||||
|
|
||||||
|
this.name = "CleanupProcessError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CancelledProcessError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super("Cancelled");
|
||||||
|
|
||||||
|
this.name = "CancelledProcessError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SigKillTimeoutProcessError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super("Process kill timeout");
|
||||||
|
|
||||||
|
this.name = "SigKillTimeoutProcessError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GracefulExitTimeoutError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super("Graceful exit timeout");
|
||||||
|
|
||||||
|
this.name = "GracefulExitTimeoutError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import {
|
|||||||
TaskRunError,
|
TaskRunError,
|
||||||
TaskRunErrorCodes,
|
TaskRunErrorCodes,
|
||||||
TaskRunExecution,
|
TaskRunExecution,
|
||||||
|
TaskRunExecutionLazyAttemptPayload,
|
||||||
TaskRunExecutionPayload,
|
TaskRunExecutionPayload,
|
||||||
TaskRunExecutionResult,
|
TaskRunExecutionResult,
|
||||||
|
TaskRunFailedExecutionResult,
|
||||||
childToWorkerMessages,
|
childToWorkerMessages,
|
||||||
correctErrorStackTrace,
|
correctErrorStackTrace,
|
||||||
formatDurationMilliseconds,
|
formatDurationMilliseconds,
|
||||||
@@ -36,8 +38,15 @@ import {
|
|||||||
import { safeDeleteFileSync } from "../../utilities/fileSystem.js";
|
import { safeDeleteFileSync } from "../../utilities/fileSystem.js";
|
||||||
import { installPackages } from "../../utilities/installPackages.js";
|
import { installPackages } from "../../utilities/installPackages.js";
|
||||||
import { logger } from "../../utilities/logger.js";
|
import { logger } from "../../utilities/logger.js";
|
||||||
import { TaskMetadataParseError, UncaughtExceptionError } from "../common/errors.js";
|
import {
|
||||||
import { env } from "node:process";
|
CancelledProcessError,
|
||||||
|
CleanupProcessError,
|
||||||
|
SigKillTimeoutProcessError,
|
||||||
|
TaskMetadataParseError,
|
||||||
|
UncaughtExceptionError,
|
||||||
|
UnexpectedExitError,
|
||||||
|
} from "../common/errors.js";
|
||||||
|
import { CliApiClient } from "../../apiClient.js";
|
||||||
|
|
||||||
export type CurrentWorkers = BackgroundWorkerCoordinator["currentWorkers"];
|
export type CurrentWorkers = BackgroundWorkerCoordinator["currentWorkers"];
|
||||||
export class BackgroundWorkerCoordinator {
|
export class BackgroundWorkerCoordinator {
|
||||||
@@ -47,37 +56,52 @@ export class BackgroundWorkerCoordinator {
|
|||||||
worker: BackgroundWorker;
|
worker: BackgroundWorker;
|
||||||
execution: TaskRunExecution;
|
execution: TaskRunExecution;
|
||||||
}> = new Evt();
|
}> = new Evt();
|
||||||
|
public onTaskFailedToRun: Evt<{
|
||||||
|
backgroundWorkerId: string;
|
||||||
|
worker: BackgroundWorker;
|
||||||
|
completion: TaskRunFailedExecutionResult;
|
||||||
|
}> = new Evt();
|
||||||
public onWorkerRegistered: Evt<{
|
public onWorkerRegistered: Evt<{
|
||||||
worker: BackgroundWorker;
|
worker: BackgroundWorker;
|
||||||
id: string;
|
id: string;
|
||||||
record: CreateBackgroundWorkerResponse;
|
record: CreateBackgroundWorkerResponse;
|
||||||
}> = new Evt();
|
}> = new Evt();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use onWorkerTaskRunHeartbeat instead
|
||||||
|
*/
|
||||||
public onWorkerTaskHeartbeat: Evt<{
|
public onWorkerTaskHeartbeat: Evt<{
|
||||||
id: string;
|
id: string;
|
||||||
backgroundWorkerId: string;
|
backgroundWorkerId: string;
|
||||||
worker: BackgroundWorker;
|
worker: BackgroundWorker;
|
||||||
}> = new Evt();
|
}> = new Evt();
|
||||||
|
public onWorkerTaskRunHeartbeat: Evt<{
|
||||||
|
id: string;
|
||||||
|
backgroundWorkerId: string;
|
||||||
|
worker: BackgroundWorker;
|
||||||
|
}> = new Evt();
|
||||||
public onWorkerDeprecated: Evt<{ worker: BackgroundWorker; id: string }> = new Evt();
|
public onWorkerDeprecated: Evt<{ worker: BackgroundWorker; id: string }> = new Evt();
|
||||||
private _backgroundWorkers: Map<string, BackgroundWorker> = new Map();
|
private _backgroundWorkers: Map<string, BackgroundWorker> = new Map();
|
||||||
private _records: Map<string, CreateBackgroundWorkerResponse> = new Map();
|
private _records: Map<string, CreateBackgroundWorkerResponse> = new Map();
|
||||||
private _deprecatedWorkers: Set<string> = new Set();
|
private _deprecatedWorkers: Set<string> = new Set();
|
||||||
|
|
||||||
constructor(private baseURL: string) {
|
constructor(private baseURL: string) {
|
||||||
this.onTaskCompleted.attach(async ({ completion, execution }) => {
|
this.onTaskCompleted.attach(async ({ completion }) => {
|
||||||
if (!completion.ok && typeof completion.retry !== "undefined") {
|
if (!completion.ok && typeof completion.retry !== "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.#notifyWorkersOfTaskCompletion(completion, execution);
|
await this.#notifyWorkersOfTaskCompletion(completion);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.onTaskFailedToRun.attach(async ({ completion }) => {
|
||||||
|
await this.#notifyWorkersOfTaskCompletion(completion);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async #notifyWorkersOfTaskCompletion(
|
async #notifyWorkersOfTaskCompletion(completion: TaskRunExecutionResult) {
|
||||||
completion: TaskRunExecutionResult,
|
|
||||||
execution: TaskRunExecution
|
|
||||||
) {
|
|
||||||
for (const worker of this._backgroundWorkers.values()) {
|
for (const worker of this._backgroundWorkers.values()) {
|
||||||
await worker.taskRunCompletedNotification(completion, execution);
|
await worker.taskRunCompletedNotification(completion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +131,10 @@ export class BackgroundWorkerCoordinator {
|
|||||||
worker.onTaskHeartbeat.attach((id) => {
|
worker.onTaskHeartbeat.attach((id) => {
|
||||||
this.onWorkerTaskHeartbeat.post({ id, backgroundWorkerId: record.id, worker });
|
this.onWorkerTaskHeartbeat.post({ id, backgroundWorkerId: record.id, worker });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
worker.onTaskRunHeartbeat.attach((id) => {
|
||||||
|
this.onWorkerTaskRunHeartbeat.post({ id, backgroundWorkerId: record.id, worker });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
@@ -136,7 +164,65 @@ export class BackgroundWorkerCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await worker.cancelRun(message.taskRunId);
|
await worker.cancelRun(message.taskRunId);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
case "EXECUTE_RUN_LAZY_ATTEMPT": {
|
||||||
|
await this.#executeTaskRunLazyAttempt(id, message.payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #executeTaskRunLazyAttempt(id: string, payload: TaskRunExecutionLazyAttemptPayload) {
|
||||||
|
const worker = this._backgroundWorkers.get(id);
|
||||||
|
|
||||||
|
if (!worker) {
|
||||||
|
logger.error(`Could not find worker ${id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const record = this._records.get(id);
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
logger.error(`Could not find worker record ${id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { completion, execution } = await worker.executeTaskRunLazyAttempt(
|
||||||
|
payload,
|
||||||
|
this.baseURL
|
||||||
|
);
|
||||||
|
|
||||||
|
this.onTaskCompleted.post({
|
||||||
|
completion,
|
||||||
|
execution,
|
||||||
|
worker,
|
||||||
|
backgroundWorkerId: id,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.onTaskFailedToRun.post({
|
||||||
|
backgroundWorkerId: id,
|
||||||
|
worker,
|
||||||
|
completion: {
|
||||||
|
ok: false,
|
||||||
|
id: payload.runId,
|
||||||
|
retry: undefined,
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? {
|
||||||
|
type: "BUILT_IN_ERROR",
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stackTrace: error.stack ?? "",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: "BUILT_IN_ERROR",
|
||||||
|
name: "UnknownError",
|
||||||
|
message: String(error),
|
||||||
|
stackTrace: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,106 +241,14 @@ export class BackgroundWorkerCoordinator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { execution } = payload;
|
const completion = await worker.executeTaskRun(payload, this.baseURL);
|
||||||
|
|
||||||
// ○ Mar 27 09:17:25.653 -> View logs | 20240326.20 | create-avatar | run_slufhjdfiv8ejnrkw9dsj.1
|
this.onTaskCompleted.post({
|
||||||
|
completion,
|
||||||
const logsUrl = `${this.baseURL}/runs/${execution.run.id}`;
|
execution: payload.execution,
|
||||||
|
worker,
|
||||||
const pipe = chalkGrey("|");
|
backgroundWorkerId: id,
|
||||||
const bullet = chalkGrey("○");
|
});
|
||||||
const link = chalkLink(terminalLink("View logs", logsUrl));
|
|
||||||
let timestampPrefix = chalkGrey(prettyPrintDate(payload.execution.attempt.startedAt));
|
|
||||||
const workerPrefix = chalkWorker(record.version);
|
|
||||||
const taskPrefix = chalkTask(execution.task.id);
|
|
||||||
const runId = chalkRun(`${execution.run.id}.${execution.attempt.number}`);
|
|
||||||
|
|
||||||
logger.log(
|
|
||||||
`${bullet} ${timestampPrefix} ${chalkGrey(
|
|
||||||
"->"
|
|
||||||
)} ${link} ${pipe} ${workerPrefix} ${pipe} ${taskPrefix} ${pipe} ${runId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const now = performance.now();
|
|
||||||
|
|
||||||
const completion = await worker.executeTaskRun(payload);
|
|
||||||
|
|
||||||
const elapsed = performance.now() - now;
|
|
||||||
|
|
||||||
const retryingText = chalkGrey(
|
|
||||||
!completion.ok && completion.skippedRetrying
|
|
||||||
? " (retrying skipped)"
|
|
||||||
: !completion.ok && completion.retry !== undefined
|
|
||||||
? ` (retrying in ${completion.retry.delay}ms)`
|
|
||||||
: ""
|
|
||||||
);
|
|
||||||
|
|
||||||
const resultText = !completion.ok
|
|
||||||
? completion.error.type === "INTERNAL_ERROR" &&
|
|
||||||
(completion.error.code === TaskRunErrorCodes.TASK_EXECUTION_ABORTED ||
|
|
||||||
completion.error.code === TaskRunErrorCodes.TASK_RUN_CANCELLED)
|
|
||||||
? chalkWarning("Cancelled")
|
|
||||||
: `${chalkError("Error")}${retryingText}`
|
|
||||||
: chalkSuccess("Success");
|
|
||||||
|
|
||||||
const errorText = !completion.ok
|
|
||||||
? this.#formatErrorLog(completion.error)
|
|
||||||
: "retry" in completion
|
|
||||||
? `retry in ${completion.retry}ms`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const elapsedText = chalkGrey(`(${formatDurationMilliseconds(elapsed, { style: "short" })})`);
|
|
||||||
|
|
||||||
timestampPrefix = chalkGrey(prettyPrintDate());
|
|
||||||
|
|
||||||
logger.log(
|
|
||||||
`${bullet} ${timestampPrefix} ${chalkGrey(
|
|
||||||
"->"
|
|
||||||
)} ${link} ${pipe} ${workerPrefix} ${pipe} ${taskPrefix} ${pipe} ${runId} ${pipe} ${resultText} ${elapsedText}${errorText}`
|
|
||||||
);
|
|
||||||
|
|
||||||
this.onTaskCompleted.post({ completion, execution, worker, backgroundWorkerId: id });
|
|
||||||
}
|
|
||||||
|
|
||||||
#formatErrorLog(error: TaskRunError) {
|
|
||||||
switch (error.type) {
|
|
||||||
case "INTERNAL_ERROR": {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
case "STRING_ERROR": {
|
|
||||||
return `\n\n${chalkError("X Error:")} ${error.raw}\n`;
|
|
||||||
}
|
|
||||||
case "CUSTOM_ERROR": {
|
|
||||||
return `\n\n${chalkError("X Error:")} ${error.raw}\n`;
|
|
||||||
}
|
|
||||||
case "BUILT_IN_ERROR": {
|
|
||||||
return `\n\n${error.stackTrace.replace(/^Error: /, chalkError("X Error: "))}\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnexpectedExitError extends Error {
|
|
||||||
constructor(public code: number) {
|
|
||||||
super(`Unexpected exit with code ${code}`);
|
|
||||||
|
|
||||||
this.name = "UnexpectedExitError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CleanupProcessError extends Error {
|
|
||||||
constructor() {
|
|
||||||
super("Cancelled");
|
|
||||||
|
|
||||||
this.name = "CleanupProcessError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CancelledProcessError extends Error {
|
|
||||||
constructor() {
|
|
||||||
super("Cancelled");
|
|
||||||
|
|
||||||
this.name = "CancelledProcessError";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,13 +270,18 @@ export class BackgroundWorker {
|
|||||||
schema: childToWorkerMessages,
|
schema: childToWorkerMessages,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use onTaskRunHeartbeat instead
|
||||||
|
*/
|
||||||
public onTaskHeartbeat: Evt<string> = new Evt();
|
public onTaskHeartbeat: Evt<string> = new Evt();
|
||||||
|
public onTaskRunHeartbeat: Evt<string> = new Evt();
|
||||||
private _onClose: Evt<void> = new Evt();
|
private _onClose: Evt<void> = new Evt();
|
||||||
|
|
||||||
public tasks: Array<TaskMetadataWithFilePath> = [];
|
public tasks: Array<TaskMetadataWithFilePath> = [];
|
||||||
public metadata: BackgroundWorkerProperties | undefined;
|
public metadata: BackgroundWorkerProperties | undefined;
|
||||||
|
|
||||||
_taskRunProcesses: Map<string, TaskRunProcess> = new Map();
|
_taskRunProcesses: Map<string, TaskRunProcess> = new Map();
|
||||||
|
private _taskRunProcessesBeingKilled: Set<number> = new Set();
|
||||||
|
|
||||||
private _closed: boolean = false;
|
private _closed: boolean = false;
|
||||||
|
|
||||||
@@ -290,7 +289,8 @@ export class BackgroundWorker {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public path: string,
|
public path: string,
|
||||||
public params: BackgroundWorkerParams
|
public params: BackgroundWorkerParams,
|
||||||
|
private apiClient: CliApiClient
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
@@ -301,6 +301,7 @@ export class BackgroundWorker {
|
|||||||
this._closed = true;
|
this._closed = true;
|
||||||
|
|
||||||
this.onTaskHeartbeat.detach();
|
this.onTaskHeartbeat.detach();
|
||||||
|
this.onTaskRunHeartbeat.detach();
|
||||||
|
|
||||||
// We need to close all the task run processes
|
// We need to close all the task run processes
|
||||||
for (const taskRunProcess of this._taskRunProcesses.values()) {
|
for (const taskRunProcess of this._taskRunProcesses.values()) {
|
||||||
@@ -314,6 +315,10 @@ export class BackgroundWorker {
|
|||||||
safeDeleteFileSync(`${this.path}.map`);
|
safeDeleteFileSync(`${this.path}.map`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get inProgressRuns(): Array<string> {
|
||||||
|
return Array.from(this._taskRunProcesses.keys());
|
||||||
|
}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
if (this._initialized) {
|
if (this._initialized) {
|
||||||
throw new Error("Worker already initialized");
|
throw new Error("Worker already initialized");
|
||||||
@@ -408,46 +413,142 @@ export class BackgroundWorker {
|
|||||||
|
|
||||||
// We need to notify all the task run processes that a task run has completed,
|
// We need to notify all the task run processes that a task run has completed,
|
||||||
// in case they are waiting for it through triggerAndWait
|
// in case they are waiting for it through triggerAndWait
|
||||||
async taskRunCompletedNotification(
|
async taskRunCompletedNotification(completion: TaskRunExecutionResult) {
|
||||||
completion: TaskRunExecutionResult,
|
|
||||||
execution: TaskRunExecution
|
|
||||||
) {
|
|
||||||
for (const taskRunProcess of this._taskRunProcesses.values()) {
|
for (const taskRunProcess of this._taskRunProcesses.values()) {
|
||||||
taskRunProcess.taskRunCompletedNotification(completion, execution);
|
taskRunProcess.taskRunCompletedNotification(completion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #initializeTaskRunProcess(payload: TaskRunExecutionPayload): Promise<TaskRunProcess> {
|
#prefixedMessage(payload: TaskRunExecutionPayload, message: string = "") {
|
||||||
|
return `[${payload.execution.run.id}.${payload.execution.attempt.number}] ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #getFreshTaskRunProcess(
|
||||||
|
payload: TaskRunExecutionPayload,
|
||||||
|
messageId?: string
|
||||||
|
): Promise<TaskRunProcess> {
|
||||||
|
logger.debug(this.#prefixedMessage(payload, "getFreshTaskRunProcess()"));
|
||||||
|
|
||||||
if (!this.metadata) {
|
if (!this.metadata) {
|
||||||
throw new Error("Worker not registered");
|
throw new Error("Worker not registered");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._taskRunProcesses.has(payload.execution.run.id)) {
|
this._closed = false;
|
||||||
const taskRunProcess = new TaskRunProcess(
|
|
||||||
payload.execution,
|
|
||||||
this.path,
|
|
||||||
{
|
|
||||||
...this._fullEnv,
|
|
||||||
...(payload.environment ?? {}),
|
|
||||||
},
|
|
||||||
this.metadata,
|
|
||||||
this.params
|
|
||||||
);
|
|
||||||
|
|
||||||
taskRunProcess.onExit.attach(() => {
|
logger.debug(this.#prefixedMessage(payload, "killing current task run process before attempt"));
|
||||||
|
|
||||||
|
await this.#killCurrentTaskRunProcessBeforeAttempt(payload.execution.run.id);
|
||||||
|
|
||||||
|
logger.debug(this.#prefixedMessage(payload, "creating new task run process"));
|
||||||
|
|
||||||
|
const taskRunProcess = new TaskRunProcess(
|
||||||
|
payload.execution.run.id,
|
||||||
|
payload.execution.run.isTest,
|
||||||
|
this.path,
|
||||||
|
{
|
||||||
|
...this._fullEnv,
|
||||||
|
...(payload.environment ?? {}),
|
||||||
|
...this.#readEnvVars(),
|
||||||
|
},
|
||||||
|
this.metadata,
|
||||||
|
this.params,
|
||||||
|
messageId
|
||||||
|
);
|
||||||
|
|
||||||
|
taskRunProcess.onExit.attach(({ pid }) => {
|
||||||
|
logger.debug(this.#prefixedMessage(payload, "onExit()"), { pid });
|
||||||
|
|
||||||
|
const taskRunProcess = this._taskRunProcesses.get(payload.execution.run.id);
|
||||||
|
|
||||||
|
// Only delete the task run process if the pid matches
|
||||||
|
if (taskRunProcess?.pid === pid) {
|
||||||
this._taskRunProcesses.delete(payload.execution.run.id);
|
this._taskRunProcesses.delete(payload.execution.run.id);
|
||||||
});
|
}
|
||||||
|
|
||||||
taskRunProcess.onTaskHeartbeat.attach((id) => {
|
if (pid) {
|
||||||
this.onTaskHeartbeat.post(id);
|
this._taskRunProcessesBeingKilled.delete(pid);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await taskRunProcess.initialize();
|
taskRunProcess.onIsBeingKilled.attach((pid) => {
|
||||||
|
if (pid) {
|
||||||
|
this._taskRunProcessesBeingKilled.add(pid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this._taskRunProcesses.set(payload.execution.run.id, taskRunProcess);
|
taskRunProcess.onTaskHeartbeat.attach((id) => {
|
||||||
|
this.onTaskHeartbeat.post(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
taskRunProcess.onTaskRunHeartbeat.attach((id) => {
|
||||||
|
this.onTaskRunHeartbeat.post(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
await taskRunProcess.initialize();
|
||||||
|
|
||||||
|
this._taskRunProcesses.set(payload.execution.run.id, taskRunProcess);
|
||||||
|
|
||||||
|
return taskRunProcess;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #killCurrentTaskRunProcessBeforeAttempt(runId: string) {
|
||||||
|
const taskRunProcess = this._taskRunProcesses.get(runId);
|
||||||
|
|
||||||
|
if (!taskRunProcess) {
|
||||||
|
logger.debug(`[${runId}] no current task process to kill`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._taskRunProcesses.get(payload.execution.run.id) as TaskRunProcess;
|
logger.debug(`[${runId}] killing current task process`, {
|
||||||
|
pid: taskRunProcess.pid,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (taskRunProcess.isBeingKilled) {
|
||||||
|
if (this._taskRunProcessesBeingKilled.size > 1) {
|
||||||
|
await this.#tryGracefulExit(taskRunProcess);
|
||||||
|
} else {
|
||||||
|
// If there's only one or none being killed, don't do anything so we can create a fresh one in parallel
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's not being killed, so kill it
|
||||||
|
if (this._taskRunProcessesBeingKilled.size > 0) {
|
||||||
|
await this.#tryGracefulExit(taskRunProcess);
|
||||||
|
} else {
|
||||||
|
// There's none being killed yet, so we can kill it without waiting. We still set a timeout to kill it forcefully just in case it sticks around.
|
||||||
|
taskRunProcess.kill("SIGTERM", 5_000).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #tryGracefulExit(
|
||||||
|
taskRunProcess: TaskRunProcess,
|
||||||
|
kill = false,
|
||||||
|
initialSignal: number | NodeJS.Signals = "SIGTERM"
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const initialExit = taskRunProcess.onExit.waitFor(5_000);
|
||||||
|
|
||||||
|
if (kill) {
|
||||||
|
taskRunProcess.kill(initialSignal);
|
||||||
|
}
|
||||||
|
|
||||||
|
await initialExit;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("TaskRunProcess graceful kill timeout exceeded", error);
|
||||||
|
|
||||||
|
this.#tryForcefulExit(taskRunProcess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #tryForcefulExit(taskRunProcess: TaskRunProcess) {
|
||||||
|
try {
|
||||||
|
const forcedKill = taskRunProcess.onExit.waitFor(5_000);
|
||||||
|
taskRunProcess.kill("SIGKILL");
|
||||||
|
await forcedKill;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("TaskRunProcess forced kill timeout exceeded", error);
|
||||||
|
throw new SigKillTimeoutProcessError();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelRun(taskRunId: string) {
|
async cancelRun(taskRunId: string) {
|
||||||
@@ -460,14 +561,113 @@ export class BackgroundWorker {
|
|||||||
await taskRunProcess.cancel();
|
await taskRunProcess.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async executeTaskRunLazyAttempt(payload: TaskRunExecutionLazyAttemptPayload, baseURL: string) {
|
||||||
|
const attemptResponse = await this.apiClient.createTaskRunAttempt(payload.runId);
|
||||||
|
|
||||||
|
if (!attemptResponse.success) {
|
||||||
|
throw new Error(`Failed to create task run attempt: ${attemptResponse.error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const execution = attemptResponse.data;
|
||||||
|
|
||||||
|
const completion = await this.executeTaskRun(
|
||||||
|
{ execution, traceContext: payload.traceContext, environment: payload.environment },
|
||||||
|
baseURL,
|
||||||
|
payload.messageId
|
||||||
|
);
|
||||||
|
|
||||||
|
return { execution, completion };
|
||||||
|
}
|
||||||
|
|
||||||
// We need to fork the process before we can execute any tasks
|
// We need to fork the process before we can execute any tasks
|
||||||
async executeTaskRun(payload: TaskRunExecutionPayload): Promise<TaskRunExecutionResult> {
|
async executeTaskRun(
|
||||||
|
payload: TaskRunExecutionPayload,
|
||||||
|
baseURL: string,
|
||||||
|
messageId?: string
|
||||||
|
): Promise<TaskRunExecutionResult> {
|
||||||
|
if (this._closed) {
|
||||||
|
throw new Error("Worker is closed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.metadata) {
|
||||||
|
throw new Error("Worker not registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { execution } = payload;
|
||||||
|
// ○ Mar 27 09:17:25.653 -> View logs | 20240326.20 | create-avatar | run_slufhjdfiv8ejnrkw9dsj.1
|
||||||
|
|
||||||
|
const logsUrl = `${baseURL}/runs/${execution.run.id}`;
|
||||||
|
|
||||||
|
const pipe = chalkGrey("|");
|
||||||
|
const bullet = chalkGrey("○");
|
||||||
|
const link = chalkLink(terminalLink("View logs", logsUrl));
|
||||||
|
let timestampPrefix = chalkGrey(prettyPrintDate(payload.execution.attempt.startedAt));
|
||||||
|
const workerPrefix = chalkWorker(this.metadata.version);
|
||||||
|
const taskPrefix = chalkTask(execution.task.id);
|
||||||
|
const runId = chalkRun(`${execution.run.id}.${execution.attempt.number}`);
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
`${bullet} ${timestampPrefix} ${chalkGrey(
|
||||||
|
"->"
|
||||||
|
)} ${link} ${pipe} ${workerPrefix} ${pipe} ${taskPrefix} ${pipe} ${runId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const now = performance.now();
|
||||||
|
|
||||||
|
const completion = await this.#doExecuteTaskRun(payload, messageId);
|
||||||
|
|
||||||
|
const elapsed = performance.now() - now;
|
||||||
|
|
||||||
|
const retryingText = chalkGrey(
|
||||||
|
!completion.ok && completion.skippedRetrying
|
||||||
|
? " (retrying skipped)"
|
||||||
|
: !completion.ok && completion.retry !== undefined
|
||||||
|
? ` (retrying in ${completion.retry.delay}ms)`
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const resultText = !completion.ok
|
||||||
|
? completion.error.type === "INTERNAL_ERROR" &&
|
||||||
|
(completion.error.code === TaskRunErrorCodes.TASK_EXECUTION_ABORTED ||
|
||||||
|
completion.error.code === TaskRunErrorCodes.TASK_RUN_CANCELLED)
|
||||||
|
? chalkWarning("Cancelled")
|
||||||
|
: `${chalkError("Error")}${retryingText}`
|
||||||
|
: chalkSuccess("Success");
|
||||||
|
|
||||||
|
const errorText = !completion.ok
|
||||||
|
? formatErrorLog(completion.error)
|
||||||
|
: "retry" in completion
|
||||||
|
? `retry in ${completion.retry}ms`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const elapsedText = chalkGrey(`(${formatDurationMilliseconds(elapsed, { style: "short" })})`);
|
||||||
|
|
||||||
|
timestampPrefix = chalkGrey(prettyPrintDate());
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
`${bullet} ${timestampPrefix} ${chalkGrey(
|
||||||
|
"->"
|
||||||
|
)} ${link} ${pipe} ${workerPrefix} ${pipe} ${taskPrefix} ${pipe} ${runId} ${pipe} ${resultText} ${elapsedText}${errorText}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return completion;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #doExecuteTaskRun(
|
||||||
|
payload: TaskRunExecutionPayload,
|
||||||
|
messageId?: string
|
||||||
|
): Promise<TaskRunExecutionResult> {
|
||||||
try {
|
try {
|
||||||
const taskRunProcess = await this.#initializeTaskRunProcess(payload);
|
const taskRunProcess = await this.#getFreshTaskRunProcess(payload, messageId);
|
||||||
|
|
||||||
|
logger.debug(this.#prefixedMessage(payload, "executing task run"), {
|
||||||
|
pid: taskRunProcess.pid,
|
||||||
|
});
|
||||||
|
|
||||||
const result = await taskRunProcess.executeTaskRun(payload);
|
const result = await taskRunProcess.executeTaskRun(payload);
|
||||||
|
|
||||||
// Kill the worker if the task was successful or if it's not going to be retried);
|
// Always kill the worker
|
||||||
await taskRunProcess.cleanup(result.ok || result.retry === undefined);
|
await taskRunProcess.cleanup(true);
|
||||||
|
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
return result;
|
return result;
|
||||||
@@ -568,6 +768,7 @@ class TaskRunProcess {
|
|||||||
});
|
});
|
||||||
private _sender: ZodMessageSender<typeof workerToChildMessages>;
|
private _sender: ZodMessageSender<typeof workerToChildMessages>;
|
||||||
private _child: ChildProcess | undefined;
|
private _child: ChildProcess | undefined;
|
||||||
|
private _childPid?: number;
|
||||||
private _attemptPromises: Map<
|
private _attemptPromises: Map<
|
||||||
string,
|
string,
|
||||||
{ resolver: (value: TaskRunExecutionResult) => void; rejecter: (err?: any) => void }
|
{ resolver: (value: TaskRunExecutionResult) => void; rejecter: (err?: any) => void }
|
||||||
@@ -576,15 +777,23 @@ class TaskRunProcess {
|
|||||||
private _currentExecution: TaskRunExecution | undefined;
|
private _currentExecution: TaskRunExecution | undefined;
|
||||||
private _isBeingKilled: boolean = false;
|
private _isBeingKilled: boolean = false;
|
||||||
private _isBeingCancelled: boolean = false;
|
private _isBeingCancelled: boolean = false;
|
||||||
|
/**
|
||||||
|
* @deprecated use onTaskRunHeartbeat instead
|
||||||
|
*/
|
||||||
public onTaskHeartbeat: Evt<string> = new Evt();
|
public onTaskHeartbeat: Evt<string> = new Evt();
|
||||||
public onExit: Evt<number> = new Evt();
|
public onTaskRunHeartbeat: Evt<string> = new Evt();
|
||||||
|
public onExit: Evt<{ code: number | null; signal: NodeJS.Signals | null; pid?: number }> =
|
||||||
|
new Evt();
|
||||||
|
public onIsBeingKilled: Evt<number | undefined> = new Evt();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private execution: TaskRunExecution,
|
private runId: string,
|
||||||
|
private isTest: boolean,
|
||||||
private path: string,
|
private path: string,
|
||||||
private env: NodeJS.ProcessEnv,
|
private env: NodeJS.ProcessEnv,
|
||||||
private metadata: BackgroundWorkerProperties,
|
private metadata: BackgroundWorkerProperties,
|
||||||
private worker: BackgroundWorkerParams
|
private worker: BackgroundWorkerParams,
|
||||||
|
private messageId?: string
|
||||||
) {
|
) {
|
||||||
this._sender = new ZodMessageSender({
|
this._sender = new ZodMessageSender({
|
||||||
schema: workerToChildMessages,
|
schema: workerToChildMessages,
|
||||||
@@ -604,7 +813,7 @@ class TaskRunProcess {
|
|||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
const fullEnv = {
|
const fullEnv = {
|
||||||
...(this.execution.run.isTest ? { TRIGGER_LOG_LEVEL: "debug" } : {}),
|
...(this.isTest ? { TRIGGER_LOG_LEVEL: "debug" } : {}),
|
||||||
...this.env,
|
...this.env,
|
||||||
OTEL_RESOURCE_ATTRIBUTES: JSON.stringify({
|
OTEL_RESOURCE_ATTRIBUTES: JSON.stringify({
|
||||||
[SemanticInternalAttributes.PROJECT_DIR]: this.worker.projectConfig.projectDir,
|
[SemanticInternalAttributes.PROJECT_DIR]: this.worker.projectConfig.projectDir,
|
||||||
@@ -615,7 +824,7 @@ class TaskRunProcess {
|
|||||||
|
|
||||||
const cwd = dirname(this.path);
|
const cwd = dirname(this.path);
|
||||||
|
|
||||||
logger.debug(`[${this.execution.run.id}] initializing task run process`, {
|
logger.debug(`[${this.runId}] initializing task run process`, {
|
||||||
env: fullEnv,
|
env: fullEnv,
|
||||||
path: this.path,
|
path: this.path,
|
||||||
cwd,
|
cwd,
|
||||||
@@ -629,6 +838,7 @@ class TaskRunProcess {
|
|||||||
? ["--inspect-brk", "--trace-uncaught", "--no-warnings=ExperimentalWarning"]
|
? ["--inspect-brk", "--trace-uncaught", "--no-warnings=ExperimentalWarning"]
|
||||||
: ["--trace-uncaught", "--no-warnings=ExperimentalWarning"],
|
: ["--trace-uncaught", "--no-warnings=ExperimentalWarning"],
|
||||||
});
|
});
|
||||||
|
this._childPid = this._child?.pid;
|
||||||
|
|
||||||
this._child.on("message", this.#handleMessage.bind(this));
|
this._child.on("message", this.#handleMessage.bind(this));
|
||||||
this._child.on("exit", this.#handleExit.bind(this));
|
this._child.on("exit", this.#handleExit.bind(this));
|
||||||
@@ -641,19 +851,28 @@ class TaskRunProcess {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`[${this.execution.run.id}] cleaning up task run process`, { kill });
|
if (kill) {
|
||||||
|
this._isBeingKilled = true;
|
||||||
|
this.onIsBeingKilled.post(this._child?.pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`[${this.runId}] cleaning up task run process`, { kill, pid: this.pid });
|
||||||
|
|
||||||
await this._sender.send("CLEANUP", {
|
await this._sender.send("CLEANUP", {
|
||||||
flush: true,
|
flush: true,
|
||||||
kill,
|
kill,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._isBeingKilled = kill;
|
// FIXME: Something broke READY_TO_DISPOSE. We never receive it, so we always have to kill the process after the timeout below.
|
||||||
|
|
||||||
|
if (!kill) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Set a timeout to kill the child process if it hasn't been killed within 5 seconds
|
// Set a timeout to kill the child process if it hasn't been killed within 5 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this._child && !this._child.killed) {
|
if (this._child && !this._child.killed) {
|
||||||
logger.debug(`[${this.execution.run.id}] killing task run process after timeout`);
|
logger.debug(`[${this.runId}] killing task run process after timeout`, { pid: this.pid });
|
||||||
|
|
||||||
this._child.kill();
|
this._child.kill();
|
||||||
}
|
}
|
||||||
@@ -691,24 +910,23 @@ class TaskRunProcess {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
taskRunCompletedNotification(completion: TaskRunExecutionResult, execution: TaskRunExecution) {
|
taskRunCompletedNotification(completion: TaskRunExecutionResult) {
|
||||||
if (!completion.ok && typeof completion.retry !== "undefined") {
|
if (!completion.ok && typeof completion.retry !== "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (execution.run.id === this.execution.run.id) {
|
if (completion.id === this.runId) {
|
||||||
// We don't need to notify the task run process if it's the same as the one we're running
|
// We don't need to notify the task run process if it's the same as the one we're running
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`[${this.execution.run.id}] task run completed notification`, {
|
logger.debug(`[${this.runId}] task run completed notification`, {
|
||||||
completion,
|
completion,
|
||||||
execution,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this._sender.send("TASK_RUN_COMPLETED_NOTIFICATION", {
|
this._sender.send("TASK_RUN_COMPLETED_NOTIFICATION", {
|
||||||
|
version: "v2",
|
||||||
completion,
|
completion,
|
||||||
execution,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -740,14 +958,18 @@ class TaskRunProcess {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "READY_TO_DISPOSE": {
|
case "READY_TO_DISPOSE": {
|
||||||
logger.debug(`[${this.execution.run.id}] task run process is ready to dispose`);
|
logger.debug(`[${this.runId}] task run process is ready to dispose`);
|
||||||
|
|
||||||
this.#kill();
|
this.#kill();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "TASK_HEARTBEAT": {
|
case "TASK_HEARTBEAT": {
|
||||||
this.onTaskHeartbeat.post(message.payload.id);
|
if (this.messageId) {
|
||||||
|
this.onTaskRunHeartbeat.post(this.messageId);
|
||||||
|
} else {
|
||||||
|
this.onTaskHeartbeat.post(message.payload.id);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -757,8 +979,8 @@ class TaskRunProcess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #handleExit(code: number) {
|
async #handleExit(code: number | null, signal: NodeJS.Signals | null) {
|
||||||
logger.debug(`[${this.execution.run.id}] task run process exiting`, { code });
|
logger.debug(`[${this.runId}] handle task run process exit`, { code, signal, pid: this.pid });
|
||||||
|
|
||||||
// Go through all the attempts currently pending and reject them
|
// Go through all the attempts currently pending and reject them
|
||||||
for (const [id, status] of this._attemptStatuses.entries()) {
|
for (const [id, status] of this._attemptStatuses.entries()) {
|
||||||
@@ -778,12 +1000,12 @@ class TaskRunProcess {
|
|||||||
} else if (this._isBeingKilled) {
|
} else if (this._isBeingKilled) {
|
||||||
rejecter(new CleanupProcessError());
|
rejecter(new CleanupProcessError());
|
||||||
} else {
|
} else {
|
||||||
rejecter(new UnexpectedExitError(code));
|
rejecter(new UnexpectedExitError(code ?? -1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onExit.post(code);
|
this.onExit.post({ code, signal, pid: this.pid });
|
||||||
}
|
}
|
||||||
|
|
||||||
#handleLog(data: Buffer) {
|
#handleLog(data: Buffer) {
|
||||||
@@ -823,10 +1045,54 @@ class TaskRunProcess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#kill() {
|
#kill() {
|
||||||
if (this._child && !this._child.killed) {
|
logger.debug(`[${this.runId}] #kill()`, { pid: this.pid });
|
||||||
logger.debug(`[${this.execution.run.id}] killing task run process`);
|
|
||||||
|
|
||||||
|
if (this._child && !this._child.killed) {
|
||||||
this._child?.kill();
|
this._child?.kill();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async kill(signal?: number | NodeJS.Signals, timeoutInMs?: number) {
|
||||||
|
logger.debug(`[${this.runId}] killing task run process`, {
|
||||||
|
signal,
|
||||||
|
timeoutInMs,
|
||||||
|
pid: this.pid,
|
||||||
|
});
|
||||||
|
|
||||||
|
this._isBeingKilled = true;
|
||||||
|
|
||||||
|
const killTimeout = this.onExit.waitFor(timeoutInMs);
|
||||||
|
|
||||||
|
this.onIsBeingKilled.post(this._child?.pid);
|
||||||
|
this._child?.kill(signal);
|
||||||
|
|
||||||
|
if (timeoutInMs) {
|
||||||
|
await killTimeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isBeingKilled() {
|
||||||
|
return this._isBeingKilled || this._child?.killed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get pid() {
|
||||||
|
return this._childPid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatErrorLog(error: TaskRunError) {
|
||||||
|
switch (error.type) {
|
||||||
|
case "INTERNAL_ERROR": {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
case "STRING_ERROR": {
|
||||||
|
return `\n\n${chalkError("X Error:")} ${error.raw}\n`;
|
||||||
|
}
|
||||||
|
case "CUSTOM_ERROR": {
|
||||||
|
return `\n\n${chalkError("X Error:")} ${error.raw}\n`;
|
||||||
|
}
|
||||||
|
case "BUILT_IN_ERROR": {
|
||||||
|
return `\n\n${error.stackTrace.replace(/^Error: /, chalkError("X Error: "))}\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,8 +182,17 @@ const handler = new ZodMessageHandler({
|
|||||||
_isRunning = false;
|
_isRunning = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TASK_RUN_COMPLETED_NOTIFICATION: async ({ completion, execution }) => {
|
TASK_RUN_COMPLETED_NOTIFICATION: async (payload) => {
|
||||||
devRuntimeManager.resumeTask(completion, execution);
|
switch (payload.version) {
|
||||||
|
case "v1": {
|
||||||
|
devRuntimeManager.resumeTask(payload.completion, payload.execution.run.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "v2": {
|
||||||
|
devRuntimeManager.resumeTask(payload.completion, payload.completion.id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
CLEANUP: async ({ flush, kill }) => {
|
CLEANUP: async ({ flush, kill }) => {
|
||||||
if (kill) {
|
if (kill) {
|
||||||
@@ -215,7 +224,7 @@ sender.send("TASKS_READY", { tasks: TASK_METADATA }).catch((err) => {
|
|||||||
|
|
||||||
process.title = "trigger-dev-worker";
|
process.title = "trigger-dev-worker";
|
||||||
|
|
||||||
async function asyncHeartbeat(initialDelayInSeconds: number = 30, intervalInSeconds: number = 5) {
|
async function asyncHeartbeat(initialDelayInSeconds: number = 30, intervalInSeconds: number = 30) {
|
||||||
async function _doHeartbeat() {
|
async function _doHeartbeat() {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (_isRunning && _execution) {
|
if (_isRunning && _execution) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
TaskRunBuiltInError,
|
TaskRunBuiltInError,
|
||||||
TaskRunErrorCodes,
|
TaskRunErrorCodes,
|
||||||
TaskRunExecution,
|
TaskRunExecution,
|
||||||
|
TaskRunExecutionLazyAttemptPayload,
|
||||||
TaskRunExecutionPayload,
|
TaskRunExecutionPayload,
|
||||||
TaskRunExecutionResult,
|
TaskRunExecutionResult,
|
||||||
WaitReason,
|
WaitReason,
|
||||||
@@ -20,31 +21,15 @@ import { ZodIpcConnection } from "@trigger.dev/core/v3/zodIpc";
|
|||||||
import type { InferSocketMessageSchema } from "@trigger.dev/core/v3/zodSocket";
|
import type { InferSocketMessageSchema } from "@trigger.dev/core/v3/zodSocket";
|
||||||
import { Evt } from "evt";
|
import { Evt } from "evt";
|
||||||
import { ChildProcess, fork } from "node:child_process";
|
import { ChildProcess, fork } from "node:child_process";
|
||||||
import { TaskMetadataParseError, UncaughtExceptionError } from "../common/errors";
|
import {
|
||||||
|
CancelledProcessError,
|
||||||
class UnexpectedExitError extends Error {
|
CleanupProcessError,
|
||||||
constructor(public code: number) {
|
GracefulExitTimeoutError,
|
||||||
super(`Unexpected exit with code ${code}`);
|
SigKillTimeoutProcessError,
|
||||||
|
TaskMetadataParseError,
|
||||||
this.name = "UnexpectedExitError";
|
UncaughtExceptionError,
|
||||||
}
|
UnexpectedExitError,
|
||||||
}
|
} from "../common/errors";
|
||||||
|
|
||||||
class CleanupProcessError extends Error {
|
|
||||||
constructor() {
|
|
||||||
super("Cancelled");
|
|
||||||
|
|
||||||
this.name = "CleanupProcessError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CancelledProcessError extends Error {
|
|
||||||
constructor() {
|
|
||||||
super("Cancelled");
|
|
||||||
|
|
||||||
this.name = "CancelledProcessError";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type BackgroundWorkerParams = {
|
type BackgroundWorkerParams = {
|
||||||
env: Record<string, string>;
|
env: Record<string, string>;
|
||||||
@@ -56,7 +41,11 @@ type BackgroundWorkerParams = {
|
|||||||
export class ProdBackgroundWorker {
|
export class ProdBackgroundWorker {
|
||||||
private _initialized: boolean = false;
|
private _initialized: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use onTaskRunHeartbeat instead
|
||||||
|
*/
|
||||||
public onTaskHeartbeat: Evt<string> = new Evt();
|
public onTaskHeartbeat: Evt<string> = new Evt();
|
||||||
|
public onTaskRunHeartbeat: Evt<string> = new Evt();
|
||||||
|
|
||||||
public onWaitForBatch: Evt<
|
public onWaitForBatch: Evt<
|
||||||
InferSocketMessageSchema<typeof ProdChildToWorkerMessages, "WAIT_FOR_BATCH">
|
InferSocketMessageSchema<typeof ProdChildToWorkerMessages, "WAIT_FOR_BATCH">
|
||||||
@@ -74,11 +63,24 @@ export class ProdBackgroundWorker {
|
|||||||
public onReadyForCheckpoint = Evt.create<{ version?: "v1" }>();
|
public onReadyForCheckpoint = Evt.create<{ version?: "v1" }>();
|
||||||
public onCancelCheckpoint = Evt.create<{ version?: "v1" | "v2"; reason?: WaitReason }>();
|
public onCancelCheckpoint = Evt.create<{ version?: "v1" | "v2"; reason?: WaitReason }>();
|
||||||
|
|
||||||
|
public onCreateTaskRunAttempt = Evt.create<{ version?: "v1"; runId: string }>();
|
||||||
|
public attemptCreatedNotification = Evt.create<
|
||||||
|
| {
|
||||||
|
success: false;
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
execution: ProdTaskRunExecution;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
private _onClose: Evt<void> = new Evt();
|
private _onClose: Evt<void> = new Evt();
|
||||||
|
|
||||||
public tasks: Array<TaskMetadataWithFilePath> = [];
|
public tasks: Array<TaskMetadataWithFilePath> = [];
|
||||||
|
|
||||||
_taskRunProcess: TaskRunProcess | undefined;
|
_taskRunProcess: TaskRunProcess | undefined;
|
||||||
|
private _taskRunProcessesBeingKilled: Map<number, TaskRunProcess> = new Map();
|
||||||
|
|
||||||
private _closed: boolean = false;
|
private _closed: boolean = false;
|
||||||
|
|
||||||
@@ -87,7 +89,9 @@ export class ProdBackgroundWorker {
|
|||||||
private params: BackgroundWorkerParams
|
private params: BackgroundWorkerParams
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async close() {
|
async close(gracefulExitTimeoutElapsed = false) {
|
||||||
|
console.log("Closing worker", { gracefulExitTimeoutElapsed, closed: this._closed });
|
||||||
|
|
||||||
if (this._closed) {
|
if (this._closed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -95,9 +99,35 @@ export class ProdBackgroundWorker {
|
|||||||
this._closed = true;
|
this._closed = true;
|
||||||
|
|
||||||
this.onTaskHeartbeat.detach();
|
this.onTaskHeartbeat.detach();
|
||||||
|
this.onTaskRunHeartbeat.detach();
|
||||||
|
|
||||||
// We need to close the task run process
|
// We need to close the task run process
|
||||||
await this._taskRunProcess?.cleanup(true);
|
await this._taskRunProcess?.cleanup(true, gracefulExitTimeoutElapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #killTaskRunProcess(flush = true, initialSignal: number | NodeJS.Signals = "SIGTERM") {
|
||||||
|
console.log("Killing task run process", { flush, initialSignal, closed: this._closed });
|
||||||
|
|
||||||
|
if (this._closed || !this._taskRunProcess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flush) {
|
||||||
|
await this.flushTelemetry();
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTaskRunProcess = this._taskRunProcess;
|
||||||
|
|
||||||
|
// Try graceful exit but don't wait. We limit the amount of processes during creation instead.
|
||||||
|
this.#tryGracefulExit(currentTaskRunProcess, true, initialSignal).catch((error) => {
|
||||||
|
console.error("Error while trying graceful exit", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Killed task run process, setting closed to true", {
|
||||||
|
closed: this._closed,
|
||||||
|
pid: currentTaskRunProcess.pid,
|
||||||
|
});
|
||||||
|
this._closed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async flushTelemetry() {
|
async flushTelemetry() {
|
||||||
@@ -193,83 +223,191 @@ export class ProdBackgroundWorker {
|
|||||||
|
|
||||||
// We need to notify all the task run processes that a task run has completed,
|
// We need to notify all the task run processes that a task run has completed,
|
||||||
// in case they are waiting for it through triggerAndWait
|
// in case they are waiting for it through triggerAndWait
|
||||||
async taskRunCompletedNotification(
|
async taskRunCompletedNotification(completion: TaskRunExecutionResult) {
|
||||||
completion: TaskRunExecutionResult,
|
this._taskRunProcess?.taskRunCompletedNotification(completion);
|
||||||
execution: TaskRunExecution
|
|
||||||
) {
|
|
||||||
this._taskRunProcess?.taskRunCompletedNotification(completion, execution);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async waitCompletedNotification() {
|
async waitCompletedNotification() {
|
||||||
this._taskRunProcess?.waitCompletedNotification();
|
this._taskRunProcess?.waitCompletedNotification();
|
||||||
}
|
}
|
||||||
|
|
||||||
async #initializeTaskRunProcess(payload: ProdTaskRunExecutionPayload): Promise<TaskRunProcess> {
|
async #getFreshTaskRunProcess(
|
||||||
|
payload: ProdTaskRunExecutionPayload,
|
||||||
|
messageId?: string
|
||||||
|
): Promise<TaskRunProcess> {
|
||||||
const metadata = this.getMetadata(
|
const metadata = this.getMetadata(
|
||||||
payload.execution.worker.id,
|
payload.execution.worker.id,
|
||||||
payload.execution.worker.version
|
payload.execution.worker.version
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this._taskRunProcess) {
|
console.log("Getting fresh task run process, setting closed to false", {
|
||||||
const taskRunProcess = new TaskRunProcess(
|
closed: this._closed,
|
||||||
payload.execution,
|
});
|
||||||
this.path,
|
this._closed = false;
|
||||||
{
|
|
||||||
...this.params.env,
|
|
||||||
...(payload.environment ?? {}),
|
|
||||||
},
|
|
||||||
metadata,
|
|
||||||
this.params
|
|
||||||
);
|
|
||||||
|
|
||||||
taskRunProcess.onExit.attach(() => {
|
await this.#killCurrentTaskRunProcessBeforeAttempt();
|
||||||
|
|
||||||
|
const taskRunProcess = new TaskRunProcess(
|
||||||
|
payload.execution.run.id,
|
||||||
|
payload.execution.run.isTest,
|
||||||
|
this.path,
|
||||||
|
{
|
||||||
|
...this.params.env,
|
||||||
|
...(payload.environment ?? {}),
|
||||||
|
},
|
||||||
|
metadata,
|
||||||
|
this.params,
|
||||||
|
messageId
|
||||||
|
);
|
||||||
|
|
||||||
|
taskRunProcess.onExit.attach(({ pid }) => {
|
||||||
|
console.log("Task run process exited", { pid });
|
||||||
|
|
||||||
|
// Only delete the task run process if the pid matches
|
||||||
|
if (this._taskRunProcess?.pid === pid) {
|
||||||
this._taskRunProcess = undefined;
|
this._taskRunProcess = undefined;
|
||||||
});
|
}
|
||||||
|
|
||||||
taskRunProcess.onTaskHeartbeat.attach((id) => {
|
if (pid) {
|
||||||
this.onTaskHeartbeat.post(id);
|
this._taskRunProcessesBeingKilled.delete(pid);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
|
||||||
taskRunProcess.onWaitForBatch.attach((message) => {
|
taskRunProcess.onIsBeingKilled.attach((taskRunProcess) => {
|
||||||
this.onWaitForBatch.post(message);
|
if (taskRunProcess?.pid) {
|
||||||
});
|
this._taskRunProcessesBeingKilled.set(taskRunProcess.pid, taskRunProcess);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
taskRunProcess.onWaitForDuration.attach((message) => {
|
taskRunProcess.onTaskHeartbeat.attach((id) => {
|
||||||
this.onWaitForDuration.post(message);
|
this.onTaskHeartbeat.post(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
taskRunProcess.onWaitForTask.attach((message) => {
|
taskRunProcess.onTaskRunHeartbeat.attach((id) => {
|
||||||
this.onWaitForTask.post(message);
|
this.onTaskRunHeartbeat.post(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
taskRunProcess.onReadyForCheckpoint.attach((message) => {
|
taskRunProcess.onWaitForBatch.attach((message) => {
|
||||||
this.onReadyForCheckpoint.post(message);
|
this.onWaitForBatch.post(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
taskRunProcess.onCancelCheckpoint.attach((message) => {
|
taskRunProcess.onWaitForDuration.attach((message) => {
|
||||||
this.onCancelCheckpoint.post(message);
|
this.onWaitForDuration.post(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notify down the chain
|
taskRunProcess.onWaitForTask.attach((message) => {
|
||||||
this.preCheckpointNotification.attach((message) => {
|
this.onWaitForTask.post(message);
|
||||||
taskRunProcess.preCheckpointNotification.post(message);
|
});
|
||||||
});
|
|
||||||
this.checkpointCanceledNotification.attach((message) => {
|
|
||||||
taskRunProcess.checkpointCanceledNotification.post(message);
|
|
||||||
});
|
|
||||||
|
|
||||||
await taskRunProcess.initialize();
|
taskRunProcess.onReadyForCheckpoint.attach((message) => {
|
||||||
|
this.onReadyForCheckpoint.post(message);
|
||||||
|
});
|
||||||
|
|
||||||
this._taskRunProcess = taskRunProcess;
|
taskRunProcess.onCancelCheckpoint.attach((message) => {
|
||||||
}
|
this.onCancelCheckpoint.post(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Notify down the chain
|
||||||
|
this.preCheckpointNotification.attach((message) => {
|
||||||
|
taskRunProcess.preCheckpointNotification.post(message);
|
||||||
|
});
|
||||||
|
this.checkpointCanceledNotification.attach((message) => {
|
||||||
|
taskRunProcess.checkpointCanceledNotification.post(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
await taskRunProcess.initialize();
|
||||||
|
|
||||||
|
this._taskRunProcess = taskRunProcess;
|
||||||
|
|
||||||
return this._taskRunProcess;
|
return this._taskRunProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to fork the process before we can execute any tasks
|
async forceKillOldTaskRunProcesses() {
|
||||||
async executeTaskRun(payload: ProdTaskRunExecutionPayload): Promise<TaskRunExecutionResult> {
|
for (const taskRunProcess of this._taskRunProcessesBeingKilled.values()) {
|
||||||
|
try {
|
||||||
|
await taskRunProcess.kill("SIGKILL");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error while force killing old task run processes", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #killCurrentTaskRunProcessBeforeAttempt() {
|
||||||
|
console.log("killCurrentTaskRunProcessBeforeAttempt()", {
|
||||||
|
hasTaskRunProcess: !!this._taskRunProcess,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this._taskRunProcess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTaskRunProcess = this._taskRunProcess;
|
||||||
|
|
||||||
|
console.log("Killing current task run process", {
|
||||||
|
isBeingKilled: currentTaskRunProcess?.isBeingKilled,
|
||||||
|
totalBeingKilled: this._taskRunProcessesBeingKilled.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentTaskRunProcess.isBeingKilled) {
|
||||||
|
if (this._taskRunProcessesBeingKilled.size > 1) {
|
||||||
|
await this.#tryGracefulExit(currentTaskRunProcess);
|
||||||
|
} else {
|
||||||
|
// If there's only one or none being killed, don't do anything so we can create a fresh one in parallel
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's not being killed, so kill it
|
||||||
|
if (this._taskRunProcessesBeingKilled.size > 0) {
|
||||||
|
await this.#tryGracefulExit(currentTaskRunProcess);
|
||||||
|
} else {
|
||||||
|
// There's none being killed yet, so we can kill it without waiting. We still set a timeout to kill it forcefully just in case it sticks around.
|
||||||
|
currentTaskRunProcess.kill("SIGTERM", 5_000).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #tryGracefulExit(
|
||||||
|
taskRunProcess: TaskRunProcess,
|
||||||
|
kill = false,
|
||||||
|
initialSignal: number | NodeJS.Signals = "SIGTERM"
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const taskRunProcess = await this.#initializeTaskRunProcess(payload);
|
const initialExit = taskRunProcess.onExit.waitFor(5_000);
|
||||||
|
|
||||||
|
if (kill) {
|
||||||
|
taskRunProcess.kill(initialSignal);
|
||||||
|
}
|
||||||
|
|
||||||
|
await initialExit;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("TaskRunProcess graceful kill timeout exceeded", error);
|
||||||
|
|
||||||
|
this.#tryForcefulExit(taskRunProcess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async #tryForcefulExit(taskRunProcess: TaskRunProcess) {
|
||||||
|
try {
|
||||||
|
const forcedKill = taskRunProcess.onExit.waitFor(5_000);
|
||||||
|
taskRunProcess.kill("SIGKILL");
|
||||||
|
await forcedKill;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("TaskRunProcess forced kill timeout exceeded", error);
|
||||||
|
throw new SigKillTimeoutProcessError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to fork the process before we can execute any tasks, use a fresh process for each execution
|
||||||
|
async executeTaskRun(
|
||||||
|
payload: ProdTaskRunExecutionPayload,
|
||||||
|
messageId?: string
|
||||||
|
): Promise<TaskRunExecutionResult> {
|
||||||
|
try {
|
||||||
|
const taskRunProcess = await this.#getFreshTaskRunProcess(payload, messageId);
|
||||||
|
|
||||||
|
console.log("executing task run", {
|
||||||
|
attempt: payload.execution.attempt.id,
|
||||||
|
taskRunPid: taskRunProcess.pid,
|
||||||
|
});
|
||||||
|
|
||||||
const result = await taskRunProcess.executeTaskRun(payload);
|
const result = await taskRunProcess.executeTaskRun(payload);
|
||||||
|
|
||||||
@@ -326,6 +464,31 @@ export class ProdBackgroundWorker {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e instanceof SigKillTimeoutProcessError) {
|
||||||
|
return {
|
||||||
|
id: payload.execution.attempt.id,
|
||||||
|
ok: false,
|
||||||
|
retry: undefined,
|
||||||
|
error: {
|
||||||
|
type: "INTERNAL_ERROR",
|
||||||
|
code: TaskRunErrorCodes.TASK_PROCESS_SIGKILL_TIMEOUT,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e instanceof GracefulExitTimeoutError) {
|
||||||
|
return {
|
||||||
|
id: payload.execution.attempt.id,
|
||||||
|
ok: false,
|
||||||
|
retry: undefined,
|
||||||
|
error: {
|
||||||
|
type: "INTERNAL_ERROR",
|
||||||
|
code: TaskRunErrorCodes.GRACEFUL_EXIT_TIMEOUT,
|
||||||
|
message: "Worker process killed while attempt in progress.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: payload.execution.attempt.id,
|
id: payload.execution.attempt.id,
|
||||||
ok: false,
|
ok: false,
|
||||||
@@ -335,11 +498,52 @@ export class ProdBackgroundWorker {
|
|||||||
code: TaskRunErrorCodes.TASK_EXECUTION_FAILED,
|
code: TaskRunErrorCodes.TASK_EXECUTION_FAILED,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
} finally {
|
||||||
|
await this.#killTaskRunProcess();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelAttempt(attemptId: string) {
|
async cancelAttempt(attemptId: string) {
|
||||||
await this._taskRunProcess?.cancel();
|
if (!this._taskRunProcess) {
|
||||||
|
console.error("No task run process to cancel attempt", { attemptId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this._taskRunProcess.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeTaskRunLazyAttempt(payload: TaskRunExecutionLazyAttemptPayload) {
|
||||||
|
// Post to coordinator
|
||||||
|
this.onCreateTaskRunAttempt.post({ runId: payload.runId });
|
||||||
|
|
||||||
|
let execution: ProdTaskRunExecution;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ..and wait for response
|
||||||
|
const attemptCreated = await this.attemptCreatedNotification.waitFor(30_000);
|
||||||
|
|
||||||
|
if (!attemptCreated.success) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to create attempt${attemptCreated.reason ? `: ${attemptCreated.reason}` : ""}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
execution = attemptCreated.execution;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error while creating attempt", error);
|
||||||
|
throw new Error(`Failed to create task run attempt: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const completion = await this.executeTaskRun(
|
||||||
|
{
|
||||||
|
execution,
|
||||||
|
traceContext: payload.traceContext,
|
||||||
|
environment: payload.environment,
|
||||||
|
},
|
||||||
|
payload.messageId
|
||||||
|
);
|
||||||
|
|
||||||
|
return { execution, completion };
|
||||||
}
|
}
|
||||||
|
|
||||||
async #correctError(
|
async #correctError(
|
||||||
@@ -359,6 +563,7 @@ class TaskRunProcess {
|
|||||||
typeof ProdWorkerToChildMessages
|
typeof ProdWorkerToChildMessages
|
||||||
>;
|
>;
|
||||||
private _child?: ChildProcess;
|
private _child?: ChildProcess;
|
||||||
|
private _childPid?: number;
|
||||||
|
|
||||||
private _attemptPromises: Map<
|
private _attemptPromises: Map<
|
||||||
string,
|
string,
|
||||||
@@ -368,9 +573,16 @@ class TaskRunProcess {
|
|||||||
private _currentExecution: TaskRunExecution | undefined;
|
private _currentExecution: TaskRunExecution | undefined;
|
||||||
private _isBeingKilled: boolean = false;
|
private _isBeingKilled: boolean = false;
|
||||||
private _isBeingCancelled: boolean = false;
|
private _isBeingCancelled: boolean = false;
|
||||||
|
private _gracefulExitTimeoutElapsed: boolean = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use onTaskRunHeartbeat instead
|
||||||
|
*/
|
||||||
public onTaskHeartbeat: Evt<string> = new Evt();
|
public onTaskHeartbeat: Evt<string> = new Evt();
|
||||||
public onExit: Evt<number> = new Evt();
|
public onTaskRunHeartbeat: Evt<string> = new Evt();
|
||||||
|
public onExit: Evt<{ code: number | null; signal: NodeJS.Signals | null; pid?: number }> =
|
||||||
|
new Evt();
|
||||||
|
public onIsBeingKilled: Evt<TaskRunProcess> = new Evt();
|
||||||
|
|
||||||
public onWaitForBatch: Evt<
|
public onWaitForBatch: Evt<
|
||||||
InferSocketMessageSchema<typeof ProdChildToWorkerMessages, "WAIT_FOR_BATCH">
|
InferSocketMessageSchema<typeof ProdChildToWorkerMessages, "WAIT_FOR_BATCH">
|
||||||
@@ -389,18 +601,20 @@ class TaskRunProcess {
|
|||||||
public onCancelCheckpoint = Evt.create<{ version?: "v1" | "v2"; reason?: WaitReason }>();
|
public onCancelCheckpoint = Evt.create<{ version?: "v1" | "v2"; reason?: WaitReason }>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private execution: ProdTaskRunExecution,
|
private runId: string,
|
||||||
|
private isTest: boolean,
|
||||||
private path: string,
|
private path: string,
|
||||||
private env: NodeJS.ProcessEnv,
|
private env: NodeJS.ProcessEnv,
|
||||||
private metadata: BackgroundWorkerProperties,
|
private metadata: BackgroundWorkerProperties,
|
||||||
private worker: BackgroundWorkerParams
|
private worker: BackgroundWorkerParams,
|
||||||
|
private messageId?: string
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
this._child = fork(this.path, {
|
this._child = fork(this.path, {
|
||||||
stdio: [/*stdin*/ "ignore", /*stdout*/ "pipe", /*stderr*/ "pipe", "ipc"],
|
stdio: [/*stdin*/ "ignore", /*stdout*/ "pipe", /*stderr*/ "pipe", "ipc"],
|
||||||
env: {
|
env: {
|
||||||
...(this.execution.run.isTest ? { TRIGGER_LOG_LEVEL: "debug" } : {}),
|
...(this.isTest ? { TRIGGER_LOG_LEVEL: "debug" } : {}),
|
||||||
...this.env,
|
...this.env,
|
||||||
OTEL_RESOURCE_ATTRIBUTES: JSON.stringify({
|
OTEL_RESOURCE_ATTRIBUTES: JSON.stringify({
|
||||||
[SemanticInternalAttributes.PROJECT_DIR]: this.worker.projectConfig.projectDir,
|
[SemanticInternalAttributes.PROJECT_DIR]: this.worker.projectConfig.projectDir,
|
||||||
@@ -408,6 +622,7 @@ class TaskRunProcess {
|
|||||||
...(this.worker.debugOtel ? { OTEL_LOG_LEVEL: "debug" } : {}),
|
...(this.worker.debugOtel ? { OTEL_LOG_LEVEL: "debug" } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
this._childPid = this._child?.pid;
|
||||||
|
|
||||||
this._ipc = new ZodIpcConnection({
|
this._ipc = new ZodIpcConnection({
|
||||||
listenSchema: ProdChildToWorkerMessages,
|
listenSchema: ProdChildToWorkerMessages,
|
||||||
@@ -439,7 +654,11 @@ class TaskRunProcess {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
},
|
},
|
||||||
TASK_HEARTBEAT: async (message) => {
|
TASK_HEARTBEAT: async (message) => {
|
||||||
this.onTaskHeartbeat.post(message.id);
|
if (this.messageId) {
|
||||||
|
this.onTaskRunHeartbeat.post(this.messageId);
|
||||||
|
} else {
|
||||||
|
this.onTaskHeartbeat.post(message.id);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
TASKS_READY: async (message) => {},
|
TASKS_READY: async (message) => {},
|
||||||
WAIT_FOR_TASK: async (message) => {
|
WAIT_FOR_TASK: async (message) => {
|
||||||
@@ -513,17 +732,38 @@ class TaskRunProcess {
|
|||||||
await this.cleanup(true);
|
await this.cleanup(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanup(kill: boolean = false) {
|
async cleanup(kill = false, gracefulExitTimeoutElapsed = false) {
|
||||||
|
console.log("cleanup()", { kill, gracefulExitTimeoutElapsed });
|
||||||
|
|
||||||
if (kill && this._isBeingKilled) {
|
if (kill && this._isBeingKilled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._isBeingKilled = kill;
|
if (kill) {
|
||||||
|
this._isBeingKilled = true;
|
||||||
|
this.onIsBeingKilled.post(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
const killChildProcess = gracefulExitTimeoutElapsed && !!this._currentExecution;
|
||||||
|
|
||||||
|
// Kill parent unless graceful exit timeout has elapsed and we're in the middle of an execution
|
||||||
|
const killParentProcess = kill && !killChildProcess;
|
||||||
|
|
||||||
|
console.log("Cleaning up task run process", {
|
||||||
|
killChildProcess,
|
||||||
|
killParentProcess,
|
||||||
|
});
|
||||||
|
|
||||||
await this._ipc?.sendWithAck("CLEANUP", {
|
await this._ipc?.sendWithAck("CLEANUP", {
|
||||||
flush: true,
|
flush: true,
|
||||||
kill,
|
kill: killParentProcess,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (killChildProcess) {
|
||||||
|
this._gracefulExitTimeoutElapsed = true;
|
||||||
|
// Kill the child process
|
||||||
|
await this.kill("SIGKILL");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async executeTaskRun(payload: TaskRunExecutionPayload): Promise<TaskRunExecutionResult> {
|
async executeTaskRun(payload: TaskRunExecutionPayload): Promise<TaskRunExecutionResult> {
|
||||||
@@ -559,15 +799,15 @@ class TaskRunProcess {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
taskRunCompletedNotification(completion: TaskRunExecutionResult, execution: TaskRunExecution) {
|
taskRunCompletedNotification(completion: TaskRunExecutionResult) {
|
||||||
if (!completion.ok && typeof completion.retry !== "undefined") {
|
if (!completion.ok && typeof completion.retry !== "undefined") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._child?.connected && !this._isBeingKilled && !this._child.killed) {
|
if (this._child?.connected && !this._isBeingKilled && !this._child.killed) {
|
||||||
this._ipc?.send("TASK_RUN_COMPLETED_NOTIFICATION", {
|
this._ipc?.send("TASK_RUN_COMPLETED_NOTIFICATION", {
|
||||||
|
version: "v2",
|
||||||
completion,
|
completion,
|
||||||
execution,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -578,10 +818,14 @@ class TaskRunProcess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #handleExit(code: number) {
|
async #handleExit(code: number | null, signal: NodeJS.Signals | null) {
|
||||||
|
console.log("handling child exit", { code, signal });
|
||||||
|
|
||||||
// Go through all the attempts currently pending and reject them
|
// Go through all the attempts currently pending and reject them
|
||||||
for (const [id, status] of this._attemptStatuses.entries()) {
|
for (const [id, status] of this._attemptStatuses.entries()) {
|
||||||
if (status === "PENDING") {
|
if (status === "PENDING") {
|
||||||
|
console.log("found pending attempt", { id });
|
||||||
|
|
||||||
this._attemptStatuses.set(id, "REJECTED");
|
this._attemptStatuses.set(id, "REJECTED");
|
||||||
|
|
||||||
const attemptPromise = this._attemptPromises.get(id);
|
const attemptPromise = this._attemptPromises.get(id);
|
||||||
@@ -594,15 +838,18 @@ class TaskRunProcess {
|
|||||||
|
|
||||||
if (this._isBeingCancelled) {
|
if (this._isBeingCancelled) {
|
||||||
rejecter(new CancelledProcessError());
|
rejecter(new CancelledProcessError());
|
||||||
|
} else if (this._gracefulExitTimeoutElapsed) {
|
||||||
|
// Order matters, this has to be before the graceful exit timeout
|
||||||
|
rejecter(new GracefulExitTimeoutError());
|
||||||
} else if (this._isBeingKilled) {
|
} else if (this._isBeingKilled) {
|
||||||
rejecter(new CleanupProcessError());
|
rejecter(new CleanupProcessError());
|
||||||
} else {
|
} else {
|
||||||
rejecter(new UnexpectedExitError(code));
|
rejecter(new UnexpectedExitError(code ?? -1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.onExit.post(code);
|
this.onExit.post({ code, signal, pid: this.pid });
|
||||||
}
|
}
|
||||||
|
|
||||||
#handleLog(data: Buffer) {
|
#handleLog(data: Buffer) {
|
||||||
@@ -635,9 +882,24 @@ class TaskRunProcess {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#kill() {
|
async kill(signal?: number | NodeJS.Signals, timeoutInMs?: number) {
|
||||||
if (this._child && !this._child.killed) {
|
this._isBeingKilled = true;
|
||||||
this._child?.kill();
|
|
||||||
|
const killTimeout = this.onExit.waitFor(timeoutInMs);
|
||||||
|
|
||||||
|
this.onIsBeingKilled.post(this);
|
||||||
|
this._child?.kill(signal);
|
||||||
|
|
||||||
|
if (timeoutInMs) {
|
||||||
|
await killTimeout;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isBeingKilled() {
|
||||||
|
return this._isBeingKilled || this._child?.killed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get pid() {
|
||||||
|
return this._childPid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
PreStopCauses,
|
PreStopCauses,
|
||||||
ProdWorkerToCoordinatorMessages,
|
ProdWorkerToCoordinatorMessages,
|
||||||
TaskResource,
|
TaskResource,
|
||||||
|
TaskRunFailedExecutionResult,
|
||||||
WaitReason,
|
WaitReason,
|
||||||
} from "@trigger.dev/core/v3";
|
} from "@trigger.dev/core/v3";
|
||||||
import { ZodSocketConnection } from "@trigger.dev/core/v3/zodSocket";
|
import { ZodSocketConnection } from "@trigger.dev/core/v3/zodSocket";
|
||||||
@@ -60,104 +61,7 @@ class ProdWorker {
|
|||||||
process.on("SIGTERM", this.#handleSignal.bind(this, "SIGTERM"));
|
process.on("SIGTERM", this.#handleSignal.bind(this, "SIGTERM"));
|
||||||
|
|
||||||
this.#coordinatorSocket = this.#createCoordinatorSocket(COORDINATOR_HOST);
|
this.#coordinatorSocket = this.#createCoordinatorSocket(COORDINATOR_HOST);
|
||||||
|
this.#backgroundWorker = this.#createBackgroundWorker();
|
||||||
this.#backgroundWorker = new ProdBackgroundWorker("worker.js", {
|
|
||||||
projectConfig: __PROJECT_CONFIG__,
|
|
||||||
env: {
|
|
||||||
...gatherProcessEnv(),
|
|
||||||
TRIGGER_API_URL: this.apiUrl,
|
|
||||||
TRIGGER_SECRET_KEY: this.apiKey,
|
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT:
|
|
||||||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://0.0.0.0:4318",
|
|
||||||
},
|
|
||||||
contentHash: this.contentHash,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#backgroundWorker.onTaskHeartbeat.attach((attemptFriendlyId) => {
|
|
||||||
// TODO: Switch to .send() once coordinator uses zod handler for all messages
|
|
||||||
this.#coordinatorSocket.socket.emit("TASK_HEARTBEAT", { version: "v1", attemptFriendlyId });
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#backgroundWorker.onReadyForCheckpoint.attach(async (message) => {
|
|
||||||
// Flush before checkpointing so we don't flush the same spans again after restore
|
|
||||||
await this.#backgroundWorker.flushTelemetry();
|
|
||||||
this.#coordinatorSocket.socket.emit("READY_FOR_CHECKPOINT", { version: "v1" });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Currently, this is only used for duration waits. Might need adjusting for other use cases.
|
|
||||||
this.#backgroundWorker.onCancelCheckpoint.attach(async (message) => {
|
|
||||||
logger.log("onCancelCheckpoint", { message });
|
|
||||||
|
|
||||||
const { checkpointCanceled } = await this.#coordinatorSocket.socket.emitWithAck(
|
|
||||||
"CANCEL_CHECKPOINT",
|
|
||||||
{
|
|
||||||
version: "v2",
|
|
||||||
reason: message.reason,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (checkpointCanceled) {
|
|
||||||
if (message.reason === "WAIT_FOR_DURATION") {
|
|
||||||
// Worker will resume immediately
|
|
||||||
this.paused = false;
|
|
||||||
this.nextResumeAfter = undefined;
|
|
||||||
this.waitForPostStart = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.#backgroundWorker.checkpointCanceledNotification.post({ checkpointCanceled });
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#backgroundWorker.onWaitForDuration.attach(async (message) => {
|
|
||||||
if (!this.attemptFriendlyId) {
|
|
||||||
logger.error("Failed to send wait message, attempt friendly ID not set", { message });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { willCheckpointAndRestore } = await this.#coordinatorSocket.socket.emitWithAck(
|
|
||||||
"WAIT_FOR_DURATION",
|
|
||||||
{
|
|
||||||
...message,
|
|
||||||
attemptFriendlyId: this.attemptFriendlyId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.#prepareForWait("WAIT_FOR_DURATION", willCheckpointAndRestore);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#backgroundWorker.onWaitForTask.attach(async (message) => {
|
|
||||||
if (!this.attemptFriendlyId) {
|
|
||||||
logger.error("Failed to send wait message, attempt friendly ID not set", { message });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { willCheckpointAndRestore } = await this.#coordinatorSocket.socket.emitWithAck(
|
|
||||||
"WAIT_FOR_TASK",
|
|
||||||
{
|
|
||||||
...message,
|
|
||||||
attemptFriendlyId: this.attemptFriendlyId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.#prepareForWait("WAIT_FOR_TASK", willCheckpointAndRestore);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#backgroundWorker.onWaitForBatch.attach(async (message) => {
|
|
||||||
if (!this.attemptFriendlyId) {
|
|
||||||
logger.error("Failed to send wait message, attempt friendly ID not set", { message });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { willCheckpointAndRestore } = await this.#coordinatorSocket.socket.emitWithAck(
|
|
||||||
"WAIT_FOR_BATCH",
|
|
||||||
{
|
|
||||||
...message,
|
|
||||||
attemptFriendlyId: this.attemptFriendlyId,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.#prepareForWait("WAIT_FOR_BATCH", willCheckpointAndRestore);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#httpPort = port;
|
this.#httpPort = port;
|
||||||
this.#httpServer = this.#createHttpServer();
|
this.#httpServer = this.#createHttpServer();
|
||||||
@@ -167,6 +71,8 @@ class ProdWorker {
|
|||||||
logger.log("Received signal", { signal });
|
logger.log("Received signal", { signal });
|
||||||
|
|
||||||
if (signal === "SIGTERM") {
|
if (signal === "SIGTERM") {
|
||||||
|
let gracefulExitTimeoutElapsed = false;
|
||||||
|
|
||||||
if (this.executing) {
|
if (this.executing) {
|
||||||
const terminationGracePeriodSeconds = 60 * 60;
|
const terminationGracePeriodSeconds = 60 * 60;
|
||||||
|
|
||||||
@@ -176,21 +82,27 @@ class ProdWorker {
|
|||||||
|
|
||||||
// Wait for termination grace period minus 5s to give cleanup a chance to complete
|
// Wait for termination grace period minus 5s to give cleanup a chance to complete
|
||||||
await setTimeout(terminationGracePeriodSeconds * 1000 - 5000);
|
await setTimeout(terminationGracePeriodSeconds * 1000 - 5000);
|
||||||
|
gracefulExitTimeoutElapsed = true;
|
||||||
|
|
||||||
logger.log("Termination timeout reached, exiting gracefully.");
|
logger.log("Termination timeout reached, exiting gracefully.");
|
||||||
} else {
|
} else {
|
||||||
logger.log("Not executing, exiting immediately.");
|
logger.log("Not executing, exiting immediately.");
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.#exitGracefully();
|
await this.#exitGracefully(gracefulExitTimeoutElapsed);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log("Unhandled signal", { signal });
|
logger.log("Unhandled signal", { signal });
|
||||||
}
|
}
|
||||||
|
|
||||||
async #exitGracefully() {
|
async #exitGracefully(gracefulExitTimeoutElapsed = false) {
|
||||||
await this.#backgroundWorker.close();
|
await this.#backgroundWorker.close(gracefulExitTimeoutElapsed);
|
||||||
process.exit(0);
|
|
||||||
|
if (!gracefulExitTimeoutElapsed) {
|
||||||
|
// TODO: Maybe add a sensible timeout instead of a conditional to avoid zombies
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async #reconnect(isPostStart = false, reconnectImmediately = false) {
|
async #reconnect(isPostStart = false, reconnectImmediately = false) {
|
||||||
@@ -222,12 +134,169 @@ class ProdWorker {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("taskinfo read error during reconnect", { error });
|
logger.error("taskinfo read error during reconnect", {
|
||||||
|
error: error instanceof Error ? error.message : error,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
this.#coordinatorSocket = this.#createCoordinatorSocket(coordinatorHost);
|
this.#coordinatorSocket = this.#createCoordinatorSocket(coordinatorHost);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#createBackgroundWorker() {
|
||||||
|
const backgroundWorker = new ProdBackgroundWorker("worker.js", {
|
||||||
|
projectConfig: __PROJECT_CONFIG__,
|
||||||
|
env: {
|
||||||
|
...gatherProcessEnv(),
|
||||||
|
TRIGGER_API_URL: this.apiUrl,
|
||||||
|
TRIGGER_SECRET_KEY: this.apiKey,
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT:
|
||||||
|
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://0.0.0.0:4318",
|
||||||
|
},
|
||||||
|
contentHash: this.contentHash,
|
||||||
|
});
|
||||||
|
|
||||||
|
backgroundWorker.onTaskHeartbeat.attach((attemptFriendlyId) => {
|
||||||
|
// TODO: Switch to .send() once coordinator uses zod handler for all messages
|
||||||
|
this.#coordinatorSocket.socket.emit("TASK_HEARTBEAT", { version: "v1", attemptFriendlyId });
|
||||||
|
});
|
||||||
|
|
||||||
|
backgroundWorker.onTaskRunHeartbeat.attach((runId) => {
|
||||||
|
this.#coordinatorSocket.socket.emit("TASK_RUN_HEARTBEAT", { version: "v1", runId });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Currently, this is only used for duration waits
|
||||||
|
backgroundWorker.onReadyForCheckpoint.attach(async (message) => {
|
||||||
|
await this.#prepareForCheckpoint();
|
||||||
|
|
||||||
|
this.#coordinatorSocket.socket.emit("READY_FOR_CHECKPOINT", { version: "v1" });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Currently, this is only used for duration waits. Might need adjusting for other use cases.
|
||||||
|
backgroundWorker.onCancelCheckpoint.attach(async (message) => {
|
||||||
|
logger.log("onCancelCheckpoint", { message });
|
||||||
|
|
||||||
|
const { checkpointCanceled } = await this.#coordinatorSocket.socket.emitWithAck(
|
||||||
|
"CANCEL_CHECKPOINT",
|
||||||
|
{
|
||||||
|
version: "v2",
|
||||||
|
reason: message.reason,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.log("onCancelCheckpoint coordinator response", { checkpointCanceled });
|
||||||
|
|
||||||
|
if (checkpointCanceled) {
|
||||||
|
if (message.reason === "WAIT_FOR_DURATION") {
|
||||||
|
// Worker will resume immediately
|
||||||
|
this.paused = false;
|
||||||
|
this.nextResumeAfter = undefined;
|
||||||
|
this.waitForPostStart = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backgroundWorker.checkpointCanceledNotification.post({ checkpointCanceled });
|
||||||
|
});
|
||||||
|
|
||||||
|
backgroundWorker.onCreateTaskRunAttempt.attach(async (message) => {
|
||||||
|
logger.log("onCreateTaskRunAttempt()", { message });
|
||||||
|
|
||||||
|
const createAttempt = await this.#coordinatorSocket.socket.emitWithAck(
|
||||||
|
"CREATE_TASK_RUN_ATTEMPT",
|
||||||
|
{
|
||||||
|
version: "v1",
|
||||||
|
runId: message.runId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!createAttempt.success) {
|
||||||
|
backgroundWorker.attemptCreatedNotification.post({
|
||||||
|
success: false,
|
||||||
|
reason: createAttempt.reason,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
backgroundWorker.attemptCreatedNotification.post({
|
||||||
|
success: true,
|
||||||
|
execution: createAttempt.executionPayload.execution,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
backgroundWorker.attemptCreatedNotification.attach((message) => {
|
||||||
|
if (!message.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workers with lazy attempt support set their friendly ID here
|
||||||
|
this.attemptFriendlyId = message.execution.attempt.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
backgroundWorker.onWaitForDuration.attach(async (message) => {
|
||||||
|
if (!this.attemptFriendlyId) {
|
||||||
|
logger.error("Failed to send wait message, attempt friendly ID not set", { message });
|
||||||
|
|
||||||
|
this.#emitUnrecoverableError(
|
||||||
|
"NoAttemptId",
|
||||||
|
"Attempt ID not set before waiting for duration"
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { willCheckpointAndRestore } = await this.#coordinatorSocket.socket.emitWithAck(
|
||||||
|
"WAIT_FOR_DURATION",
|
||||||
|
{
|
||||||
|
...message,
|
||||||
|
attemptFriendlyId: this.attemptFriendlyId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#prepareForWait("WAIT_FOR_DURATION", willCheckpointAndRestore);
|
||||||
|
});
|
||||||
|
|
||||||
|
backgroundWorker.onWaitForTask.attach(async (message) => {
|
||||||
|
if (!this.attemptFriendlyId) {
|
||||||
|
logger.error("Failed to send wait message, attempt friendly ID not set", { message });
|
||||||
|
|
||||||
|
this.#emitUnrecoverableError("NoAttemptId", "Attempt ID not set before waiting for task");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { willCheckpointAndRestore } = await this.#coordinatorSocket.socket.emitWithAck(
|
||||||
|
"WAIT_FOR_TASK",
|
||||||
|
{
|
||||||
|
...message,
|
||||||
|
attemptFriendlyId: this.attemptFriendlyId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#prepareForWait("WAIT_FOR_TASK", willCheckpointAndRestore);
|
||||||
|
});
|
||||||
|
|
||||||
|
backgroundWorker.onWaitForBatch.attach(async (message) => {
|
||||||
|
if (!this.attemptFriendlyId) {
|
||||||
|
logger.error("Failed to send wait message, attempt friendly ID not set", { message });
|
||||||
|
|
||||||
|
this.#emitUnrecoverableError("NoAttemptId", "Attempt ID not set before waiting for batch");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { willCheckpointAndRestore } = await this.#coordinatorSocket.socket.emitWithAck(
|
||||||
|
"WAIT_FOR_BATCH",
|
||||||
|
{
|
||||||
|
...message,
|
||||||
|
attemptFriendlyId: this.attemptFriendlyId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.#prepareForWait("WAIT_FOR_BATCH", willCheckpointAndRestore);
|
||||||
|
});
|
||||||
|
|
||||||
|
return backgroundWorker;
|
||||||
|
}
|
||||||
|
|
||||||
async #prepareForWait(reason: WaitReason, willCheckpointAndRestore: boolean) {
|
async #prepareForWait(reason: WaitReason, willCheckpointAndRestore: boolean) {
|
||||||
logger.log(`prepare for ${reason}`, { willCheckpointAndRestore });
|
logger.log(`prepare for ${reason}`, { willCheckpointAndRestore });
|
||||||
|
|
||||||
@@ -239,9 +308,8 @@ class ProdWorker {
|
|||||||
this.waitForPostStart = true;
|
this.waitForPostStart = true;
|
||||||
|
|
||||||
if (reason === "WAIT_FOR_TASK" || reason === "WAIT_FOR_BATCH") {
|
if (reason === "WAIT_FOR_TASK" || reason === "WAIT_FOR_BATCH") {
|
||||||
// Flush before checkpointing so we don't flush the same spans again after restore
|
|
||||||
// Duration waits do this via the "ready for checkpoint" event instead
|
// Duration waits do this via the "ready for checkpoint" event instead
|
||||||
await this.#backgroundWorker.flushTelemetry();
|
await this.#prepareForCheckpoint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,18 +324,36 @@ class ProdWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.#exitGracefully();
|
await this.#exitGracefully();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear state for next execution
|
||||||
|
this.paused = false;
|
||||||
|
this.waitForPostStart = false;
|
||||||
this.executing = false;
|
this.executing = false;
|
||||||
this.attemptFriendlyId = undefined;
|
this.attemptFriendlyId = undefined;
|
||||||
|
|
||||||
if (willCheckpointAndRestore) {
|
if (willCheckpointAndRestore) {
|
||||||
this.waitForPostStart = true;
|
this.waitForPostStart = true;
|
||||||
|
|
||||||
|
// We already flush after completion, so we don't need to do it here
|
||||||
|
this.#prepareForCheckpoint(false);
|
||||||
|
|
||||||
this.#coordinatorSocket.socket.emit("READY_FOR_CHECKPOINT", { version: "v1" });
|
this.#coordinatorSocket.socket.emit("READY_FOR_CHECKPOINT", { version: "v1" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async #prepareForCheckpoint(flush = true) {
|
||||||
|
if (flush) {
|
||||||
|
// Flush before checkpointing so we don't flush the same spans again after restore
|
||||||
|
await this.#backgroundWorker.flushTelemetry();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kill the previous worker process to prevent large checkpoints
|
||||||
|
await this.#backgroundWorker.forceKillOldTaskRunProcesses();
|
||||||
|
}
|
||||||
|
|
||||||
#resumeAfterDuration() {
|
#resumeAfterDuration() {
|
||||||
this.paused = false;
|
this.paused = false;
|
||||||
this.nextResumeAfter = undefined;
|
this.nextResumeAfter = undefined;
|
||||||
@@ -303,11 +389,8 @@ class ProdWorker {
|
|||||||
extraHeaders["x-trigger-attempt-friendly-id"] = this.attemptFriendlyId;
|
extraHeaders["x-trigger-attempt-friendly-id"] = this.attemptFriendlyId;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log("connecting to coordinator", {
|
logger.log(`connecting to coordinator: ${host}:${COORDINATOR_PORT}`);
|
||||||
host,
|
logger.debug(`connecting with extra headers`, { extraHeaders });
|
||||||
port: COORDINATOR_PORT,
|
|
||||||
extraHeaders,
|
|
||||||
});
|
|
||||||
|
|
||||||
const coordinatorConnection = new ZodSocketConnection({
|
const coordinatorConnection = new ZodSocketConnection({
|
||||||
namespace: "prod-worker",
|
namespace: "prod-worker",
|
||||||
@@ -317,28 +400,14 @@ class ProdWorker {
|
|||||||
serverMessages: CoordinatorToProdWorkerMessages,
|
serverMessages: CoordinatorToProdWorkerMessages,
|
||||||
extraHeaders,
|
extraHeaders,
|
||||||
handlers: {
|
handlers: {
|
||||||
RESUME_AFTER_DEPENDENCY: async (message) => {
|
RESUME_AFTER_DEPENDENCY: async ({ completions }) => {
|
||||||
if (!this.paused) {
|
if (!this.paused) {
|
||||||
logger.error("worker not paused", {
|
logger.error("Failed to resume after dependency: Worker not paused");
|
||||||
completions: message.completions,
|
|
||||||
executions: message.executions,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.completions.length !== message.executions.length) {
|
if (completions.length === 0) {
|
||||||
logger.error("did not receive the same number of completions and executions", {
|
logger.error("Failed to resume after dependency: No completions");
|
||||||
completions: message.completions,
|
|
||||||
executions: message.executions,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.completions.length === 0 || message.executions.length === 0) {
|
|
||||||
logger.error("no completions or executions", {
|
|
||||||
completions: message.completions,
|
|
||||||
executions: message.executions,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,17 +415,19 @@ class ProdWorker {
|
|||||||
this.nextResumeAfter !== "WAIT_FOR_TASK" &&
|
this.nextResumeAfter !== "WAIT_FOR_TASK" &&
|
||||||
this.nextResumeAfter !== "WAIT_FOR_BATCH"
|
this.nextResumeAfter !== "WAIT_FOR_BATCH"
|
||||||
) {
|
) {
|
||||||
logger.error("not waiting to resume after dependency", {
|
logger.error("Failed to resume after dependency: Invalid next resume", {
|
||||||
nextResumeAfter: this.nextResumeAfter,
|
nextResumeAfter: this.nextResumeAfter,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.nextResumeAfter === "WAIT_FOR_TASK" && message.completions.length > 1) {
|
if (this.nextResumeAfter === "WAIT_FOR_TASK" && completions.length > 1) {
|
||||||
logger.error("waiting for single task but got multiple completions", {
|
logger.error(
|
||||||
completions: message.completions,
|
"Failed to resume after dependency: Waiting for single task but got multiple completions",
|
||||||
executions: message.executions,
|
{
|
||||||
});
|
completions: completions,
|
||||||
|
}
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,13 +435,12 @@ class ProdWorker {
|
|||||||
this.nextResumeAfter = undefined;
|
this.nextResumeAfter = undefined;
|
||||||
this.waitForPostStart = false;
|
this.waitForPostStart = false;
|
||||||
|
|
||||||
for (let i = 0; i < message.completions.length; i++) {
|
for (let i = 0; i < completions.length; i++) {
|
||||||
const completion = message.completions[i];
|
const completion = completions[i];
|
||||||
const execution = message.executions[i];
|
|
||||||
|
|
||||||
if (!completion || !execution) continue;
|
if (!completion) continue;
|
||||||
|
|
||||||
this.#backgroundWorker.taskRunCompletedNotification(completion, execution);
|
this.#backgroundWorker.taskRunCompletedNotification(completion);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
RESUME_AFTER_DURATION: async (message) => {
|
RESUME_AFTER_DURATION: async (message) => {
|
||||||
@@ -420,14 +490,75 @@ class ProdWorker {
|
|||||||
|
|
||||||
this.#prepareForRetry(willCheckpointAndRestore, shouldExit);
|
this.#prepareForRetry(willCheckpointAndRestore, shouldExit);
|
||||||
},
|
},
|
||||||
REQUEST_ATTEMPT_CANCELLATION: async (message) => {
|
EXECUTE_TASK_RUN_LAZY_ATTEMPT: async (message) => {
|
||||||
if (!this.executing) {
|
if (this.executing) {
|
||||||
|
logger.error("dropping execute request, already executing");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.executing = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { completion, execution } =
|
||||||
|
await this.#backgroundWorker.executeTaskRunLazyAttempt(message.lazyPayload);
|
||||||
|
|
||||||
|
logger.log("completed", completion);
|
||||||
|
|
||||||
|
this.completed.add(execution.attempt.id);
|
||||||
|
|
||||||
|
const { willCheckpointAndRestore, shouldExit } =
|
||||||
|
await this.#coordinatorSocket.socket.emitWithAck("TASK_RUN_COMPLETED", {
|
||||||
|
version: "v1",
|
||||||
|
execution,
|
||||||
|
completion,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log("completion acknowledged", { willCheckpointAndRestore, shouldExit });
|
||||||
|
|
||||||
|
this.#prepareForRetry(willCheckpointAndRestore, shouldExit);
|
||||||
|
} catch (error) {
|
||||||
|
const completion: TaskRunFailedExecutionResult = {
|
||||||
|
ok: false,
|
||||||
|
id: message.lazyPayload.runId,
|
||||||
|
retry: undefined,
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? {
|
||||||
|
type: "BUILT_IN_ERROR",
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stackTrace: error.stack ?? "",
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: "BUILT_IN_ERROR",
|
||||||
|
name: "UnknownError",
|
||||||
|
message: String(error),
|
||||||
|
stackTrace: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#coordinatorSocket.socket.emit("TASK_RUN_FAILED_TO_RUN", {
|
||||||
|
version: "v1",
|
||||||
|
completion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
REQUEST_ATTEMPT_CANCELLATION: async (message) => {
|
||||||
|
if (!this.executing) {
|
||||||
|
logger.log("dropping cancel request, not executing", { status: this.#status });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log("cancelling attempt", { attemptId: message.attemptId, status: this.#status });
|
||||||
|
|
||||||
await this.#backgroundWorker.cancelAttempt(message.attemptId);
|
await this.#backgroundWorker.cancelAttempt(message.attemptId);
|
||||||
},
|
},
|
||||||
REQUEST_EXIT: async () => {
|
REQUEST_EXIT: async (message) => {
|
||||||
|
if (message.version === "v2" && message.delayInMs) {
|
||||||
|
logger.log("exit requested with delay", { delayInMs: message.delayInMs });
|
||||||
|
await setTimeout(message.delayInMs);
|
||||||
|
}
|
||||||
|
|
||||||
this.#coordinatorSocket.close();
|
this.#coordinatorSocket.close();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
},
|
},
|
||||||
@@ -436,7 +567,7 @@ class ProdWorker {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#coordinatorSocket.socket.emit("READY_FOR_EXECUTION", {
|
this.#coordinatorSocket.socket.emit("READY_FOR_LAZY_ATTEMPT", {
|
||||||
version: "v1",
|
version: "v1",
|
||||||
runId: this.runId,
|
runId: this.runId,
|
||||||
totalCompletions: this.completed.size,
|
totalCompletions: this.completed.size,
|
||||||
@@ -444,6 +575,8 @@ class ProdWorker {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
onConnection: async (socket, handler, sender, logger) => {
|
onConnection: async (socket, handler, sender, logger) => {
|
||||||
|
logger.log("connected to coordinator", { status: this.#status });
|
||||||
|
|
||||||
if (this.waitForPostStart) {
|
if (this.waitForPostStart) {
|
||||||
logger.log("skip connection handler, waiting for post start hook");
|
logger.log("skip connection handler, waiting for post start hook");
|
||||||
return;
|
return;
|
||||||
@@ -451,11 +584,24 @@ class ProdWorker {
|
|||||||
|
|
||||||
if (this.paused) {
|
if (this.paused) {
|
||||||
if (!this.nextResumeAfter) {
|
if (!this.nextResumeAfter) {
|
||||||
|
logger.error("Missing next resume reason", { status: this.#status });
|
||||||
|
|
||||||
|
this.#emitUnrecoverableError(
|
||||||
|
"NoNextResume",
|
||||||
|
"Next resume reason not set while resuming from paused state"
|
||||||
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.attemptFriendlyId) {
|
if (!this.attemptFriendlyId) {
|
||||||
logger.error("Missing friendly ID");
|
logger.error("Missing friendly ID", { status: this.#status });
|
||||||
|
|
||||||
|
this.#emitUnrecoverableError(
|
||||||
|
"NoAttemptId",
|
||||||
|
"Attempt ID not set while resuming from paused state"
|
||||||
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,9 +619,10 @@ class ProdWorker {
|
|||||||
const taskResources = await this.#initializeWorker();
|
const taskResources = await this.#initializeWorker();
|
||||||
|
|
||||||
const { success } = await socket.emitWithAck("INDEX_TASKS", {
|
const { success } = await socket.emitWithAck("INDEX_TASKS", {
|
||||||
version: "v1",
|
version: "v2",
|
||||||
deploymentId: this.deploymentId,
|
deploymentId: this.deploymentId,
|
||||||
...taskResources,
|
...taskResources,
|
||||||
|
supportsLazyAttempts: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -563,7 +710,7 @@ class ProdWorker {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.emit("READY_FOR_EXECUTION", {
|
socket.emit("READY_FOR_LAZY_ATTEMPT", {
|
||||||
version: "v1",
|
version: "v1",
|
||||||
runId: this.runId,
|
runId: this.runId,
|
||||||
totalCompletions: this.completed.size,
|
totalCompletions: this.completed.size,
|
||||||
@@ -601,12 +748,7 @@ class ProdWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "/status": {
|
case "/status": {
|
||||||
return reply.json({
|
return reply.json(this.#status);
|
||||||
executing: this.executing,
|
|
||||||
paused: this.paused,
|
|
||||||
completed: this.completed.size,
|
|
||||||
nextResumeAfter: this.nextResumeAfter,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case "/connect": {
|
case "/connect": {
|
||||||
@@ -768,6 +910,27 @@ class ProdWorker {
|
|||||||
return data?.variables ?? {};
|
return data?.variables ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get #status() {
|
||||||
|
return {
|
||||||
|
executing: this.executing,
|
||||||
|
paused: this.paused,
|
||||||
|
completed: this.completed.size,
|
||||||
|
nextResumeAfter: this.nextResumeAfter,
|
||||||
|
waitForPostStart: this.waitForPostStart,
|
||||||
|
attemptFriendlyId: this.attemptFriendlyId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#emitUnrecoverableError(name: string, message: string) {
|
||||||
|
this.#coordinatorSocket.socket.emit("UNRECOVERABLE_ERROR", {
|
||||||
|
version: "v1",
|
||||||
|
error: {
|
||||||
|
name,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
this.#httpServer.listen(this.#httpPort, this.host);
|
this.#httpServer.listen(this.#httpPort, this.host);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,8 +170,8 @@ const zodIpc = new ZodIpcConnection({
|
|||||||
_isRunning = false;
|
_isRunning = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
TASK_RUN_COMPLETED_NOTIFICATION: async ({ completion, execution }) => {
|
TASK_RUN_COMPLETED_NOTIFICATION: async ({ completion }) => {
|
||||||
prodRuntimeManager.resumeTask(completion, execution);
|
prodRuntimeManager.resumeTask(completion);
|
||||||
},
|
},
|
||||||
WAIT_COMPLETED_NOTIFICATION: async () => {
|
WAIT_COMPLETED_NOTIFICATION: async () => {
|
||||||
prodRuntimeManager.resumeAfterDuration();
|
prodRuntimeManager.resumeAfterDuration();
|
||||||
@@ -179,23 +179,6 @@ const zodIpc = new ZodIpcConnection({
|
|||||||
CLEANUP: async ({ flush, kill }, sender) => {
|
CLEANUP: async ({ flush, kill }, sender) => {
|
||||||
if (kill) {
|
if (kill) {
|
||||||
await tracingSDK.flush();
|
await tracingSDK.flush();
|
||||||
|
|
||||||
if (_execution) {
|
|
||||||
// Fail currently executing attempt
|
|
||||||
await sender.send("TASK_RUN_COMPLETED", {
|
|
||||||
execution: _execution,
|
|
||||||
result: {
|
|
||||||
ok: false,
|
|
||||||
id: _execution.run.id,
|
|
||||||
error: {
|
|
||||||
type: "INTERNAL_ERROR",
|
|
||||||
code: TaskRunErrorCodes.GRACEFUL_EXIT_TIMEOUT,
|
|
||||||
message: "Worker process killed while attempt in progress.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we need to exit the process
|
// Now we need to exit the process
|
||||||
await sender.send("READY_TO_DISPOSE", undefined);
|
await sender.send("READY_TO_DISPOSE", undefined);
|
||||||
} else {
|
} else {
|
||||||
@@ -228,7 +211,7 @@ zodIpc.send("TASKS_READY", { tasks: TASK_METADATA }).catch((err) => {
|
|||||||
|
|
||||||
process.title = "trigger-prod-worker";
|
process.title = "trigger-prod-worker";
|
||||||
|
|
||||||
async function asyncHeartbeat(initialDelayInSeconds: number = 30, intervalInSeconds: number = 5) {
|
async function asyncHeartbeat(initialDelayInSeconds: number = 30, intervalInSeconds: number = 20) {
|
||||||
async function _doHeartbeat() {
|
async function _doHeartbeat() {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (_isRunning && _execution) {
|
if (_isRunning && _execution) {
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ export interface TaskOperationsCreateOptions {
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
runId: string;
|
runId: string;
|
||||||
attemptId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TaskOperationsRestoreOptions {
|
export interface TaskOperationsRestoreOptions {
|
||||||
@@ -129,7 +128,6 @@ export class ProviderShell implements Provider {
|
|||||||
orgId: message.data.orgId,
|
orgId: message.data.orgId,
|
||||||
projectId: message.data.projectId,
|
projectId: message.data.projectId,
|
||||||
runId: message.data.runId,
|
runId: message.data.runId,
|
||||||
attemptId: message.data.id,
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("create failed", error);
|
logger.error("create failed", error);
|
||||||
|
|||||||
@@ -37,6 +37,14 @@
|
|||||||
"require": "./dist/v3/otel/index.js",
|
"require": "./dist/v3/otel/index.js",
|
||||||
"types": "./dist/v3/otel/index.d.ts"
|
"types": "./dist/v3/otel/index.d.ts"
|
||||||
},
|
},
|
||||||
|
"./v3/zodfetch": {
|
||||||
|
"import": {
|
||||||
|
"types": "./dist/v3/zodfetch.d.mts",
|
||||||
|
"default": "./dist/v3/zodfetch.mjs"
|
||||||
|
},
|
||||||
|
"require": "./dist/v3/zodfetch.js",
|
||||||
|
"types": "./dist/v3/zodfetch.d.ts"
|
||||||
|
},
|
||||||
"./v3/zodMessageHandler": {
|
"./v3/zodMessageHandler": {
|
||||||
"import": {
|
"import": {
|
||||||
"types": "./dist/v3/zodMessageHandler.d.mts",
|
"types": "./dist/v3/zodMessageHandler.d.mts",
|
||||||
|
|||||||
@@ -84,13 +84,13 @@ export function createJsonErrorObject(error: TaskRunError) {
|
|||||||
export function correctErrorStackTrace(
|
export function correctErrorStackTrace(
|
||||||
stackTrace: string,
|
stackTrace: string,
|
||||||
projectDir?: string,
|
projectDir?: string,
|
||||||
options?: { removeFirstLine?: boolean }
|
options?: { removeFirstLine?: boolean; isDev?: boolean }
|
||||||
) {
|
) {
|
||||||
const [errorLine, ...traceLines] = stackTrace.split("\n");
|
const [errorLine, ...traceLines] = stackTrace.split("\n");
|
||||||
|
|
||||||
return [
|
return [
|
||||||
options?.removeFirstLine ? undefined : errorLine,
|
options?.removeFirstLine ? undefined : errorLine,
|
||||||
...traceLines.map((line) => correctStackTraceLine(line, projectDir)),
|
...traceLines.map((line) => correctStackTraceLine(line, projectDir, options?.isDev)),
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
@@ -102,17 +102,21 @@ const LINES_TO_IGNORE = [
|
|||||||
/TaskExecutor/,
|
/TaskExecutor/,
|
||||||
/EXECUTE_TASK_RUN/,
|
/EXECUTE_TASK_RUN/,
|
||||||
/@trigger.dev\/core/,
|
/@trigger.dev\/core/,
|
||||||
|
/packages\/core\/src\/v3/,
|
||||||
/safeJsonProcess/,
|
/safeJsonProcess/,
|
||||||
/__entryPoint.ts/,
|
/__entryPoint.ts/,
|
||||||
|
/ZodIpc/,
|
||||||
|
/startActiveSpan/,
|
||||||
|
/processTicksAndRejections/,
|
||||||
];
|
];
|
||||||
|
|
||||||
function correctStackTraceLine(line: string, projectDir?: string) {
|
function correctStackTraceLine(line: string, projectDir?: string, isDev?: boolean) {
|
||||||
if (LINES_TO_IGNORE.some((regex) => regex.test(line))) {
|
if (LINES_TO_IGNORE.some((regex) => regex.test(line))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check to see if the path is inside the project directory
|
// Check to see if the path is inside the project directory
|
||||||
if (projectDir && !line.includes(projectDir)) {
|
if (isDev && projectDir && !line.includes(projectDir)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,18 +80,18 @@ export class DevRuntimeManager implements RuntimeManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
resumeTask(completion: TaskRunExecutionResult, execution: TaskRunExecution): void {
|
resumeTask(completion: TaskRunExecutionResult, runId: string): void {
|
||||||
const wait = this._taskWaits.get(execution.run.id);
|
const wait = this._taskWaits.get(runId);
|
||||||
|
|
||||||
if (!wait) {
|
if (!wait) {
|
||||||
// We need to store the completion in case the task is awaited later
|
// We need to store the completion in case the task is awaited later
|
||||||
this._pendingCompletionNotifications.set(execution.run.id, completion);
|
this._pendingCompletionNotifications.set(runId, completion);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
wait.resolve(completion);
|
wait.resolve(completion);
|
||||||
|
|
||||||
this._taskWaits.delete(execution.run.id);
|
this._taskWaits.delete(runId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,10 +55,14 @@ export class ProdRuntimeManager implements RuntimeManager {
|
|||||||
this._waitForDuration = { resolve, reject };
|
this._waitForDuration = { resolve, reject };
|
||||||
});
|
});
|
||||||
|
|
||||||
const { willCheckpointAndRestore } = await this.ipc.sendWithAck("WAIT_FOR_DURATION", {
|
const { willCheckpointAndRestore } = await this.ipc.sendWithAck(
|
||||||
ms,
|
"WAIT_FOR_DURATION",
|
||||||
now,
|
{
|
||||||
});
|
ms,
|
||||||
|
now,
|
||||||
|
},
|
||||||
|
31_000
|
||||||
|
);
|
||||||
|
|
||||||
if (!willCheckpointAndRestore) {
|
if (!willCheckpointAndRestore) {
|
||||||
await internalTimeout;
|
await internalTimeout;
|
||||||
@@ -74,18 +78,24 @@ export class ProdRuntimeManager implements RuntimeManager {
|
|||||||
// Resets the clock to the current time
|
// Resets the clock to the current time
|
||||||
clock.reset();
|
clock.reset();
|
||||||
|
|
||||||
// The coordinator should cancel any in-progress checkpoints
|
try {
|
||||||
const { checkpointCanceled, version } = await this.ipc.sendWithAck(
|
// The coordinator should cancel any in-progress checkpoints
|
||||||
"CANCEL_CHECKPOINT",
|
const { checkpointCanceled, version } = await this.ipc.sendWithAck(
|
||||||
{
|
"CANCEL_CHECKPOINT",
|
||||||
version: "v2",
|
{
|
||||||
reason: "WAIT_FOR_DURATION",
|
version: "v2",
|
||||||
},
|
reason: "WAIT_FOR_DURATION",
|
||||||
31_000
|
},
|
||||||
);
|
31_000
|
||||||
|
);
|
||||||
|
|
||||||
if (checkpointCanceled) {
|
if (checkpointCanceled) {
|
||||||
// There won't be a checkpoint or external resume and we've already completed our internal timeout
|
// There won't be a checkpoint or external resume and we've already completed our internal timeout
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If the cancellation times out, we will proceed as if the checkpoint was canceled
|
||||||
|
logger.debug("Checkpoint cancellation timed out", { error });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,19 +108,9 @@ export class ProdRuntimeManager implements RuntimeManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdout.write("pre");
|
|
||||||
process.stdout.write(JSON.stringify(clock.preciseNow()));
|
|
||||||
|
|
||||||
console.log("pre", clock.preciseNow());
|
|
||||||
|
|
||||||
// Resets the clock to the current time
|
// Resets the clock to the current time
|
||||||
clock.reset();
|
clock.reset();
|
||||||
|
|
||||||
console.log("post", clock.preciseNow());
|
|
||||||
|
|
||||||
process.stdout.write("post");
|
|
||||||
process.stdout.write(JSON.stringify(clock.preciseNow()));
|
|
||||||
|
|
||||||
this._waitForDuration.resolve("external");
|
this._waitForDuration.resolve("external");
|
||||||
this._waitForDuration = undefined;
|
this._waitForDuration = undefined;
|
||||||
}
|
}
|
||||||
@@ -167,8 +167,8 @@ export class ProdRuntimeManager implements RuntimeManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
resumeTask(completion: TaskRunExecutionResult, execution: TaskRunExecution): void {
|
resumeTask(completion: TaskRunExecutionResult): void {
|
||||||
const wait = this._taskWaits.get(execution.run.id);
|
const wait = this._taskWaits.get(completion.id);
|
||||||
|
|
||||||
if (!wait) {
|
if (!wait) {
|
||||||
return;
|
return;
|
||||||
@@ -176,7 +176,7 @@ export class ProdRuntimeManager implements RuntimeManager {
|
|||||||
|
|
||||||
wait.resolve(completion);
|
wait.resolve(completion);
|
||||||
|
|
||||||
this._taskWaits.delete(execution.run.id);
|
this._taskWaits.delete(completion.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private get waitThresholdInMs(): number {
|
private get waitThresholdInMs(): number {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export type GetProjectEnvResponse = z.infer<typeof GetProjectEnvResponse>;
|
|||||||
export const CreateBackgroundWorkerRequestBody = z.object({
|
export const CreateBackgroundWorkerRequestBody = z.object({
|
||||||
localOnly: z.boolean(),
|
localOnly: z.boolean(),
|
||||||
metadata: BackgroundWorkerMetadata,
|
metadata: BackgroundWorkerMetadata,
|
||||||
|
supportsLazyAttempts: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateBackgroundWorkerRequestBody = z.infer<typeof CreateBackgroundWorkerRequestBody>;
|
export type CreateBackgroundWorkerRequestBody = z.infer<typeof CreateBackgroundWorkerRequestBody>;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export const TaskRunErrorCodes = {
|
|||||||
TASK_EXECUTION_FAILED: "TASK_EXECUTION_FAILED",
|
TASK_EXECUTION_FAILED: "TASK_EXECUTION_FAILED",
|
||||||
TASK_EXECUTION_ABORTED: "TASK_EXECUTION_ABORTED",
|
TASK_EXECUTION_ABORTED: "TASK_EXECUTION_ABORTED",
|
||||||
TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE: "TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE",
|
TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE: "TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE",
|
||||||
|
TASK_PROCESS_SIGKILL_TIMEOUT: "TASK_PROCESS_SIGKILL_TIMEOUT",
|
||||||
TASK_RUN_CANCELLED: "TASK_RUN_CANCELLED",
|
TASK_RUN_CANCELLED: "TASK_RUN_CANCELLED",
|
||||||
TASK_OUTPUT_ERROR: "TASK_OUTPUT_ERROR",
|
TASK_OUTPUT_ERROR: "TASK_OUTPUT_ERROR",
|
||||||
HANDLE_ERROR_ERROR: "HANDLE_ERROR_ERROR",
|
HANDLE_ERROR_ERROR: "HANDLE_ERROR_ERROR",
|
||||||
@@ -47,10 +48,12 @@ export const TaskRunInternalError = z.object({
|
|||||||
"TASK_EXECUTION_FAILED",
|
"TASK_EXECUTION_FAILED",
|
||||||
"TASK_EXECUTION_ABORTED",
|
"TASK_EXECUTION_ABORTED",
|
||||||
"TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE",
|
"TASK_PROCESS_EXITED_WITH_NON_ZERO_CODE",
|
||||||
|
"TASK_PROCESS_SIGKILL_TIMEOUT",
|
||||||
"TASK_RUN_CANCELLED",
|
"TASK_RUN_CANCELLED",
|
||||||
"TASK_OUTPUT_ERROR",
|
"TASK_OUTPUT_ERROR",
|
||||||
"HANDLE_ERROR_ERROR",
|
"HANDLE_ERROR_ERROR",
|
||||||
"GRACEFUL_EXIT_TIMEOUT",
|
"GRACEFUL_EXIT_TIMEOUT",
|
||||||
|
"TASK_RUN_HEARTBEAT_TIMEOUT",
|
||||||
]),
|
]),
|
||||||
message: z.string().optional(),
|
message: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TaskRunExecution, TaskRunExecutionResult } from "./common";
|
import { TaskRunExecution, TaskRunExecutionResult, TaskRunFailedExecutionResult } from "./common";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EnvironmentType,
|
EnvironmentType,
|
||||||
Machine,
|
Machine,
|
||||||
ProdTaskRunExecution,
|
ProdTaskRunExecution,
|
||||||
ProdTaskRunExecutionPayload,
|
ProdTaskRunExecutionPayload,
|
||||||
TaskMetadataWithFilePath,
|
TaskMetadataWithFilePath,
|
||||||
|
TaskRunExecutionLazyAttemptPayload,
|
||||||
TaskRunExecutionPayload,
|
TaskRunExecutionPayload,
|
||||||
WaitReason,
|
WaitReason,
|
||||||
} from "./schemas";
|
} from "./schemas";
|
||||||
@@ -27,13 +29,17 @@ export const BackgroundWorkerServerMessages = z.discriminatedUnion("type", [
|
|||||||
version: z.string(),
|
version: z.string(),
|
||||||
machine: Machine,
|
machine: Machine,
|
||||||
// identifiers
|
// identifiers
|
||||||
id: z.string(), // attempt
|
id: z.string().optional(), // TODO: Remove this completely in a future release
|
||||||
envId: z.string(),
|
envId: z.string(),
|
||||||
envType: EnvironmentType,
|
envType: EnvironmentType,
|
||||||
orgId: z.string(),
|
orgId: z.string(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
runId: z.string(),
|
runId: z.string(),
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
type: z.literal("EXECUTE_RUN_LAZY_ATTEMPT"),
|
||||||
|
payload: TaskRunExecutionLazyAttemptPayload,
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type BackgroundWorkerServerMessages = z.infer<typeof BackgroundWorkerServerMessages>;
|
export type BackgroundWorkerServerMessages = z.infer<typeof BackgroundWorkerServerMessages>;
|
||||||
@@ -57,11 +63,21 @@ export const BackgroundWorkerClientMessages = z.discriminatedUnion("type", [
|
|||||||
completion: TaskRunExecutionResult,
|
completion: TaskRunExecutionResult,
|
||||||
execution: TaskRunExecution,
|
execution: TaskRunExecution,
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
type: z.literal("TASK_RUN_FAILED_TO_RUN"),
|
||||||
|
completion: TaskRunFailedExecutionResult,
|
||||||
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
type: z.literal("TASK_HEARTBEAT"),
|
type: z.literal("TASK_HEARTBEAT"),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
|
z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
type: z.literal("TASK_RUN_HEARTBEAT"),
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type BackgroundWorkerClientMessages = z.infer<typeof BackgroundWorkerClientMessages>;
|
export type BackgroundWorkerClientMessages = z.infer<typeof BackgroundWorkerClientMessages>;
|
||||||
@@ -78,6 +94,7 @@ export const clientWebsocketMessages = {
|
|||||||
READY_FOR_TASKS: z.object({
|
READY_FOR_TASKS: z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
backgroundWorkerId: z.string(),
|
backgroundWorkerId: z.string(),
|
||||||
|
inProgressRuns: z.string().array().optional(),
|
||||||
}),
|
}),
|
||||||
BACKGROUND_WORKER_DEPRECATED: z.object({
|
BACKGROUND_WORKER_DEPRECATED: z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
@@ -97,11 +114,17 @@ export const workerToChildMessages = {
|
|||||||
traceContext: z.record(z.unknown()),
|
traceContext: z.record(z.unknown()),
|
||||||
metadata: BackgroundWorkerProperties,
|
metadata: BackgroundWorkerProperties,
|
||||||
}),
|
}),
|
||||||
TASK_RUN_COMPLETED_NOTIFICATION: z.object({
|
TASK_RUN_COMPLETED_NOTIFICATION: z.discriminatedUnion("version", [
|
||||||
version: z.literal("v1").default("v1"),
|
z.object({
|
||||||
completion: TaskRunExecutionResult,
|
version: z.literal("v1"),
|
||||||
execution: TaskRunExecution,
|
completion: TaskRunExecutionResult,
|
||||||
}),
|
execution: TaskRunExecution,
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
version: z.literal("v2"),
|
||||||
|
completion: TaskRunExecutionResult,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
CLEANUP: z.object({
|
CLEANUP: z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
flush: z.boolean().default(false),
|
flush: z.boolean().default(false),
|
||||||
@@ -142,6 +165,10 @@ export const childToWorkerMessages = {
|
|||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
|
TASK_RUN_HEARTBEAT: z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
READY_TO_DISPOSE: z.undefined(),
|
READY_TO_DISPOSE: z.undefined(),
|
||||||
WAIT_FOR_DURATION: z.object({
|
WAIT_FOR_DURATION: z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
@@ -182,6 +209,12 @@ export const ProdChildToWorkerMessages = {
|
|||||||
id: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
TASK_RUN_HEARTBEAT: {
|
||||||
|
message: z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
READY_TO_DISPOSE: {
|
READY_TO_DISPOSE: {
|
||||||
message: z.undefined(),
|
message: z.undefined(),
|
||||||
},
|
},
|
||||||
@@ -247,11 +280,17 @@ export const ProdWorkerToChildMessages = {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
TASK_RUN_COMPLETED_NOTIFICATION: {
|
TASK_RUN_COMPLETED_NOTIFICATION: {
|
||||||
message: z.object({
|
message: z.discriminatedUnion("version", [
|
||||||
version: z.literal("v1").default("v1"),
|
z.object({
|
||||||
completion: TaskRunExecutionResult,
|
version: z.literal("v1"),
|
||||||
execution: TaskRunExecution,
|
completion: TaskRunExecutionResult,
|
||||||
}),
|
execution: TaskRunExecution,
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
version: z.literal("v2"),
|
||||||
|
completion: TaskRunExecutionResult,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
CLEANUP: {
|
CLEANUP: {
|
||||||
message: z.object({
|
message: z.object({
|
||||||
@@ -379,6 +418,18 @@ export const PlatformToProviderMessages = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CreateWorkerMessage = z.object({
|
||||||
|
projectRef: z.string(),
|
||||||
|
envId: z.string(),
|
||||||
|
deploymentId: z.string(),
|
||||||
|
metadata: z.object({
|
||||||
|
cliPackageVersion: z.string().optional(),
|
||||||
|
contentHash: z.string(),
|
||||||
|
packageVersion: z.string(),
|
||||||
|
tasks: TaskResource.array(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export const CoordinatorToPlatformMessages = {
|
export const CoordinatorToPlatformMessages = {
|
||||||
LOG: {
|
LOG: {
|
||||||
message: z.object({
|
message: z.object({
|
||||||
@@ -388,18 +439,15 @@ export const CoordinatorToPlatformMessages = {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
CREATE_WORKER: {
|
CREATE_WORKER: {
|
||||||
message: z.object({
|
message: z.discriminatedUnion("version", [
|
||||||
version: z.literal("v1").default("v1"),
|
CreateWorkerMessage.extend({
|
||||||
projectRef: z.string(),
|
version: z.literal("v1"),
|
||||||
envId: z.string(),
|
|
||||||
deploymentId: z.string(),
|
|
||||||
metadata: z.object({
|
|
||||||
cliPackageVersion: z.string().optional(),
|
|
||||||
contentHash: z.string(),
|
|
||||||
packageVersion: z.string(),
|
|
||||||
tasks: TaskResource.array(),
|
|
||||||
}),
|
}),
|
||||||
}),
|
CreateWorkerMessage.extend({
|
||||||
|
version: z.literal("v2"),
|
||||||
|
supportsLazyAttempts: z.boolean(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
callback: z.discriminatedUnion("success", [
|
callback: z.discriminatedUnion("success", [
|
||||||
z.object({
|
z.object({
|
||||||
success: z.literal(false),
|
success: z.literal(false),
|
||||||
@@ -409,6 +457,23 @@ export const CoordinatorToPlatformMessages = {
|
|||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
CREATE_TASK_RUN_ATTEMPT: {
|
||||||
|
message: z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
runId: z.string(),
|
||||||
|
envId: z.string(),
|
||||||
|
}),
|
||||||
|
callback: z.discriminatedUnion("success", [
|
||||||
|
z.object({
|
||||||
|
success: z.literal(false),
|
||||||
|
reason: z.string().optional(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
success: z.literal(true),
|
||||||
|
executionPayload: ProdTaskRunExecutionPayload,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
READY_FOR_EXECUTION: {
|
READY_FOR_EXECUTION: {
|
||||||
message: z.object({
|
message: z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
@@ -425,6 +490,24 @@ export const CoordinatorToPlatformMessages = {
|
|||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
READY_FOR_LAZY_ATTEMPT: {
|
||||||
|
message: z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
runId: z.string(),
|
||||||
|
envId: z.string(),
|
||||||
|
totalCompletions: z.number(),
|
||||||
|
}),
|
||||||
|
callback: z.discriminatedUnion("success", [
|
||||||
|
z.object({
|
||||||
|
success: z.literal(false),
|
||||||
|
reason: z.string().optional(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
success: z.literal(true),
|
||||||
|
lazyPayload: TaskRunExecutionLazyAttemptPayload,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
READY_FOR_RESUME: {
|
READY_FOR_RESUME: {
|
||||||
message: z.object({
|
message: z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
@@ -445,12 +528,24 @@ export const CoordinatorToPlatformMessages = {
|
|||||||
.optional(),
|
.optional(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
TASK_RUN_FAILED_TO_RUN: {
|
||||||
|
message: z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
completion: TaskRunFailedExecutionResult,
|
||||||
|
}),
|
||||||
|
},
|
||||||
TASK_HEARTBEAT: {
|
TASK_HEARTBEAT: {
|
||||||
message: z.object({
|
message: z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
attemptFriendlyId: z.string(),
|
attemptFriendlyId: z.string(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
TASK_RUN_HEARTBEAT: {
|
||||||
|
message: z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
runId: z.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
CHECKPOINT_CREATED: {
|
CHECKPOINT_CREATED: {
|
||||||
message: z.object({
|
message: z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
@@ -490,6 +585,17 @@ export const CoordinatorToPlatformMessages = {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
RUN_CRASHED: {
|
||||||
|
message: z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
runId: z.string(),
|
||||||
|
error: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
stack: z.string().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PlatformToCoordinatorMessages = {
|
export const PlatformToCoordinatorMessages = {
|
||||||
@@ -517,6 +623,13 @@ export const PlatformToCoordinatorMessages = {
|
|||||||
attemptFriendlyId: z.string(),
|
attemptFriendlyId: z.string(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
REQUEST_RUN_CANCELLATION: {
|
||||||
|
message: z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
runId: z.string(),
|
||||||
|
delayInMs: z.number().optional(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
READY_FOR_RETRY: {
|
READY_FOR_RETRY: {
|
||||||
message: z.object({
|
message: z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
@@ -563,6 +676,13 @@ export const SharedQueueToClientMessages = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const IndexTasksMessage = z.object({
|
||||||
|
version: z.literal("v1"),
|
||||||
|
deploymentId: z.string(),
|
||||||
|
tasks: TaskResource.array(),
|
||||||
|
packageVersion: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export const ProdWorkerToCoordinatorMessages = {
|
export const ProdWorkerToCoordinatorMessages = {
|
||||||
LOG: {
|
LOG: {
|
||||||
message: z.object({
|
message: z.object({
|
||||||
@@ -572,12 +692,15 @@ export const ProdWorkerToCoordinatorMessages = {
|
|||||||
callback: z.void(),
|
callback: z.void(),
|
||||||
},
|
},
|
||||||
INDEX_TASKS: {
|
INDEX_TASKS: {
|
||||||
message: z.object({
|
message: z.discriminatedUnion("version", [
|
||||||
version: z.literal("v1").default("v1"),
|
IndexTasksMessage.extend({
|
||||||
deploymentId: z.string(),
|
version: z.literal("v1"),
|
||||||
tasks: TaskResource.array(),
|
}),
|
||||||
packageVersion: z.string(),
|
IndexTasksMessage.extend({
|
||||||
}),
|
version: z.literal("v2"),
|
||||||
|
supportsLazyAttempts: z.boolean(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
callback: z.discriminatedUnion("success", [
|
callback: z.discriminatedUnion("success", [
|
||||||
z.object({
|
z.object({
|
||||||
success: z.literal(false),
|
success: z.literal(false),
|
||||||
@@ -594,6 +717,13 @@ export const ProdWorkerToCoordinatorMessages = {
|
|||||||
totalCompletions: z.number(),
|
totalCompletions: z.number(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
READY_FOR_LAZY_ATTEMPT: {
|
||||||
|
message: z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
runId: z.string(),
|
||||||
|
totalCompletions: z.number(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
READY_FOR_RESUME: {
|
READY_FOR_RESUME: {
|
||||||
message: z.object({
|
message: z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
@@ -630,6 +760,12 @@ export const ProdWorkerToCoordinatorMessages = {
|
|||||||
attemptFriendlyId: z.string(),
|
attemptFriendlyId: z.string(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
TASK_RUN_HEARTBEAT: {
|
||||||
|
message: z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
runId: z.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
TASK_RUN_COMPLETED: {
|
TASK_RUN_COMPLETED: {
|
||||||
message: z.object({
|
message: z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
@@ -641,6 +777,12 @@ export const ProdWorkerToCoordinatorMessages = {
|
|||||||
shouldExit: z.boolean(),
|
shouldExit: z.boolean(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
TASK_RUN_FAILED_TO_RUN: {
|
||||||
|
message: z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
completion: TaskRunFailedExecutionResult,
|
||||||
|
}),
|
||||||
|
},
|
||||||
WAIT_FOR_DURATION: {
|
WAIT_FOR_DURATION: {
|
||||||
message: z.object({
|
message: z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
@@ -686,8 +828,35 @@ export const ProdWorkerToCoordinatorMessages = {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
CREATE_TASK_RUN_ATTEMPT: {
|
||||||
|
message: z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
runId: z.string(),
|
||||||
|
}),
|
||||||
|
callback: z.discriminatedUnion("success", [
|
||||||
|
z.object({
|
||||||
|
success: z.literal(false),
|
||||||
|
reason: z.string().optional(),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
success: z.literal(true),
|
||||||
|
executionPayload: ProdTaskRunExecutionPayload,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
UNRECOVERABLE_ERROR: {
|
||||||
|
message: z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
error: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
stack: z.string().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: The coordinator can only safely use v1 worker messages, higher versions will need a new flag, e.g. SUPPORTS_VERSIONED_MESSAGES
|
||||||
export const CoordinatorToProdWorkerMessages = {
|
export const CoordinatorToProdWorkerMessages = {
|
||||||
RESUME_AFTER_DEPENDENCY: {
|
RESUME_AFTER_DEPENDENCY: {
|
||||||
message: z.object({
|
message: z.object({
|
||||||
@@ -709,6 +878,12 @@ export const CoordinatorToProdWorkerMessages = {
|
|||||||
executionPayload: ProdTaskRunExecutionPayload,
|
executionPayload: ProdTaskRunExecutionPayload,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
EXECUTE_TASK_RUN_LAZY_ATTEMPT: {
|
||||||
|
message: z.object({
|
||||||
|
version: z.literal("v1").default("v1"),
|
||||||
|
lazyPayload: TaskRunExecutionLazyAttemptPayload,
|
||||||
|
}),
|
||||||
|
},
|
||||||
REQUEST_ATTEMPT_CANCELLATION: {
|
REQUEST_ATTEMPT_CANCELLATION: {
|
||||||
message: z.object({
|
message: z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.literal("v1").default("v1"),
|
||||||
@@ -716,9 +891,15 @@ export const CoordinatorToProdWorkerMessages = {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
REQUEST_EXIT: {
|
REQUEST_EXIT: {
|
||||||
message: z.object({
|
message: z.discriminatedUnion("version", [
|
||||||
version: z.literal("v1").default("v1"),
|
z.object({
|
||||||
}),
|
version: z.literal("v1"),
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
version: z.literal("v2"),
|
||||||
|
delayInMs: z.number().optional(),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
READY_FOR_RETRY: {
|
READY_FOR_RETRY: {
|
||||||
message: z.object({
|
message: z.object({
|
||||||
|
|||||||
@@ -224,3 +224,13 @@ export type ResolvedConfig = RequireKeys<
|
|||||||
export const WaitReason = z.enum(["WAIT_FOR_DURATION", "WAIT_FOR_TASK", "WAIT_FOR_BATCH"]);
|
export const WaitReason = z.enum(["WAIT_FOR_DURATION", "WAIT_FOR_TASK", "WAIT_FOR_BATCH"]);
|
||||||
|
|
||||||
export type WaitReason = z.infer<typeof WaitReason>;
|
export type WaitReason = z.infer<typeof WaitReason>;
|
||||||
|
|
||||||
|
export const TaskRunExecutionLazyAttemptPayload = z.object({
|
||||||
|
runId: z.string(),
|
||||||
|
messageId: z.string(),
|
||||||
|
isTest: z.boolean(),
|
||||||
|
traceContext: z.record(z.unknown()),
|
||||||
|
environment: z.record(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TaskRunExecutionLazyAttemptPayload = z.infer<typeof TaskRunExecutionLazyAttemptPayload>;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { Socket } from "socket.io-client";
|
import type { Socket } from "socket.io-client";
|
||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
import { z } from "zod";
|
import { ZodError, z } from "zod";
|
||||||
import { EventEmitterLike, ZodMessageValueSchema } from "./zodMessageHandler";
|
import { EventEmitterLike, ZodMessageValueSchema } from "./zodMessageHandler";
|
||||||
import { LogLevel, SimpleStructuredLogger, StructuredLogger } from "./utils/structuredLogger";
|
import { LogLevel, SimpleStructuredLogger, StructuredLogger } from "./utils/structuredLogger";
|
||||||
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
|
||||||
export interface ZodSocketMessageCatalogSchema {
|
export interface ZodSocketMessageCatalogSchema {
|
||||||
[key: string]:
|
[key: string]:
|
||||||
@@ -81,7 +82,7 @@ export type MessagesFromSocketCatalog<TMessageCatalog extends ZodSocketMessageCa
|
|||||||
}[keyof TMessageCatalog];
|
}[keyof TMessageCatalog];
|
||||||
|
|
||||||
const messageSchema = z.object({
|
const messageSchema = z.object({
|
||||||
version: z.literal("v1").default("v1"),
|
version: z.string(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
payload: z.unknown(),
|
payload: z.unknown(),
|
||||||
});
|
});
|
||||||
@@ -127,10 +128,22 @@ export class ZodSocketMessageHandler<TRPCCatalog extends ZodSocketMessageCatalog
|
|||||||
throw new Error(`Unknown message type: ${parsedMessage.data.type}`);
|
throw new Error(`Unknown message type: ${parsedMessage.data.type}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedPayload = schema.safeParse(parsedMessage.data.payload);
|
const messageWithVersion = {
|
||||||
|
version: parsedMessage.data.version,
|
||||||
|
...(typeof parsedMessage.data.payload === "object" ? parsedMessage.data.payload : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsedPayload = schema.safeParse(messageWithVersion);
|
||||||
|
|
||||||
if (!parsedPayload.success) {
|
if (!parsedPayload.success) {
|
||||||
throw new Error(`Failed to parse message payload: ${JSON.stringify(parsedPayload.error)}`);
|
console.error("Failed to parse message payload", {
|
||||||
|
message,
|
||||||
|
payload: messageWithVersion,
|
||||||
|
});
|
||||||
|
|
||||||
|
throw parsedPayload.error instanceof ZodError
|
||||||
|
? fromZodError(parsedPayload.error)
|
||||||
|
: parsedPayload.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -166,7 +179,15 @@ export class ZodSocketMessageHandler<TRPCCatalog extends ZodSocketMessageCatalog
|
|||||||
ack = await this.handleMessage({ type: eventName, version, payload });
|
ack = await this.handleMessage({ type: eventName, version, payload });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Error while handling message", { error });
|
log.error("Error while handling message", {
|
||||||
|
error:
|
||||||
|
error instanceof Error
|
||||||
|
? {
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
}
|
||||||
|
: error,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default defineConfig({
|
|||||||
"./src/v3/dev/index.ts",
|
"./src/v3/dev/index.ts",
|
||||||
"./src/v3/prod/index.ts",
|
"./src/v3/prod/index.ts",
|
||||||
"./src/v3/workers/index.ts",
|
"./src/v3/workers/index.ts",
|
||||||
|
"./src/v3/zodfetch.ts",
|
||||||
],
|
],
|
||||||
external: ["node:stream"],
|
external: ["node:stream"],
|
||||||
});
|
});
|
||||||
|
|||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "BackgroundWorker" ADD COLUMN "supportsLazyAttempts" BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -1563,6 +1563,8 @@ model BackgroundWorker {
|
|||||||
|
|
||||||
deployment WorkerDeployment?
|
deployment WorkerDeployment?
|
||||||
|
|
||||||
|
supportsLazyAttempts Boolean @default(false)
|
||||||
|
|
||||||
@@unique([projectId, runtimeEnvironmentId, version])
|
@@unique([projectId, runtimeEnvironmentId, version])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,394 @@
|
|||||||
|
import { logger, task, wait } from "@trigger.dev/sdk/v3";
|
||||||
|
|
||||||
|
export const lazyImmediate = task({
|
||||||
|
id: "lazy-immediate",
|
||||||
|
run: async (payload: { forceError?: boolean }) => {
|
||||||
|
logger.info("Log something", { payload });
|
||||||
|
logger.info("Log something else", { payload });
|
||||||
|
|
||||||
|
if (payload.forceError) {
|
||||||
|
throw new Error("Forced error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "This is a message",
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lazyWait = task({
|
||||||
|
id: "lazy-wait",
|
||||||
|
run: async (payload: { forceError?: boolean; delayInSeconds?: number }) => {
|
||||||
|
logger.info("Log something", { payload });
|
||||||
|
|
||||||
|
await wait.for({ seconds: payload.delayInSeconds ?? 1 });
|
||||||
|
|
||||||
|
logger.info("Log something else", { payload });
|
||||||
|
|
||||||
|
if (payload.forceError) {
|
||||||
|
throw new Error("Forced error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "This is a message",
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lazySingleDependency = task({
|
||||||
|
id: "lazy-single-dependency",
|
||||||
|
run: async (payload: {
|
||||||
|
forceError?: boolean;
|
||||||
|
forceChildError?: boolean;
|
||||||
|
delayInSeconds?: number;
|
||||||
|
}) => {
|
||||||
|
logger.info("Log something", { payload });
|
||||||
|
|
||||||
|
const result = await lazyWait.triggerAndWait({
|
||||||
|
delayInSeconds: payload.delayInSeconds,
|
||||||
|
forceError: payload.forceChildError,
|
||||||
|
});
|
||||||
|
logger.info("Single result", { result });
|
||||||
|
|
||||||
|
logger.info("Log something else", { payload });
|
||||||
|
|
||||||
|
if (payload.forceError) {
|
||||||
|
throw new Error("Forced error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "This is a message",
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lazyBatchDependency = task({
|
||||||
|
id: "lazy-batch-dependency",
|
||||||
|
run: async (payload: {
|
||||||
|
forceError?: boolean;
|
||||||
|
forceChildError?: boolean;
|
||||||
|
delayInSeconds?: number;
|
||||||
|
}) => {
|
||||||
|
logger.info("Log something", { payload });
|
||||||
|
|
||||||
|
const results = await lazyWait.batchTriggerAndWait([
|
||||||
|
{ payload: { delayInSeconds: payload.delayInSeconds, forceError: payload.forceChildError } },
|
||||||
|
{ payload: { delayInSeconds: payload.delayInSeconds, forceError: payload.forceChildError } },
|
||||||
|
]);
|
||||||
|
logger.info("Batch results", { results });
|
||||||
|
|
||||||
|
logger.info("Log something else", { payload });
|
||||||
|
|
||||||
|
if (payload.forceError) {
|
||||||
|
throw new Error("Forced error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "This is a message",
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lazyConsecutiveWaits = task({
|
||||||
|
id: "lazy-consecutive-waits",
|
||||||
|
run: async (payload: {
|
||||||
|
forceError?: boolean;
|
||||||
|
forceChildError?: boolean;
|
||||||
|
delayInSeconds?: number;
|
||||||
|
}) => {
|
||||||
|
logger.info("Log something", { payload });
|
||||||
|
|
||||||
|
await wait.for({ seconds: payload.delayInSeconds ?? 1 });
|
||||||
|
|
||||||
|
logger.info("Log something else", { payload });
|
||||||
|
|
||||||
|
await wait.for({ seconds: payload.delayInSeconds ?? 1 });
|
||||||
|
|
||||||
|
logger.info("Log something else again", { payload });
|
||||||
|
|
||||||
|
if (payload.forceError) {
|
||||||
|
throw new Error("Forced error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "This is a message",
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lazyConsecutiveDependencies = task({
|
||||||
|
id: "lazy-consecutive-dependencies",
|
||||||
|
run: async (payload: {
|
||||||
|
forceError?: boolean;
|
||||||
|
forceChildError?: boolean;
|
||||||
|
delayInSeconds?: number;
|
||||||
|
}) => {
|
||||||
|
logger.info("Log something", { payload });
|
||||||
|
|
||||||
|
const result = await lazyWait.triggerAndWait({
|
||||||
|
delayInSeconds: payload.delayInSeconds,
|
||||||
|
forceError: payload.forceChildError,
|
||||||
|
});
|
||||||
|
logger.info("Single result #1", { result });
|
||||||
|
|
||||||
|
logger.info("Log something else", { payload });
|
||||||
|
|
||||||
|
const result2 = await lazyWait.triggerAndWait({
|
||||||
|
delayInSeconds: payload.delayInSeconds,
|
||||||
|
forceError: payload.forceChildError,
|
||||||
|
});
|
||||||
|
logger.info("Single result #2", { result2 });
|
||||||
|
|
||||||
|
logger.info("Log something else again", { payload });
|
||||||
|
|
||||||
|
if (payload.forceError) {
|
||||||
|
throw new Error("Forced error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "This is a message",
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lazyConsecutiveBatchDependencies = task({
|
||||||
|
id: "lazy-consecutive-batch-dependencies",
|
||||||
|
run: async (payload: {
|
||||||
|
forceError?: boolean;
|
||||||
|
forceChildError?: boolean;
|
||||||
|
delayInSeconds?: number;
|
||||||
|
}) => {
|
||||||
|
logger.info("Log something", { payload });
|
||||||
|
|
||||||
|
const results = await lazyWait.batchTriggerAndWait([
|
||||||
|
{ payload: { delayInSeconds: payload.delayInSeconds, forceError: payload.forceChildError } },
|
||||||
|
{ payload: { delayInSeconds: payload.delayInSeconds, forceError: payload.forceChildError } },
|
||||||
|
]);
|
||||||
|
logger.info("Batch results #1", { results });
|
||||||
|
|
||||||
|
logger.info("Log something else", { payload });
|
||||||
|
|
||||||
|
const results2 = await lazyWait.batchTriggerAndWait([
|
||||||
|
{ payload: { delayInSeconds: payload.delayInSeconds, forceError: payload.forceChildError } },
|
||||||
|
{ payload: { delayInSeconds: payload.delayInSeconds, forceError: payload.forceChildError } },
|
||||||
|
]);
|
||||||
|
logger.info("Batch results #2", { results2 });
|
||||||
|
|
||||||
|
logger.info("Log something else again", { payload });
|
||||||
|
|
||||||
|
if (payload.forceError) {
|
||||||
|
throw new Error("Forced error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "This is a message",
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lazyWaitThenSingleDependency = task({
|
||||||
|
id: "lazy-wait-then-single-dependency",
|
||||||
|
run: async (payload: {
|
||||||
|
forceError?: boolean;
|
||||||
|
forceChildError?: boolean;
|
||||||
|
delayInSeconds?: number;
|
||||||
|
}) => {
|
||||||
|
logger.info("Log something", { payload });
|
||||||
|
|
||||||
|
await wait.for({ seconds: payload.delayInSeconds ?? 1 });
|
||||||
|
|
||||||
|
logger.info("Log something else", { payload });
|
||||||
|
|
||||||
|
const result = await lazyWait.triggerAndWait({
|
||||||
|
delayInSeconds: payload.delayInSeconds,
|
||||||
|
forceError: payload.forceChildError,
|
||||||
|
});
|
||||||
|
logger.info("Single result", { result });
|
||||||
|
|
||||||
|
logger.info("Log something else again", { payload });
|
||||||
|
|
||||||
|
if (payload.forceError) {
|
||||||
|
throw new Error("Forced error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "This is a message",
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lazyWaitThenBatchDependency = task({
|
||||||
|
id: "lazy-wait-then-batch-dependency",
|
||||||
|
run: async (payload: {
|
||||||
|
forceError?: boolean;
|
||||||
|
forceChildError?: boolean;
|
||||||
|
delayInSeconds?: number;
|
||||||
|
}) => {
|
||||||
|
logger.info("Log something", { payload });
|
||||||
|
|
||||||
|
await wait.for({ seconds: payload.delayInSeconds ?? 1 });
|
||||||
|
|
||||||
|
logger.info("Log something else", { payload });
|
||||||
|
|
||||||
|
const results = await lazyWait.batchTriggerAndWait([
|
||||||
|
{ payload: { delayInSeconds: payload.delayInSeconds, forceError: payload.forceChildError } },
|
||||||
|
{ payload: { delayInSeconds: payload.delayInSeconds, forceError: payload.forceChildError } },
|
||||||
|
]);
|
||||||
|
logger.info("Batch results", { results });
|
||||||
|
|
||||||
|
logger.info("Log something else again", { payload });
|
||||||
|
|
||||||
|
if (payload.forceError) {
|
||||||
|
throw new Error("Forced error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "This is a message",
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lazySingleDependencyThenWait = task({
|
||||||
|
id: "lazy-single-dependency-then-wait",
|
||||||
|
run: async (payload: {
|
||||||
|
forceError?: boolean;
|
||||||
|
forceChildError?: boolean;
|
||||||
|
delayInSeconds?: number;
|
||||||
|
}) => {
|
||||||
|
logger.info("Log something", { payload });
|
||||||
|
|
||||||
|
const result = await lazyWait.triggerAndWait({
|
||||||
|
delayInSeconds: payload.delayInSeconds,
|
||||||
|
forceError: payload.forceChildError,
|
||||||
|
});
|
||||||
|
logger.info("Single result", { result });
|
||||||
|
|
||||||
|
logger.info("Log something else", { payload });
|
||||||
|
|
||||||
|
await wait.for({ seconds: payload.delayInSeconds ?? 1 });
|
||||||
|
|
||||||
|
logger.info("Log something else again", { payload });
|
||||||
|
|
||||||
|
if (payload.forceError) {
|
||||||
|
throw new Error("Forced error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "This is a message",
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lazySingleDependencyThenBatch = task({
|
||||||
|
id: "lazy-single-dependency-then-batch",
|
||||||
|
run: async (payload: {
|
||||||
|
forceError?: boolean;
|
||||||
|
forceChildError?: boolean;
|
||||||
|
delayInSeconds?: number;
|
||||||
|
}) => {
|
||||||
|
logger.info("Log something", { payload });
|
||||||
|
|
||||||
|
const result = await lazyWait.triggerAndWait({
|
||||||
|
delayInSeconds: payload.delayInSeconds,
|
||||||
|
forceError: payload.forceChildError,
|
||||||
|
});
|
||||||
|
logger.info("Single result", { result });
|
||||||
|
|
||||||
|
logger.info("Log something else", { payload });
|
||||||
|
|
||||||
|
const results = await lazyWait.batchTriggerAndWait([
|
||||||
|
{ payload: { delayInSeconds: payload.delayInSeconds, forceError: payload.forceChildError } },
|
||||||
|
{ payload: { delayInSeconds: payload.delayInSeconds, forceError: payload.forceChildError } },
|
||||||
|
]);
|
||||||
|
logger.info("Batch results", { results });
|
||||||
|
|
||||||
|
logger.info("Log something else again", { payload });
|
||||||
|
|
||||||
|
if (payload.forceError) {
|
||||||
|
throw new Error("Forced error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "This is a message",
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lazyBatchDependencyThenWait = task({
|
||||||
|
id: "lazy-batch-dependency-then-wait",
|
||||||
|
run: async (payload: {
|
||||||
|
forceError?: boolean;
|
||||||
|
forceChildError?: boolean;
|
||||||
|
delayInSeconds?: number;
|
||||||
|
}) => {
|
||||||
|
logger.info("Log something", { payload });
|
||||||
|
|
||||||
|
const results = await lazyWait.batchTriggerAndWait([
|
||||||
|
{ payload: { delayInSeconds: payload.delayInSeconds, forceError: payload.forceChildError } },
|
||||||
|
{ payload: { delayInSeconds: payload.delayInSeconds, forceError: payload.forceChildError } },
|
||||||
|
]);
|
||||||
|
logger.info("Batch results", { results });
|
||||||
|
|
||||||
|
logger.info("Log something else", { payload });
|
||||||
|
|
||||||
|
await wait.for({ seconds: payload.delayInSeconds ?? 1 });
|
||||||
|
|
||||||
|
logger.info("Log something else again", { payload });
|
||||||
|
|
||||||
|
if (payload.forceError) {
|
||||||
|
throw new Error("Forced error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "This is a message",
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lazyBatchDependencyThenSingle = task({
|
||||||
|
id: "lazy-batch-dependency-then-single",
|
||||||
|
run: async (payload: {
|
||||||
|
forceError?: boolean;
|
||||||
|
forceChildError?: boolean;
|
||||||
|
delayInSeconds?: number;
|
||||||
|
}) => {
|
||||||
|
logger.info("Log something", { payload });
|
||||||
|
|
||||||
|
const results = await lazyWait.batchTriggerAndWait([
|
||||||
|
{ payload: { delayInSeconds: payload.delayInSeconds, forceError: payload.forceChildError } },
|
||||||
|
{ payload: { delayInSeconds: payload.delayInSeconds, forceError: payload.forceChildError } },
|
||||||
|
]);
|
||||||
|
logger.info("Batch results", { results });
|
||||||
|
|
||||||
|
logger.info("Log something else", { payload });
|
||||||
|
|
||||||
|
const result = await lazyWait.triggerAndWait({
|
||||||
|
delayInSeconds: payload.delayInSeconds,
|
||||||
|
forceError: payload.forceChildError,
|
||||||
|
});
|
||||||
|
logger.info("Single result", { result });
|
||||||
|
|
||||||
|
logger.info("Log something else again", { payload });
|
||||||
|
|
||||||
|
if (payload.forceError) {
|
||||||
|
throw new Error("Forced error");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "This is a message",
|
||||||
|
payload,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -3,7 +3,7 @@ import { logger, task } from "@trigger.dev/sdk/v3";
|
|||||||
export const longRunning = task({
|
export const longRunning = task({
|
||||||
id: "long-running",
|
id: "long-running",
|
||||||
run: async (payload: { message: string }) => {
|
run: async (payload: { message: string }) => {
|
||||||
logger.info("Long running payloadd", { payload });
|
logger.info("Long running payloadddd", { payload });
|
||||||
|
|
||||||
// Wait for 3 minutes
|
// Wait for 3 minutes
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3 * 60 * 1000));
|
await new Promise((resolve) => setTimeout(resolve, 3 * 60 * 1000));
|
||||||
@@ -19,7 +19,22 @@ export const longRunningParent = task({
|
|||||||
run: async (payload: { message: string }) => {
|
run: async (payload: { message: string }) => {
|
||||||
logger.info("Long running parent", { payload });
|
logger.info("Long running parent", { payload });
|
||||||
|
|
||||||
await longRunning.triggerAndWait({ message: "child" });
|
const result = await longRunning.triggerAndWait({ message: "child" });
|
||||||
|
|
||||||
|
return {
|
||||||
|
finished: new Date().toISOString(),
|
||||||
|
result,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const longRunningWithDotInName = task({
|
||||||
|
id: "long.running.with.dot",
|
||||||
|
run: async (payload: { message: string }) => {
|
||||||
|
logger.info("Long running payloadd", { payload });
|
||||||
|
|
||||||
|
// Wait for 3 minutes
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 3 * 60 * 1000));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
finished: new Date().toISOString(),
|
finished: new Date().toISOString(),
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const config: TriggerConfig = {
|
|||||||
retries: {
|
retries: {
|
||||||
enabledInDev: true,
|
enabledInDev: true,
|
||||||
default: {
|
default: {
|
||||||
maxAttempts: 3,
|
maxAttempts: 4,
|
||||||
minTimeoutInMs: 1000,
|
minTimeoutInMs: 1000,
|
||||||
maxTimeoutInMs: 10000,
|
maxTimeoutInMs: 10000,
|
||||||
factor: 2,
|
factor: 2,
|
||||||
|
|||||||
Reference in New Issue
Block a user