import { BackgroundWorkerMetadata, tryCatch } from "@trigger.dev/core/v3"; import { CURRENT_DEPLOYMENT_LABEL } from "@trigger.dev/core/v3/isomorphic"; import type { PrismaClientOrTransaction, WorkerDeployment } from "@trigger.dev/database"; import { logger } from "~/services/logger.server"; import { syncTaskIdentifiers } from "~/services/taskIdentifierRegistry.server"; import { type TaskMetadataCache, type TaskMetadataEntry, } from "~/services/taskMetadataCache.server"; import { taskMetadataCacheInstance } from "~/services/taskMetadataCacheInstance.server"; import { BaseService, ServiceValidationError } from "./baseService.server"; import { syncDeclarativeSchedules } from "./createBackgroundWorker.server"; import { ExecuteTasksWaitingForDeployService } from "./executeTasksWaitingForDeploy"; import { compareDeploymentVersions } from "../utils/deploymentVersions"; export type ChangeCurrentDeploymentDirection = "promote" | "rollback"; export class ChangeCurrentDeploymentService extends BaseService { private readonly _taskMetaCache: TaskMetadataCache; constructor( prisma?: PrismaClientOrTransaction, replica?: PrismaClientOrTransaction, taskMetaCache: TaskMetadataCache = taskMetadataCacheInstance ) { super(prisma, replica); this._taskMetaCache = taskMetaCache; } public async call( deployment: WorkerDeployment, direction: ChangeCurrentDeploymentDirection, disableVersionCheck?: boolean ) { if (!deployment.workerId) { throw new ServiceValidationError( direction === "promote" ? "Deployment is not associated with a worker and cannot be promoted." : "Deployment is not associated with a worker and cannot be rolled back." ); } if (deployment.status !== "DEPLOYED") { throw new ServiceValidationError( direction === "promote" ? "Deployment must be in the DEPLOYED state to be promoted." : "Deployment must be in the DEPLOYED state to be rolled back." ); } const currentPromotion = await this._prisma.workerDeploymentPromotion.findFirst({ where: { environmentId: deployment.environmentId, label: CURRENT_DEPLOYMENT_LABEL, }, select: { deployment: { select: { id: true, version: true }, }, }, }); if (currentPromotion) { if (currentPromotion.deployment.id === deployment.id) { throw new ServiceValidationError("Deployment is already the current deployment."); } // if there is a current promotion, we have to validate we are moving in the right direction based on the deployment versions if (!disableVersionCheck) { switch (direction) { case "promote": { if ( compareDeploymentVersions(currentPromotion.deployment.version, deployment.version) >= 0 ) { throw new ServiceValidationError( "Cannot promote a deployment that is older than the current deployment." ); } break; } case "rollback": { if ( compareDeploymentVersions(currentPromotion.deployment.version, deployment.version) <= 0 ) { throw new ServiceValidationError( "Cannot rollback to a deployment that is newer than the current deployment." ); } break; } } } } //set this deployment as the current deployment for this environment await this._prisma.workerDeploymentPromotion.upsert({ where: { environmentId_label: { environmentId: deployment.environmentId, label: CURRENT_DEPLOYMENT_LABEL, }, }, create: { deploymentId: deployment.id, environmentId: deployment.environmentId, label: CURRENT_DEPLOYMENT_LABEL, }, update: { deploymentId: deployment.id, }, }); const [fetchTasksError, tasks] = await tryCatch( this._prisma.backgroundWorkerTask.findMany({ where: { workerId: deployment.workerId! }, select: { slug: true, triggerSource: true, ttl: true, queue: { select: { id: true, name: true } }, }, }) ); if (fetchTasksError) { logger.error("Error fetching worker tasks on deployment change", { error: fetchTasksError, }); } if (tasks) { // Side effect 1: refresh the `TaskIdentifier` table and the existing // `tids:` Redis cache so the task-listing UI reflects the new deploy. const [syncIdentifiersError] = await tryCatch( syncTaskIdentifiers( deployment.environmentId, deployment.projectId, deployment.workerId!, tasks.map((t) => ({ id: t.slug, triggerSource: t.triggerSource })) ) ); if (syncIdentifiersError) { logger.error("Error syncing task identifiers on deployment change", { error: syncIdentifiersError, }); } // Side effect 2: refresh the `task-meta:` cache that the queue resolver // reads from. Independent of side effect 1 — if `syncTaskIdentifiers` // throws, the queue resolver still gets a warm cache for the new worker. const metadataEntries: TaskMetadataEntry[] = tasks.map((t) => ({ slug: t.slug, ttl: t.ttl, triggerSource: t.triggerSource, queueId: t.queue?.id ?? null, queueName: t.queue?.name ?? "", })); // Cache calls log+swallow internally. await this._taskMetaCache.populateByCurrentWorker( deployment.environmentId, deployment.workerId!, metadataEntries ); } const [scheduleSyncError] = await tryCatch(this.#syncSchedulesForDeployment(deployment)); if (scheduleSyncError) { logger.error("Error syncing declarative schedules on deployment change", { error: scheduleSyncError, }); } // Only V1 engine workers need the WAITING_FOR_DEPLOY drain — V2 runs sit // in PENDING_VERSION and are handled out of band, so enqueuing here for V2 // just produces empty scans of the TaskRun status index. const worker = await this._prisma.backgroundWorker.findFirst({ where: { id: deployment.workerId }, select: { engine: true }, }); if (worker?.engine === "V1") { await ExecuteTasksWaitingForDeployService.enqueue(deployment.workerId); } } async #syncSchedulesForDeployment(deployment: WorkerDeployment) { const worker = await this._prisma.backgroundWorker.findFirst({ where: { id: deployment.workerId! }, }); if (!worker) { logger.error("Worker not found for deployment schedule sync", { deploymentId: deployment.id, workerId: deployment.workerId, }); return; } const parsed = BackgroundWorkerMetadata.safeParse(worker.metadata); if (!parsed.success) { logger.error("Failed to parse worker metadata for schedule sync", { deploymentId: deployment.id, workerId: deployment.workerId, error: parsed.error, }); return; } const environment = await this._prisma.runtimeEnvironment.findFirst({ where: { id: deployment.environmentId }, include: { project: true, organization: true, orgMember: true, }, }); if (!environment) { logger.error("Environment not found for deployment schedule sync", { deploymentId: deployment.id, environmentId: deployment.environmentId, }); return; } await syncDeclarativeSchedules(parsed.data.tasks, worker, environment, this._prisma); } }