Files
2026-07-13 13:32:57 +08:00

2216 lines
65 KiB
TypeScript

import type { Context, Span, SpanOptions } from "@opentelemetry/api";
import { ROOT_CONTEXT, SpanKind, SpanStatusCode, context, trace } from "@opentelemetry/api";
import type {
AckCallbackResult,
MachinePreset,
V3ProdTaskRunExecution,
V3ProdTaskRunExecutionPayload,
TaskRunError,
TaskRunExecution,
TaskRunExecutionLazyAttemptPayload,
TaskRunExecutionResult,
TaskRunFailedExecutionResult,
TaskRunSuccessfulExecutionResult,
serverWebsocketMessages,
} from "@trigger.dev/core/v3";
import { TaskRunErrorCodes, parsePacket, SemanticInternalAttributes } from "@trigger.dev/core/v3";
import type { ZodMessageSender } from "@trigger.dev/core/v3/zodMessageHandler";
import type {
BackgroundWorker,
BackgroundWorkerTask,
Prisma,
TaskRunStatus,
} from "@trigger.dev/database";
import { z } from "zod";
import { $replica, prisma } from "~/db.server";
import { env } from "~/env.server";
import { findEnvironmentById } from "~/models/runtimeEnvironment.server";
import { findQueueInEnvironment, sanitizeQueueName } from "~/models/taskQueue.server";
import { generateJWTTokenForEnvironment } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { singleton } from "~/utils/singleton";
import { marqs } from "~/v3/marqs/index.server";
import type { RuntimeEnvironmentForEnvRepo } from "../environmentVariables/environmentVariablesRepository.server";
import {
RuntimeEnvironmentForEnvRepoPayload,
resolveVariablesForEnvironment,
} from "../environmentVariables/environmentVariablesRepository.server";
import type { EnvironmentVariable } from "../environmentVariables/repository";
import { FailedTaskRunService } from "../failedTaskRun.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import { socketIo } from "../handleSocketIo.server";
import { machinePresetFromConfig, machinePresetFromRun } from "../machinePresets.server";
import {
findCurrentWorkerDeployment,
getWorkerDeploymentFromWorker,
getWorkerDeploymentFromWorkerTask,
} from "../models/workerDeployment.server";
import { CrashTaskRunService } from "../services/crashTaskRun.server";
import { CreateTaskRunAttemptService } from "../services/createTaskRunAttempt.server";
import { RestoreCheckpointService } from "../services/restoreCheckpoint.server";
import {
FINAL_ATTEMPT_STATUSES,
FINAL_RUN_STATUSES,
isFinalAttemptStatus,
isFinalRunStatus,
} from "../taskStatus";
import { tracer } from "../tracer.server";
import { getMaxDuration } from "../utils/maxDuration";
import type { MessagePayload } from "./types";
const WithTraceContext = z.object({
traceparent: z.string().optional(),
tracestate: z.string().optional(),
});
export const SharedQueueExecuteMessageBody = WithTraceContext.extend({
type: z.literal("EXECUTE"),
taskIdentifier: z.string(),
checkpointEventId: z.string().optional(),
retryCheckpointsDisabled: z.boolean().optional(),
});
export type SharedQueueExecuteMessageBody = z.infer<typeof SharedQueueExecuteMessageBody>;
export const SharedQueueResumeMessageBody = WithTraceContext.extend({
type: z.literal("RESUME"),
completedAttemptIds: z.string().array(),
resumableAttemptId: z.string(),
checkpointEventId: z.string().optional(),
});
export type SharedQueueResumeMessageBody = z.infer<typeof SharedQueueResumeMessageBody>;
export const SharedQueueResumeAfterDurationMessageBody = WithTraceContext.extend({
type: z.literal("RESUME_AFTER_DURATION"),
resumableAttemptId: z.string(),
checkpointEventId: z.string(),
});
export type SharedQueueResumeAfterDurationMessageBody = z.infer<
typeof SharedQueueResumeAfterDurationMessageBody
>;
export const SharedQueueFailMessageBody = WithTraceContext.extend({
type: z.literal("FAIL"),
reason: z.string(),
});
export type SharedQueueFailMessageBody = z.infer<typeof SharedQueueFailMessageBody>;
export const SharedQueueMessageBody = z.discriminatedUnion("type", [
SharedQueueExecuteMessageBody,
SharedQueueResumeMessageBody,
SharedQueueResumeAfterDurationMessageBody,
SharedQueueFailMessageBody,
]);
export type SharedQueueMessageBody = z.infer<typeof SharedQueueMessageBody>;
type BackgroundWorkerWithTasks = BackgroundWorker & { tasks: BackgroundWorkerTask[] };
export type SharedQueueConsumerOptions = {
maximumItemsPerTrace?: number;
traceTimeoutSeconds?: number;
nextTickInterval?: number;
interval?: number;
};
type HandleMessageAction = "ack_and_do_more_work" | "nack" | "nack_and_do_more_work" | "noop";
type DoWorkInternalResult = {
reason: string;
outcome: "execution" | "retry_with_nack" | "fail_with_ack" | "noop";
attrs?: Record<string, string | number | boolean | undefined>;
error?: Error | string;
interval?: number;
action?: HandleMessageAction;
};
type HandleMessageResult = {
action: HandleMessageAction;
interval?: number;
retryInMs?: number;
reason?: string;
attrs?: Record<string, string | number | boolean | undefined>;
error?: Error | string;
};
export class SharedQueueConsumer {
private _backgroundWorkers: Map<string, BackgroundWorkerWithTasks> = new Map();
private _deprecatedWorkers: Map<string, BackgroundWorkerWithTasks> = new Map();
private _enabled = false;
private _options: Required<SharedQueueConsumerOptions>;
private _perTraceCountdown: number | undefined;
private _traceStartedAt: Date | undefined;
private _currentSpanContext: Context | undefined;
private _reasonStats: Record<string, number> = {};
private _actionStats: Record<string, number> = {};
private _outcomeStats: Record<string, number> = {};
private _currentSpan: Span | undefined;
private _endSpanInNextIteration = false;
private _tasks = sharedQueueTasks;
private _id: string;
private _connectedAt: Date;
private _iterationsCount = 0;
private _totalIterationsCount = 0;
private _runningDurationInMs = 0;
private _currentMessage: MessagePayload | undefined;
private _currentMessageData: SharedQueueMessageBody | undefined;
private _stopWorkerQueueConsumer?: () => void;
constructor(
private _providerSender: ZodMessageSender<typeof serverWebsocketMessages>,
options: SharedQueueConsumerOptions = {}
) {
this._options = {
maximumItemsPerTrace: options.maximumItemsPerTrace ?? 500,
traceTimeoutSeconds: options.traceTimeoutSeconds ?? 1,
nextTickInterval: options.nextTickInterval ?? 1000, // 1 second
interval: options.interval ?? 100, // 100ms
};
this._id = generateFriendlyId("shared-queue", 6);
this._connectedAt = new Date();
}
// This method is called when a background worker is deprecated and will no longer be used unless a run is locked to it
public async deprecateBackgroundWorker(id: string) {
const backgroundWorker = this._backgroundWorkers.get(id);
if (!backgroundWorker) {
return;
}
this._deprecatedWorkers.set(id, backgroundWorker);
this._backgroundWorkers.delete(id);
}
public async registerBackgroundWorker(id: string, envId?: string) {
if (!envId) {
logger.error("Environment ID is required for background worker registration", {
backgroundWorkerId: id,
});
return;
}
const backgroundWorker = await prisma.backgroundWorker.findFirst({
where: {
friendlyId: id,
runtimeEnvironmentId: envId,
},
include: {
tasks: true,
},
});
if (!backgroundWorker) {
return;
}
this._backgroundWorkers.set(backgroundWorker.id, backgroundWorker);
logger.debug("Registered background worker", { backgroundWorker: backgroundWorker.id });
// Start reading from the queue if we haven't already
this.#enable();
}
public async start() {
this.#enable();
}
public async stop(reason: string = "Provider disconnected") {
if (!this._enabled) {
return;
}
console.log("❌ Stopping the SharedQueueConsumer");
this._stopWorkerQueueConsumer?.();
logger.debug("Stopping shared queue consumer");
this._enabled = false;
if (this._currentSpan) {
this._currentSpan.end();
}
}
#enable() {
if (this._enabled) {
return;
}
this._enabled = true;
this._perTraceCountdown = this._options.maximumItemsPerTrace;
this._traceStartedAt = new Date();
this._reasonStats = {};
this._actionStats = {};
this._outcomeStats = {};
this._stopWorkerQueueConsumer = marqs?.startSharedWorkerQueueConsumer(this._id);
console.log("✅ Started the SharedQueueConsumer");
this.#doWork().finally(() => {});
}
#endCurrentSpan() {
if (this._currentSpan) {
for (const [reason, count] of Object.entries(this._reasonStats)) {
this._currentSpan.setAttribute(`reasons_${reason}`, count);
}
for (const [action, count] of Object.entries(this._actionStats)) {
this._currentSpan.setAttribute(`actions_${action}`, count);
}
for (const [outcome, count] of Object.entries(this._outcomeStats)) {
this._currentSpan.setAttribute(`outcomes_${outcome}`, count);
}
this._currentSpan.end();
}
}
async #doWork() {
if (!this._enabled) {
this.#endCurrentSpan();
return;
}
// Check if the trace has expired
if (
this._perTraceCountdown === 0 ||
Date.now() - this._traceStartedAt!.getTime() > this._options.traceTimeoutSeconds * 1000 ||
this._currentSpanContext === undefined ||
this._endSpanInNextIteration
) {
this.#endCurrentSpan();
const traceDurationInMs = this._traceStartedAt
? Date.now() - this._traceStartedAt.getTime()
: undefined;
const iterationsPerSecond = traceDurationInMs
? this._iterationsCount / (traceDurationInMs / 1000)
: undefined;
// Create a new trace
this._currentSpan = tracer.startSpan(
"SharedQueueConsumer.doWork()",
{
kind: SpanKind.CONSUMER,
attributes: {
id: this._id,
iterations: this._iterationsCount,
total_iterations: this._totalIterationsCount,
options_maximumItemsPerTrace: this._options.maximumItemsPerTrace,
options_nextTickInterval: this._options.nextTickInterval,
options_interval: this._options.interval,
connected_at: this._connectedAt.toISOString(),
consumer_age_in_seconds: (Date.now() - this._connectedAt.getTime()) / 1000,
do_work_internal_per_second: this._iterationsCount / (this._runningDurationInMs / 1000),
running_duration_ms: this._runningDurationInMs,
trace_timeout_in_seconds: this._options.traceTimeoutSeconds,
trace_duration_ms: traceDurationInMs,
iterations_per_second: iterationsPerSecond,
iterations_per_minute: iterationsPerSecond ? iterationsPerSecond * 60 : undefined,
},
},
ROOT_CONTEXT
);
logger.debug("SharedQueueConsumer starting new trace", {
reasonStats: this._reasonStats,
actionStats: this._actionStats,
outcomeStats: this._outcomeStats,
iterationCount: this._iterationsCount,
consumerId: this._id,
});
// Get the span trace context
this._currentSpanContext = trace.setSpan(ROOT_CONTEXT, this._currentSpan);
this._perTraceCountdown = this._options.maximumItemsPerTrace;
this._traceStartedAt = new Date();
this._reasonStats = {};
this._actionStats = {};
this._outcomeStats = {};
this._iterationsCount = 0;
this._runningDurationInMs = 0;
this._endSpanInNextIteration = false;
}
return context.with(this._currentSpanContext ?? ROOT_CONTEXT, async () => {
await tracer.startActiveSpan("doWorkInternal()", async (span) => {
let nextInterval = this._options.interval;
span.setAttributes({
id: this._id,
total_iterations: this._totalIterationsCount,
iterations: this._iterationsCount,
});
const startAt = performance.now();
try {
const result = await this.#doWorkInternal();
if (result.reason !== "no_message_dequeued") {
logger.debug("SharedQueueConsumer doWorkInternal result", { result });
}
this._reasonStats[result.reason] = (this._reasonStats[result.reason] ?? 0) + 1;
this._outcomeStats[result.outcome] = (this._outcomeStats[result.outcome] ?? 0) + 1;
if (result.action) {
this._actionStats[result.action] = (this._actionStats[result.action] ?? 0) + 1;
}
span.setAttribute("reason", result.reason);
if (result.attrs) {
for (const [key, value] of Object.entries(result.attrs)) {
if (value) {
span.setAttribute(key, value);
}
}
}
if (result.error) {
span.recordException(result.error);
span.setStatus({ code: SpanStatusCode.ERROR });
this._currentSpan?.recordException(result.error);
this._currentSpan?.setStatus({ code: SpanStatusCode.ERROR });
this._endSpanInNextIteration = true;
}
if (typeof result.interval === "number") {
nextInterval = Math.max(result.interval, 0); // Cannot be negative
}
span.setAttribute("nextInterval", nextInterval);
} catch (error) {
if (error instanceof Error) {
this._currentSpan?.recordException(error);
} else {
this._currentSpan?.recordException(new Error(String(error)));
}
this._endSpanInNextIteration = true;
} finally {
this._runningDurationInMs = this._runningDurationInMs + (performance.now() - startAt);
this._iterationsCount++;
this._totalIterationsCount++;
this._perTraceCountdown = this._perTraceCountdown! - 1;
span.end();
setTimeout(() => {
this.#doWork().finally(() => {});
}, nextInterval);
}
});
});
}
async #doWorkInternal(): Promise<DoWorkInternalResult> {
// Attempt to dequeue a message from the shared queue
// If no message is available, reschedule the worker to run again in 1 second
// If a message is available, find the BackgroundWorkerTask that matches the message's taskIdentifier
// If no matching task is found, nack the message and reschedule the worker to run again in 1 second
// If the matching task is found, create the task attempt and lock the task run, then send the task run to the client
// Store the message as a processing message
// If the websocket connection disconnects before the task run is completed, nack the message
// When the task run completes, ack the message
// Using a heartbeat mechanism, if the client keeps responding with a heartbeat, we'll keep the message processing and increase the visibility timeout.
this._currentMessage = undefined;
this._currentMessageData = undefined;
const message = await marqs?.dequeueMessageFromSharedWorkerQueue(this._id);
if (!message) {
return {
reason: "no_message_dequeued",
outcome: "noop",
interval: this._options.nextTickInterval,
};
}
const dequeuedAt = new Date();
logger.log("dequeueMessageInSharedQueue()", { queueMessage: message });
const messageBody = SharedQueueMessageBody.safeParse(message.data);
if (!messageBody.success) {
logger.error("Failed to parse message", {
queueMessage: message.data,
error: messageBody.error,
});
await this.#ack(message.messageId);
return {
reason: "failed_to_parse_message",
outcome: "fail_with_ack",
attrs: { message_id: message.messageId, message_version: message.version },
error: messageBody.error,
};
}
const hydrateAttributes = (attrs: Record<string, string | number | boolean | undefined>) => {
return {
...attrs,
message_id: message.messageId,
message_version: message.version,
run_id: message.messageId,
message_type: messageBody.data.type,
};
};
this._currentMessage = message;
this._currentMessageData = messageBody.data;
const messageResult = await this.#handleMessage(message, messageBody.data, dequeuedAt);
switch (messageResult.action) {
case "noop": {
return {
reason: messageResult.reason ?? "none_specified",
outcome: "execution",
attrs: hydrateAttributes(messageResult.attrs ?? {}),
error: messageResult.error,
interval: messageResult.interval,
action: "noop",
};
}
case "ack_and_do_more_work": {
await this.#ack(message.messageId);
return {
reason: messageResult.reason ?? "none_specified",
outcome: "fail_with_ack",
attrs: hydrateAttributes(messageResult.attrs ?? {}),
error: messageResult.error,
interval: messageResult.interval,
action: "ack_and_do_more_work",
};
}
case "nack_and_do_more_work": {
await this.#nack(message.messageId, messageResult.retryInMs);
return {
reason: messageResult.reason ?? "none_specified",
outcome: "retry_with_nack",
attrs: hydrateAttributes(messageResult.attrs ?? {}),
error: messageResult.error,
interval: messageResult.interval,
action: "nack_and_do_more_work",
};
}
case "nack": {
await marqs?.nackMessage(message.messageId);
return {
reason: messageResult.reason ?? "none_specified",
outcome: "retry_with_nack",
attrs: hydrateAttributes(messageResult.attrs ?? {}),
error: messageResult.error,
action: "nack",
};
}
}
}
async #handleMessage(
message: MessagePayload,
data: SharedQueueMessageBody,
dequeuedAt: Date
): Promise<HandleMessageResult> {
return await this.#startActiveSpan("handleMessage()", async (span) => {
// TODO: For every ACK, decide what should be done with the existing run and attempts. Make sure to check the current statuses first.
switch (data.type) {
// MARK: EXECUTE
case "EXECUTE": {
return await this.#handleExecuteMessage(message, data, dequeuedAt);
}
// MARK: DEP RESUME
// Resume after dependency completed with no remaining retries
case "RESUME": {
return await this.#handleResumeMessage(message, data, dequeuedAt);
}
// MARK: DURATION RESUME
// Resume after duration-based wait
case "RESUME_AFTER_DURATION": {
return await this.#handleResumeAfterDurationMessage(message, data, dequeuedAt);
}
// MARK: FAIL
// Fail for whatever reason, usually runs that have been resumed but stopped heartbeating
case "FAIL": {
return await this.#handleFailMessage(message, data, dequeuedAt);
}
}
});
}
async #handleExecuteMessage(
message: MessagePayload,
data: SharedQueueExecuteMessageBody,
dequeuedAt: Date
): Promise<HandleMessageResult> {
const existingTaskRun = await prisma.taskRun.findFirst({
where: {
id: message.messageId,
},
});
if (!existingTaskRun) {
logger.error("No existing task run", {
queueMessage: message.data,
messageId: message.messageId,
});
// INFO: There used to be a race condition where tasks could be triggered, but execute messages could be dequeued before the run finished being created in the DB
// This should not be happening anymore. In case it does, consider reqeueuing here with a brief delay while limiting total retries.
return { action: "ack_and_do_more_work", reason: "no_existing_task_run" };
}
const retryingFromCheckpoint = !!data.checkpointEventId;
const EXECUTABLE_RUN_STATUSES = {
fromCheckpoint: ["WAITING_TO_RESUME"] satisfies TaskRunStatus[],
withoutCheckpoint: ["PENDING", "RETRYING_AFTER_FAILURE"] satisfies TaskRunStatus[],
};
if (
(retryingFromCheckpoint &&
!EXECUTABLE_RUN_STATUSES.fromCheckpoint.includes(existingTaskRun.status)) ||
(!retryingFromCheckpoint &&
!EXECUTABLE_RUN_STATUSES.withoutCheckpoint.includes(existingTaskRun.status))
) {
logger.warn("Task run has invalid status for execution. Going to ack", {
queueMessage: message.data,
messageId: message.messageId,
taskRun: existingTaskRun.id,
status: existingTaskRun.status,
retryingFromCheckpoint,
});
return {
action: "ack_and_do_more_work",
reason: "invalid_run_status",
attrs: { status: existingTaskRun.status, retryingFromCheckpoint },
};
}
// Check if the task run is locked to a specific worker, if not, use the current worker deployment
const deployment = await this.#startActiveSpan("findCurrentWorkerDeployment", async (span) => {
return existingTaskRun.lockedById
? await getWorkerDeploymentFromWorkerTask(existingTaskRun.lockedById)
: existingTaskRun.lockedToVersionId
? await getWorkerDeploymentFromWorker(existingTaskRun.lockedToVersionId)
: await findCurrentWorkerDeployment({
environmentId: existingTaskRun.runtimeEnvironmentId,
type: "V1",
});
});
const worker = deployment?.worker;
if (!deployment || !worker) {
// This happens when a run is "WAITING_FOR_DEPLOY" and is expected
logger.info("No matching deployment found for task run", {
queueMessage: message.data,
messageId: message.messageId,
});
await this.#markRunAsWaitingForDeploy(existingTaskRun.id);
return {
action: "ack_and_do_more_work",
reason: "no_matching_deployment",
attrs: {
run_id: existingTaskRun.id,
locked_by_id: existingTaskRun.lockedById ?? undefined,
locked_to_version_id: existingTaskRun.lockedToVersionId ?? undefined,
environment_id: existingTaskRun.runtimeEnvironmentId,
},
};
}
const imageReference = deployment.imageReference;
if (!imageReference) {
logger.error("Deployment is missing an image reference", {
queueMessage: message.data,
messageId: message.messageId,
deployment: deployment.id,
});
await this.#markRunAsWaitingForDeploy(existingTaskRun.id);
return {
action: "ack_and_do_more_work",
reason: "missing_image_reference",
attrs: {
deployment_id: deployment.id,
},
};
}
const backgroundTask = worker.tasks.find(
(task) => task.slug === existingTaskRun.taskIdentifier
);
if (!backgroundTask) {
const nonCurrentTask = await prisma.backgroundWorkerTask.findFirst({
where: {
slug: existingTaskRun.taskIdentifier,
projectId: existingTaskRun.projectId,
runtimeEnvironmentId: existingTaskRun.runtimeEnvironmentId,
},
include: {
worker: {
include: {
deployment: {
include: {},
},
},
},
},
});
if (nonCurrentTask) {
logger.warn("Task for this run exists but is not part of the current deploy", {
taskRun: existingTaskRun.id,
taskIdentifier: existingTaskRun.taskIdentifier,
});
} else {
logger.warn("Task for this run has never been deployed", {
taskRun: existingTaskRun.id,
taskIdentifier: existingTaskRun.taskIdentifier,
});
}
await this.#markRunAsWaitingForDeploy(existingTaskRun.id);
// If this task is ever deployed, a new message will be enqueued after successful indexing
return {
action: "ack_and_do_more_work",
reason: "task_not_deployed",
attrs: {
run_id: existingTaskRun.id,
task_identifier: existingTaskRun.taskIdentifier,
},
};
}
const lockedAt = new Date();
const machinePreset =
existingTaskRun.machinePreset ??
machinePresetFromConfig(backgroundTask.machineConfig ?? {}).name;
const maxDurationInSeconds = getMaxDuration(
existingTaskRun.maxDurationInSeconds,
backgroundTask.maxDurationInSeconds
);
const startedAt = existingTaskRun.startedAt ?? dequeuedAt;
const baseCostInCents = env.CENTS_PER_RUN;
const lockedTaskRun = await prisma.taskRun.update({
where: {
id: message.messageId,
},
data: {
lockedAt,
lockedById: backgroundTask.id,
lockedToVersionId: worker.id,
taskVersion: worker.version,
sdkVersion: worker.sdkVersion,
cliVersion: worker.cliVersion,
startedAt: startedAt,
baseCostInCents: baseCostInCents,
machinePreset: machinePreset,
maxDurationInSeconds,
},
include: {
runtimeEnvironment: true,
attempts: {
take: 1,
orderBy: { number: "desc" },
},
checkpoints: {
take: 1,
orderBy: {
createdAt: "desc",
},
},
lockedBy: true,
},
});
if (!lockedTaskRun) {
logger.warn("Failed to lock task run", {
taskRun: existingTaskRun.id,
taskIdentifier: existingTaskRun.taskIdentifier,
deployment: deployment.id,
backgroundWorker: worker.id,
messageId: message.messageId,
});
return {
action: "ack_and_do_more_work",
reason: "failed_to_lock_task_run",
attrs: {
run_id: existingTaskRun.id,
task_identifier: existingTaskRun.taskIdentifier,
deployment_id: deployment.id,
background_worker_id: worker.id,
message_id: message.messageId,
},
};
}
const queue = await findQueueInEnvironment(
lockedTaskRun.queue,
lockedTaskRun.runtimeEnvironmentId,
lockedTaskRun.lockedById ?? undefined,
backgroundTask
);
if (!queue) {
logger.debug("SharedQueueConsumer queue not found, so acking message", {
queueMessage: message,
taskRunQueue: lockedTaskRun.queue,
runtimeEnvironmentId: lockedTaskRun.runtimeEnvironmentId,
});
return {
action: "ack_and_do_more_work",
reason: "queue_not_found",
attrs: {
queue_name: sanitizeQueueName(lockedTaskRun.queue),
runtime_environment_id: lockedTaskRun.runtimeEnvironmentId,
},
interval: this._options.nextTickInterval,
};
}
if (!this._enabled) {
logger.debug("SharedQueueConsumer not enabled, so nacking message", {
queueMessage: message,
});
return {
action: "nack",
reason: "not_enabled",
attrs: {
message_id: message.messageId,
},
};
}
const nextAttemptNumber = lockedTaskRun.attempts[0] ? lockedTaskRun.attempts[0].number + 1 : 1;
const isRetry =
nextAttemptNumber > 1 &&
(lockedTaskRun.status === "WAITING_TO_RESUME" ||
lockedTaskRun.status === "RETRYING_AFTER_FAILURE");
try {
if (data.checkpointEventId) {
const restoreService = new RestoreCheckpointService();
const checkpoint = await restoreService.call({
eventId: data.checkpointEventId,
isRetry,
});
if (!checkpoint) {
logger.error("Failed to restore checkpoint", {
queueMessage: message.data,
messageId: message.messageId,
runStatus: lockedTaskRun.status,
isRetry,
});
return {
action: "ack_and_do_more_work",
reason: "failed_to_restore_checkpoint",
attrs: {
run_status: lockedTaskRun.status,
is_retry: isRetry,
checkpoint_event_id: data.checkpointEventId,
},
};
}
return {
action: "noop",
reason: "restored_checkpoint",
attrs: {
checkpoint_event_id: data.checkpointEventId,
},
};
}
if (!worker.supportsLazyAttempts) {
try {
const service = new CreateTaskRunAttemptService();
await service.call({
runId: lockedTaskRun.id,
setToExecuting: false,
});
} catch (error) {
logger.error("Failed to create task run attempt for outdated worker", {
error,
taskRun: lockedTaskRun.id,
});
const service = new CrashTaskRunService();
await service.call(lockedTaskRun.id, {
errorCode: TaskRunErrorCodes.OUTDATED_SDK_VERSION,
});
return {
action: "ack_and_do_more_work",
reason: "failed_to_create_attempt_for_outdated_worker",
attrs: {
message_id: message.messageId,
run_id: lockedTaskRun.id,
},
error: error instanceof Error ? error : String(error),
};
}
}
if (isRetry && !data.retryCheckpointsDisabled) {
socketIo.coordinatorNamespace.emit("READY_FOR_RETRY", {
version: "v1",
runId: lockedTaskRun.id,
});
// Retries for workers with disabled retry checkpoints will be handled just like normal attempts
return {
action: "noop",
reason: "retry_checkpoints_disabled",
};
}
const machine =
machinePresetFromRun(lockedTaskRun) ??
machinePresetFromConfig(lockedTaskRun.lockedBy?.machineConfig ?? {});
return await this.#startActiveSpan("scheduleAttemptOnProvider", async (span) => {
span.setAttributes({
run_id: lockedTaskRun.id,
});
if (await this._providerSender.validateCanSendMessage()) {
await this._providerSender.send("BACKGROUND_WORKER_MESSAGE", {
backgroundWorkerId: worker.friendlyId,
data: {
type: "SCHEDULE_ATTEMPT",
image: imageReference,
version: deployment.version,
machine,
nextAttemptNumber,
// identifiers
id: "placeholder", // TODO: Remove this completely in a future release
envId: lockedTaskRun.runtimeEnvironment.id,
envType: lockedTaskRun.runtimeEnvironment.type,
orgId: lockedTaskRun.runtimeEnvironment.organizationId,
projectId: lockedTaskRun.runtimeEnvironment.projectId,
runId: lockedTaskRun.id,
dequeuedAt: dequeuedAt.getTime(),
},
});
return {
action: "noop",
reason: "scheduled_attempt",
attrs: {
next_attempt_number: nextAttemptNumber,
},
};
} else {
return {
action: "nack_and_do_more_work",
reason: "provider_not_connected",
attrs: {
run_id: lockedTaskRun.id,
},
interval: this._options.nextTickInterval,
retryInMs: 5_000,
};
}
});
} catch (e) {
// 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: lockedTaskRun.status,
startedAt: existingTaskRun.startedAt,
},
}),
]);
logger.error("SharedQueueConsumer errored, so nacking message", {
queueMessage: message,
error: e instanceof Error ? { name: e.name, message: e.message, stack: e.stack } : e,
});
return {
action: "nack_and_do_more_work",
reason: "failed_to_schedule_attempt",
error: e instanceof Error ? e : String(e),
interval: this._options.nextTickInterval,
retryInMs: 5_000,
};
}
}
async #handleResumeMessage(
message: MessagePayload,
data: SharedQueueResumeMessageBody,
dequeuedAt: Date
): Promise<HandleMessageResult> {
if (data.checkpointEventId) {
try {
const restoreService = new RestoreCheckpointService();
const checkpoint = await restoreService.call({
eventId: data.checkpointEventId,
});
if (!checkpoint) {
logger.error("Failed to restore checkpoint", {
queueMessage: message.data,
messageId: message.messageId,
});
return {
action: "ack_and_do_more_work",
reason: "failed_to_restore_checkpoint",
attrs: {
checkpoint_event_id: data.checkpointEventId,
},
};
}
return {
action: "noop",
reason: "restored_checkpoint",
attrs: {
checkpoint_event_id: data.checkpointEventId,
},
};
} catch (e) {
return {
action: "nack_and_do_more_work",
reason: "failed_to_restore_checkpoint",
error: e instanceof Error ? e : String(e),
};
}
}
const resumableRun = await prisma.taskRun.findFirst({
where: {
id: message.messageId,
status: {
notIn: FINAL_RUN_STATUSES,
},
},
});
if (!resumableRun) {
logger.error("Resumable run not found", {
queueMessage: message.data,
messageId: message.messageId,
});
return {
action: "ack_and_do_more_work",
reason: "run_not_found",
};
}
if (resumableRun.status !== "EXECUTING") {
logger.warn("Run is not executing, will try to resume anyway", {
queueMessage: message.data,
messageId: message.messageId,
runStatus: resumableRun.status,
});
}
const resumableAttempt = await prisma.taskRunAttempt.findFirst({
where: {
id: data.resumableAttemptId,
},
include: {
checkpoints: {
take: 1,
orderBy: {
createdAt: "desc",
},
},
},
});
if (!resumableAttempt) {
logger.error("Resumable attempt not found", {
queueMessage: message.data,
messageId: message.messageId,
});
return {
action: "ack_and_do_more_work",
reason: "resumable_attempt_not_found",
attrs: {
attempt_id: data.resumableAttemptId,
},
};
}
const queue = await findQueueInEnvironment(
resumableRun.queue,
resumableRun.runtimeEnvironmentId,
resumableRun.lockedById ?? undefined
);
if (!queue) {
logger.debug("SharedQueueConsumer queue not found, so nacking message", {
queueName: sanitizeQueueName(resumableRun.queue),
attempt: resumableAttempt,
});
return {
action: "ack_and_do_more_work",
reason: "queue_not_found",
attrs: {
queue_name: sanitizeQueueName(resumableRun.queue),
},
interval: this._options.nextTickInterval,
};
}
if (!this._enabled) {
return {
action: "nack",
reason: "not_enabled",
attrs: {
message_id: message.messageId,
},
};
}
try {
const { completions, executions } = await this.#resolveCompletedAttemptsForResumeMessage(
data.completedAttemptIds
);
const resumeMessage = {
version: "v1" as const,
runId: resumableAttempt.taskRunId,
attemptId: resumableAttempt.id,
attemptFriendlyId: resumableAttempt.friendlyId,
completions,
executions,
};
logger.debug("Broadcasting RESUME_AFTER_DEPENDENCY_WITH_ACK", {
resumeMessage,
message,
resumableRun,
});
// The attempt should still be running so we can broadcast to all coordinators to resume immediately
const responses = await this.#startActiveSpan(
"emitResumeAfterDependencyWithAck",
async (span) => {
try {
span.setAttribute("attempt_id", resumableAttempt.id);
span.setAttribute(
"timeout_in_ms",
env.SHARED_QUEUE_CONSUMER_EMIT_RESUME_DEPENDENCY_TIMEOUT_MS
);
const responses = await socketIo.coordinatorNamespace
.timeout(env.SHARED_QUEUE_CONSUMER_EMIT_RESUME_DEPENDENCY_TIMEOUT_MS)
.emitWithAck("RESUME_AFTER_DEPENDENCY_WITH_ACK", resumeMessage);
span.setAttribute("response_count", responses.length);
const hasSuccess = responses.some((response) => response.success);
span.setAttribute("has_success", hasSuccess);
span.setAttribute("is_timeout", false);
return responses;
} catch (e) {
if (e instanceof Error && "responses" in e && Array.isArray(e.responses)) {
span.setAttribute("is_timeout", false);
const responses = e.responses as AckCallbackResult[];
span.setAttribute("response_count", responses.length);
const hasSuccess = responses.some(
(response) => "success" in response && response.success
);
span.setAttribute("has_success", hasSuccess);
return responses;
}
throw e;
}
}
);
logger.debug("RESUME_AFTER_DEPENDENCY_WITH_ACK received", {
resumeMessage,
responses,
message,
});
if (responses.length === 0) {
logger.error("RESUME_AFTER_DEPENDENCY_WITH_ACK no response", {
resumeMessage,
message,
});
return {
action: "nack_and_do_more_work",
reason: "resume_after_dependency_with_ack_no_response",
attrs: {
resume_message: "RESUME_AFTER_DEPENDENCY_WITH_ACK",
},
interval: this._options.nextTickInterval,
retryInMs: 5_000,
};
}
const hasSuccess = responses.some((response) => response.success);
if (hasSuccess) {
return {
action: "noop",
reason: "resume_after_dependency_with_ack_success",
};
}
// No coordinator was able to resume the run
logger.warn("RESUME_AFTER_DEPENDENCY_WITH_ACK failed", {
resumeMessage,
responses,
message,
});
// Let's check if the run is frozen
if (resumableRun.status === "WAITING_TO_RESUME") {
logger.debug("RESUME_AFTER_DEPENDENCY_WITH_ACK run is waiting to be restored", {
queueMessage: message.data,
messageId: message.messageId,
});
try {
const restoreService = new RestoreCheckpointService();
const checkpointEvent = await restoreService.getLastCheckpointEventIfUnrestored(
resumableRun.id
);
if (checkpointEvent) {
// The last checkpoint hasn't been restored yet, so restore it
const checkpoint = await restoreService.call({
eventId: checkpointEvent.id,
});
if (!checkpoint) {
logger.debug("RESUME_AFTER_DEPENDENCY_WITH_ACK failed to restore checkpoint", {
queueMessage: message.data,
messageId: message.messageId,
});
return {
action: "ack_and_do_more_work",
reason: "failed_to_restore_checkpoint",
attrs: {
checkpoint_event_id: checkpointEvent.id,
},
};
}
logger.debug("RESUME_AFTER_DEPENDENCY_WITH_ACK restored checkpoint", {
queueMessage: message.data,
messageId: message.messageId,
checkpoint,
});
return {
action: "noop",
reason: "restored_checkpoint",
attrs: {
checkpoint_event_id: data.checkpointEventId,
},
};
} else {
logger.debug(
"RESUME_AFTER_DEPENDENCY_WITH_ACK run is frozen without last checkpoint event",
{
queueMessage: message.data,
messageId: message.messageId,
}
);
return {
action: "noop",
reason: "resume_after_dependency_with_ack_frozen",
};
}
} catch (e) {
return {
action: "nack_and_do_more_work",
reason: "waiting_to_resume_threw",
error: e instanceof Error ? e : String(e),
interval: this._options.nextTickInterval,
retryInMs: 5_000,
};
}
}
logger.debug("RESUME_AFTER_DEPENDENCY_WITH_ACK retrying", {
queueMessage: message.data,
messageId: message.messageId,
});
return {
action: "nack_and_do_more_work",
reason: "resume_after_dependency_with_ack_retrying",
attrs: {
message_id: message.messageId,
},
interval: this._options.nextTickInterval,
retryInMs: 5_000,
};
} catch (e) {
if (e instanceof ResumePayloadAttemptsNotFoundError) {
return {
action: "ack_and_do_more_work",
reason: "failed_to_get_resume_payloads_for_attempts",
attrs: {
attempt_ids: e.attemptIds.join(","),
},
};
} else if (e instanceof ResumePayloadExecutionNotFoundError) {
return {
action: "ack_and_do_more_work",
reason: "failed_to_get_resume_payloads_missing_execution",
attrs: {
attempt_id: e.attemptId,
},
};
} else if (e instanceof ResumePayloadCompletionNotFoundError) {
return {
action: "ack_and_do_more_work",
reason: "failed_to_get_resume_payloads_missing_completion",
attrs: {
attempt_id: e.attemptId,
},
};
}
logger.error("RESUME_AFTER_DEPENDENCY_WITH_ACK threw, nacking with delay", {
message,
error: e,
});
return {
action: "nack_and_do_more_work",
reason: "resume_after_dependency_with_ack_threw",
error: e instanceof Error ? e : String(e),
interval: this._options.nextTickInterval,
retryInMs: 5_000,
};
}
}
async #handleResumeAfterDurationMessage(
message: MessagePayload,
data: SharedQueueResumeAfterDurationMessageBody,
dequeuedAt: Date
): Promise<HandleMessageResult> {
try {
const restoreService = new RestoreCheckpointService();
const checkpoint = await restoreService.call({
eventId: data.checkpointEventId,
});
if (!checkpoint) {
logger.error("Failed to restore checkpoint", {
queueMessage: message.data,
messageId: message.messageId,
});
return {
action: "ack_and_do_more_work",
reason: "failed_to_restore_checkpoint",
attrs: {
checkpoint_event_id: data.checkpointEventId,
},
};
}
return {
action: "noop",
reason: "restored_checkpoint",
attrs: {
checkpoint_event_id: data.checkpointEventId,
},
};
} catch (e) {
return {
action: "nack_and_do_more_work",
reason: "restoring_checkpoint_threw",
error: e instanceof Error ? e : String(e),
};
}
}
async #handleFailMessage(
message: MessagePayload,
data: SharedQueueFailMessageBody,
dequeuedAt: Date
): Promise<HandleMessageResult> {
const existingTaskRun = await prisma.taskRun.findFirst({
where: {
id: message.messageId,
},
});
if (!existingTaskRun) {
logger.error("No existing task run to fail", {
queueMessage: data,
messageId: message.messageId,
});
return {
action: "ack_and_do_more_work",
reason: "no_existing_task_run",
};
}
// TODO: Consider failing the attempt and retrying instead. This may not be a good idea, as dequeued FAIL messages tend to point towards critical, persistent errors.
const service = new CrashTaskRunService();
await service.call(existingTaskRun.id, {
crashAttempts: true,
reason: data.reason,
});
return {
action: "ack_and_do_more_work",
reason: "message_failed",
};
}
async #ack(messageId: string) {
await marqs?.acknowledgeMessage(messageId, "Acking and doing more work in SharedQueueConsumer");
}
async #nack(messageId: string, nackRetryInMs?: number) {
const retryAt = nackRetryInMs ? Date.now() + nackRetryInMs : undefined;
await marqs?.nackMessage(messageId, retryAt);
}
async #markRunAsWaitingForDeploy(runId: string) {
logger.debug("Marking run as waiting for deploy", { runId });
const _run = await prisma.taskRun.update({
where: {
id: runId,
},
data: {
status: "WAITING_FOR_DEPLOY",
},
});
}
async #resolveCompletedAttemptsForResumeMessage(
completedAttemptIds: string[]
): Promise<{ completions: TaskRunExecutionResult[]; executions: TaskRunExecution[] }> {
return await this.#startActiveSpan("resolveCompletedAttemptsForResumeMessage", async (span) => {
span.setAttribute("completed_attempt_count", completedAttemptIds.length);
// Chunk the completedAttemptIds into chunks of 10
const chunkedCompletedAttemptIds = chunk(
completedAttemptIds,
env.SHARED_QUEUE_CONSUMER_RESOLVE_PAYLOADS_BATCH_SIZE
);
span.setAttribute("chunk_count", chunkedCompletedAttemptIds.length);
span.setAttribute("chunk_size", env.SHARED_QUEUE_CONSUMER_RESOLVE_PAYLOADS_BATCH_SIZE);
const allResumePayloads = await this.#startActiveSpan(
"resolveAllResumePayloads",
async (span) => {
span.setAttribute("chunk_count", chunkedCompletedAttemptIds.length);
span.setAttribute("chunk_size", env.SHARED_QUEUE_CONSUMER_RESOLVE_PAYLOADS_BATCH_SIZE);
span.setAttribute("completed_attempt_count", completedAttemptIds.length);
return await Promise.all(
chunkedCompletedAttemptIds.map(async (attemptIds) => {
const payloads = await this.#startActiveSpan("getResumePayloads", async (span) => {
span.setAttribute("attempt_ids", attemptIds.join(","));
span.setAttribute("attempt_count", attemptIds.length);
const payloads = await this._tasks.getResumePayloads(attemptIds);
span.setAttribute("payload_count", payloads.length);
return payloads;
});
return {
completions: payloads.map((payload) => payload.completion),
executions: payloads.map((payload) => payload.execution),
};
})
);
}
);
return {
completions: allResumePayloads.flatMap((payload) => payload.completions),
executions: allResumePayloads.flatMap((payload) => payload.executions),
};
});
}
async #startActiveSpan<T>(
name: string,
fn: (span: Span) => Promise<T>,
options?: SpanOptions
): Promise<T> {
return await tracer.startActiveSpan(name, options ?? {}, async (span) => {
if (this._currentMessage) {
span.setAttribute("message_id", this._currentMessage.messageId);
span.setAttribute("run_id", this._currentMessage.messageId);
span.setAttribute("message_version", this._currentMessage.version);
}
if (this._currentMessageData) {
span.setAttribute("message_type", this._currentMessageData.type);
}
try {
return await fn(span);
} catch (error) {
if (error instanceof Error) {
span.recordException(error);
} else {
span.recordException(String(error));
}
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
});
}
}
class ResumePayloadAttemptsNotFoundError extends Error {
constructor(public readonly attemptIds: string[]) {
super(`Resume payload attempts not found for attempts ${attemptIds.join(", ")}`);
}
}
class ResumePayloadExecutionNotFoundError extends Error {
constructor(public readonly attemptId: string) {
super(`Resume payload execution not found for attempt ${attemptId}`);
}
}
class ResumePayloadCompletionNotFoundError extends Error {
constructor(public readonly attemptId: string) {
super(`Resume payload completion not found for attempt ${attemptId}`);
}
}
function chunk<T>(arr: T[], chunkSize: number): T[][] {
return Array.from({ length: Math.ceil(arr.length / chunkSize) }, (_, i) =>
arr.slice(i * chunkSize, i * chunkSize + chunkSize)
);
}
export const AttemptForCompletionGetPayload = {
select: {
status: true,
output: true,
outputType: true,
error: true,
taskRun: {
select: {
taskIdentifier: true,
friendlyId: true,
},
},
},
} as const;
type AttemptForCompletion = Prisma.TaskRunAttemptGetPayload<typeof AttemptForCompletionGetPayload>;
export const AttemptForExecutionGetPayload = {
select: {
id: true,
friendlyId: true,
taskRunId: true,
number: true,
startedAt: true,
createdAt: true,
backgroundWorkerId: true,
backgroundWorkerTaskId: true,
backgroundWorker: {
select: {
contentHash: true,
version: true,
},
},
backgroundWorkerTask: {
select: {
machineConfig: true,
slug: true,
filePath: true,
exportName: true,
},
},
status: true,
runtimeEnvironment: {
select: {
...RuntimeEnvironmentForEnvRepoPayload.select,
organization: {
select: {
id: true,
slug: true,
title: true,
},
},
project: {
select: {
id: true,
externalRef: true,
slug: true,
name: true,
},
},
},
},
taskRun: {
select: {
id: true,
status: true,
traceContext: true,
machinePreset: true,
friendlyId: true,
payload: true,
payloadType: true,
context: true,
createdAt: true,
startedAt: true,
isTest: true,
replayedFromTaskRunFriendlyId: true,
metadata: true,
metadataType: true,
idempotencyKey: true,
usageDurationMs: true,
costInCents: true,
baseCostInCents: true,
maxDurationInSeconds: true,
runTags: true,
taskEventStore: true,
},
},
queue: {
select: {
name: true,
friendlyId: true,
},
},
},
} as const;
type AttemptForExecution = Prisma.TaskRunAttemptGetPayload<typeof AttemptForExecutionGetPayload>;
class SharedQueueTasks {
private _completionPayloadFromAttempt(attempt: AttemptForCompletion): TaskRunExecutionResult {
const ok = attempt.status === "COMPLETED";
if (ok) {
const success: TaskRunSuccessfulExecutionResult = {
ok,
id: attempt.taskRun.friendlyId,
output: attempt.output ?? undefined,
outputType: attempt.outputType,
taskIdentifier: attempt.taskRun.taskIdentifier,
};
return success;
} else {
const failure: TaskRunFailedExecutionResult = {
ok,
id: attempt.taskRun.friendlyId,
error: attempt.error as TaskRunError,
taskIdentifier: attempt.taskRun.taskIdentifier,
};
return failure;
}
}
private async _executionFromAttempt(
attempt: AttemptForExecution,
machinePreset?: MachinePreset
): Promise<V3ProdTaskRunExecution> {
const { backgroundWorkerTask, taskRun, queue } = attempt;
if (!machinePreset) {
machinePreset =
machinePresetFromRun(attempt.taskRun) ??
machinePresetFromConfig(backgroundWorkerTask.machineConfig ?? {});
}
const metadata = await parsePacket({
data: taskRun.metadata ?? undefined,
dataType: taskRun.metadataType,
});
const execution: V3ProdTaskRunExecution = {
task: {
id: backgroundWorkerTask.slug,
filePath: backgroundWorkerTask.filePath,
exportName: backgroundWorkerTask.exportName ?? backgroundWorkerTask.slug,
},
attempt: {
id: attempt.friendlyId,
number: attempt.number,
startedAt: attempt.startedAt ?? attempt.createdAt,
backgroundWorkerId: attempt.backgroundWorkerId,
backgroundWorkerTaskId: attempt.backgroundWorkerTaskId,
status: "EXECUTING" as const,
},
run: {
id: taskRun.friendlyId,
payload: taskRun.payload,
payloadType: taskRun.payloadType,
context: taskRun.context,
createdAt: taskRun.createdAt,
startedAt: taskRun.startedAt ?? taskRun.createdAt,
tags: taskRun.runTags ?? [],
isTest: taskRun.isTest,
isReplay: !!taskRun.replayedFromTaskRunFriendlyId,
idempotencyKey: taskRun.idempotencyKey ?? undefined,
durationMs: taskRun.usageDurationMs,
costInCents: taskRun.costInCents,
baseCostInCents: taskRun.baseCostInCents,
metadata,
maxDuration: taskRun.maxDurationInSeconds ?? undefined,
},
queue: {
id: queue.friendlyId,
name: queue.name,
},
environment: {
id: attempt.runtimeEnvironment.id,
slug: attempt.runtimeEnvironment.slug,
type: attempt.runtimeEnvironment.type,
},
organization: {
id: attempt.runtimeEnvironment.organization.id,
slug: attempt.runtimeEnvironment.organization.slug,
name: attempt.runtimeEnvironment.organization.title,
},
project: {
id: attempt.runtimeEnvironment.project.id,
ref: attempt.runtimeEnvironment.project.externalRef,
slug: attempt.runtimeEnvironment.project.slug,
name: attempt.runtimeEnvironment.project.name,
},
batch: undefined, // TODO: Removing this for now until we can do it more efficiently
worker: {
id: attempt.backgroundWorkerId,
contentHash: attempt.backgroundWorker.contentHash,
version: attempt.backgroundWorker.version,
},
machine: machinePreset,
};
return execution;
}
async getCompletionPayloadFromAttempt(id: string): Promise<TaskRunExecutionResult | undefined> {
const attempt = await prisma.taskRunAttempt.findFirst({
where: {
id,
status: {
in: FINAL_ATTEMPT_STATUSES,
},
},
...AttemptForCompletionGetPayload,
});
if (!attempt) {
logger.error("No completed attempt found", { id });
return;
}
return this._completionPayloadFromAttempt(attempt);
}
async getExecutionPayloadFromAttempt({
id,
setToExecuting,
isRetrying,
skipStatusChecks,
}: {
id: string;
setToExecuting?: boolean;
isRetrying?: boolean;
skipStatusChecks?: boolean;
}): Promise<V3ProdTaskRunExecutionPayload | undefined> {
const attempt = await prisma.taskRunAttempt.findFirst({
where: {
id,
},
...AttemptForExecutionGetPayload,
});
if (!attempt) {
logger.error("getExecutionPayloadFromAttempt: No attempt found", { id });
return;
}
if (!skipStatusChecks) {
switch (attempt.status) {
case "CANCELED":
case "EXECUTING": {
logger.error(
"getExecutionPayloadFromAttempt: Invalid attempt status for execution payload retrieval",
{
attemptId: id,
status: attempt.status,
}
);
return;
}
}
switch (attempt.taskRun.status) {
case "CANCELED":
case "EXECUTING":
case "INTERRUPTED": {
logger.error(
"getExecutionPayloadFromAttempt: Invalid run status for execution payload retrieval",
{
attemptId: id,
runId: attempt.taskRunId,
status: attempt.taskRun.status,
}
);
return;
}
}
}
if (setToExecuting) {
if (isFinalAttemptStatus(attempt.status) || isFinalRunStatus(attempt.taskRun.status)) {
logger.error("getExecutionPayloadFromAttempt: Status already in final state", {
attempt: {
id: attempt.id,
status: attempt.status,
},
run: {
id: attempt.taskRunId,
status: attempt.taskRun.status,
},
});
return;
}
await prisma.taskRunAttempt.update({
where: {
id,
},
data: {
status: "EXECUTING",
taskRun: {
update: {
data: {
status: isRetrying ? "RETRYING_AFTER_FAILURE" : "EXECUTING",
},
},
},
},
});
}
const { backgroundWorkerTask, taskRun } = attempt;
const machinePreset =
machinePresetFromRun(attempt.taskRun) ??
machinePresetFromConfig(backgroundWorkerTask.machineConfig ?? {});
const execution = await this._executionFromAttempt(attempt, machinePreset);
const variables = await this.#buildEnvironmentVariables(
attempt.runtimeEnvironment,
taskRun.id,
machinePreset,
taskRun.taskEventStore ?? undefined
);
const payload: V3ProdTaskRunExecutionPayload = {
execution,
traceContext: taskRun.traceContext as Record<string, unknown>,
environment: variables.reduce((acc: Record<string, string>, curr) => {
acc[curr.key] = curr.value;
return acc;
}, {}),
};
return payload;
}
async getResumePayload(attemptId: string): Promise<
| {
execution: V3ProdTaskRunExecution;
completion: TaskRunExecutionResult;
}
| undefined
> {
const attempt = await prisma.taskRunAttempt.findFirst({
where: {
id: attemptId,
},
select: {
...AttemptForExecutionGetPayload.select,
error: true,
output: true,
outputType: true,
taskRun: {
select: {
...AttemptForExecutionGetPayload.select.taskRun.select,
taskIdentifier: true,
},
},
},
});
if (!attempt) {
logger.error("getResumePayload: No attempt found", { id: attemptId });
return;
}
const execution = await this._executionFromAttempt(attempt);
const completion = this._completionPayloadFromAttempt(attempt);
return {
execution,
completion,
};
}
async getResumePayloads(attemptIds: string[]): Promise<
Array<{
execution: V3ProdTaskRunExecution;
completion: TaskRunExecutionResult;
}>
> {
const attempts = await prisma.taskRunAttempt.findMany({
where: {
id: {
in: attemptIds,
},
},
select: {
...AttemptForExecutionGetPayload.select,
error: true,
output: true,
outputType: true,
taskRun: {
select: {
...AttemptForExecutionGetPayload.select.taskRun.select,
taskIdentifier: true,
},
},
},
});
if (attempts.length !== attemptIds.length) {
logger.error("getResumePayloads: Not all attempts found", { attemptIds });
throw new ResumePayloadAttemptsNotFoundError(attemptIds);
}
const payloads = await Promise.all(
attempts.map(async (attempt) => {
const execution = await this._executionFromAttempt(attempt);
if (!execution) {
throw new ResumePayloadExecutionNotFoundError(attempt.id);
}
const completion = this._completionPayloadFromAttempt(attempt);
if (!completion) {
throw new ResumePayloadCompletionNotFoundError(attempt.id);
}
return {
execution,
completion,
};
})
);
return payloads;
}
async getLatestExecutionPayloadFromRun(
id: string,
setToExecuting?: boolean,
isRetrying?: boolean
): Promise<V3ProdTaskRunExecutionPayload | undefined> {
const run = await prisma.taskRun.findFirst({
where: {
id,
},
include: {
attempts: {
take: 1,
orderBy: {
createdAt: "desc",
},
},
},
});
const latestAttempt = run?.attempts[0];
if (!latestAttempt) {
logger.error("No attempts for run", { id });
return;
}
return this.getExecutionPayloadFromAttempt({
id: latestAttempt.id,
setToExecuting,
isRetrying,
});
}
async getLazyAttemptPayload(
envId: string,
runId: string
): Promise<TaskRunExecutionLazyAttemptPayload | undefined> {
const environment = await findEnvironmentById(envId);
if (!environment) {
logger.error("getLazyAttemptPayload: Environment not found", { runId, envId });
return;
}
const run = await prisma.taskRun.findFirst({
where: {
id: runId,
},
select: {
id: true,
traceContext: true,
friendlyId: true,
isTest: true,
replayedFromTaskRunFriendlyId: true,
lockedBy: {
select: {
machineConfig: true,
},
},
machinePreset: true,
taskEventStore: true,
},
});
if (!run) {
logger.error("getLazyAttemptPayload: Run not found", { runId, envId });
return;
}
const attemptCount = await prisma.taskRunAttempt.count({
where: {
taskRunId: run.id,
},
});
logger.debug("getLazyAttemptPayload: Getting lazy attempt payload for run", {
run,
attemptCount,
});
const machinePreset =
machinePresetFromRun(run) ?? machinePresetFromConfig(run.lockedBy?.machineConfig ?? {});
const variables = await this.#buildEnvironmentVariables(
environment,
run.id,
machinePreset,
run.taskEventStore ?? undefined
);
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,
isReplay: !!run.replayedFromTaskRunFriendlyId,
attemptCount,
metrics: [],
} satisfies TaskRunExecutionLazyAttemptPayload;
}
async taskHeartbeat(attemptFriendlyId: string) {
logger.debug("[SharedQueueConsumer] taskHeartbeat()", { id: attemptFriendlyId });
const taskRunAttempt = await prisma.taskRunAttempt.findFirst({
where: { friendlyId: attemptFriendlyId },
});
if (!taskRunAttempt) {
return;
}
await this.#heartbeat(taskRunAttempt.taskRunId);
}
async taskRunHeartbeat(runId: string) {
logger.debug("[SharedQueueConsumer] taskRunHeartbeat()", { runId });
await this.#heartbeat(runId);
}
public async taskRunFailed(completion: TaskRunFailedExecutionResult) {
logger.debug("[SharedQueueConsumer] taskRunFailed()", { completion });
const service = new FailedTaskRunService();
await service.call(completion.id, completion);
}
async #heartbeat(runId: string) {
await marqs?.heartbeatMessage(runId);
try {
// There can be a lot of calls per minute and the data doesn't have to be accurate, so use the read replica
const taskRun = await $replica.taskRun.findFirst({
where: {
id: runId,
},
select: {
id: true,
status: true,
runtimeEnvironment: {
select: {
type: true,
},
},
lockedToVersion: {
select: {
supportsLazyAttempts: true,
},
},
},
});
if (!taskRun) {
logger.error("SharedQueueTasks.#heartbeat: Task run not found", {
runId,
});
return;
}
if (taskRun.runtimeEnvironment.type === "DEVELOPMENT") {
return;
}
if (isFinalRunStatus(taskRun.status)) {
logger.debug("SharedQueueTasks.#heartbeat: Task run is in final status", {
runId,
status: taskRun.status,
});
// Signal to exit any leftover containers
socketIo.coordinatorNamespace.emit("REQUEST_RUN_CANCELLATION", {
version: "v1",
runId: taskRun.id,
// Give the run a few seconds to exit to complete any flushing etc
delayInMs: taskRun.lockedToVersion?.supportsLazyAttempts ? 5_000 : undefined,
});
return;
}
} catch (error) {
logger.error("SharedQueueTasks.#heartbeat: Error signaling run cancellation", {
runId,
error: error instanceof Error ? error.message : error,
});
}
}
async #buildEnvironmentVariables(
environment: RuntimeEnvironmentForEnvRepo,
runId: string,
machinePreset: MachinePreset,
taskEventStore?: string
): Promise<Array<EnvironmentVariable>> {
const variables = await resolveVariablesForEnvironment(environment);
const jwt = await generateJWTTokenForEnvironment(environment, {
run_id: runId,
machine_preset: machinePreset.name,
});
if (taskEventStore) {
const resourceAttributes = JSON.stringify({
[SemanticInternalAttributes.TASK_EVENT_STORE]: taskEventStore,
});
variables.push(...[{ key: "OTEL_RESOURCE_ATTRIBUTES", value: resourceAttributes }]);
}
return [
...variables,
...[
{ key: "TRIGGER_JWT", value: jwt },
{ key: "TRIGGER_RUN_ID", value: runId },
{
key: "TRIGGER_MACHINE_PRESET",
value: machinePreset.name,
},
],
];
}
}
export const sharedQueueTasks = singleton("sharedQueueTasks", () => new SharedQueueTasks());