11import { CreateBackgroundWorkerRequestBody , logger , tryCatch } from "@trigger.dev/core/v3" ;
2- import { BackgroundWorkerId } from "@trigger.dev/core/v3/isomorphic" ;
3- import type { BackgroundWorker , PrismaClientOrTransaction , WorkerDeployment } from "@trigger.dev/database" ;
2+ import type {
3+ BackgroundWorker ,
4+ PrismaClientOrTransaction ,
5+ WorkerDeployment ,
6+ } from "@trigger.dev/database" ;
47import { AuthenticatedEnvironment } from "~/services/apiAuth.server" ;
58import { type TaskMetadataCache } from "~/services/taskMetadataCache.server" ;
69import { taskMetadataCacheInstance } from "~/services/taskMetadataCacheInstance.server" ;
710import { BaseService , ServiceValidationError } from "./baseService.server" ;
811import {
912 createBackgroundFiles ,
1013 createWorkerResources ,
11- stripBackgroundWorkerMetadataForStorage ,
1214 syncDeclarativeSchedules ,
1315} from "./createBackgroundWorker.server" ;
16+ import { findOrCreateBackgroundWorker } from "./createDeploymentBackgroundWorkerV4/findOrCreateBackgroundWorker.server" ;
1417import { TimeoutDeploymentService } from "./timeoutDeployment.server" ;
1518import { env } from "~/env.server" ;
1619
@@ -50,6 +53,11 @@ export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService {
5053 } ) ;
5154
5255 if ( ! deployment ) {
56+ logger . warn ( "createDeploymentBackgroundWorker: deployment not found" , {
57+ deploymentId,
58+ environmentId : environment . id ,
59+ projectId : environment . projectId ,
60+ } ) ;
5361 return ;
5462 }
5563
@@ -69,26 +77,44 @@ export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService {
6977 }
7078 }
7179
80+ // Late-retry idempotency: if a worker was registered by a prior fully-
81+ // successful attempt and the deployment already moved past BUILDING, return
82+ // that worker so the CLI can finalize instead of seeing a 5xx.
83+ if ( deployment . workerId ) {
84+ const linkedWorker = await this . _prisma . backgroundWorker . findFirst ( {
85+ where : { id : deployment . workerId } ,
86+ } ) ;
87+ if ( linkedWorker ) {
88+ return linkedWorker ;
89+ }
90+ }
91+
7292 if ( deployment . status !== "BUILDING" ) {
93+ logger . warn ( "createDeploymentBackgroundWorker: deployment not in BUILDING state" , {
94+ deploymentId,
95+ deploymentStatus : deployment . status ,
96+ environmentId : environment . id ,
97+ projectId : environment . projectId ,
98+ } ) ;
7399 return ;
74100 }
75101
76- const backgroundWorker = await this . _prisma . backgroundWorker . create ( {
77- data : {
78- ... BackgroundWorkerId . generate ( ) ,
79- version : deployment . version ,
80- runtimeEnvironmentId : environment . id ,
81- projectId : environment . projectId ,
82- metadata : stripBackgroundWorkerMetadataForStorage ( body . metadata ) ,
83- contentHash : body . metadata . contentHash ,
84- cliVersion : body . metadata . cliPackageVersion ,
85- sdkVersion : body . metadata . packageVersion ,
86- supportsLazyAttempts : body . supportsLazyAttempts ,
87- engine : body . engine ,
88- runtime : body . metadata . runtime ,
89- runtimeVersion : body . metadata . runtimeVersion ,
90- } ,
91- } ) ;
102+ const [ findOrCreateError , backgroundWorker ] = await tryCatch (
103+ findOrCreateBackgroundWorker ( environment , deployment , body , this . _prisma )
104+ ) ;
105+
106+ if ( findOrCreateError ) {
107+ // Definitive failures (e.g. contentHash drift) surface as
108+ // `ServiceValidationError` — fail the deployment so the operator sees it
109+ // immediately instead of waiting 8 minutes for the timeout. Transient
110+ // races throw a plain `Error` and propagate as 5xx without failing.
111+ if ( findOrCreateError instanceof ServiceValidationError ) {
112+ // `#failBackgroundWorkerDeployment` already throws its argument; the
113+ // outer `throw` covers the non-SVE branch.
114+ await this . #failBackgroundWorkerDeployment ( deployment , findOrCreateError ) ;
115+ }
116+ throw findOrCreateError ;
117+ }
92118
93119 //upgrade the project to engine "V2" if it's not already
94120 if ( environment . project . engine === "V1" && body . engine === "V2" ) {
@@ -188,10 +214,11 @@ export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService {
188214 throw serviceError ;
189215 }
190216
191- // Link the deployment with the background worker
192- await this . _prisma . workerDeployment . update ( {
217+ // Guarded BUILDING → DEPLOYING transition. `updateMany` for optimistic concurrency control
218+ const { count : updatedCount } = await this . _prisma . workerDeployment . updateMany ( {
193219 where : {
194220 id : deployment . id ,
221+ status : "BUILDING" ,
195222 } ,
196223 data : {
197224 status : "DEPLOYING" ,
@@ -203,6 +230,18 @@ export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService {
203230 } ,
204231 } ) ;
205232
233+ if ( updatedCount === 0 ) {
234+ logger . warn (
235+ "createDeploymentBackgroundWorker: deployment no longer in BUILDING state, skipping DEPLOYING transition" ,
236+ {
237+ deploymentId,
238+ environmentId : environment . id ,
239+ projectId : environment . projectId ,
240+ }
241+ ) ;
242+ return backgroundWorker ;
243+ }
244+
206245 await TimeoutDeploymentService . enqueue (
207246 deployment . id ,
208247 "DEPLOYING" ,
@@ -215,9 +254,14 @@ export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService {
215254 }
216255
217256 async #failBackgroundWorkerDeployment( deployment : WorkerDeployment , error : Error ) {
218- await this . _prisma . workerDeployment . update ( {
257+ // Guarded BUILDING → FAILED transition, symmetric with the BUILDING → DEPLOYING
258+ // transition in `call()`. With idempotent retries, two attempts can run side-by-side;
259+ // without the predicate, one attempt's failure could downgrade the deployment after
260+ // the other already flipped it to DEPLOYING, leaving it stuck in FAILED with a worker.
261+ const { count : updatedCount } = await this . _prisma . workerDeployment . updateMany ( {
219262 where : {
220263 id : deployment . id ,
264+ status : "BUILDING" ,
221265 } ,
222266 data : {
223267 status : "FAILED" ,
@@ -229,7 +273,20 @@ export class CreateDeploymentBackgroundWorkerServiceV4 extends BaseService {
229273 } ,
230274 } ) ;
231275
232- await TimeoutDeploymentService . dequeue ( deployment . id , this . _prisma ) ;
276+ if ( updatedCount === 0 ) {
277+ logger . warn (
278+ "failBackgroundWorkerDeployment: deployment moved out of BUILDING during call, skipping FAILED transition" ,
279+ {
280+ deploymentId : deployment . id ,
281+ originalError : error . message ,
282+ }
283+ ) ;
284+ } else {
285+ // Only dequeue the timeout if we actually flipped to FAILED — otherwise a
286+ // sibling attempt may have just enqueued it as part of a successful
287+ // BUILDING → DEPLOYING transition.
288+ await TimeoutDeploymentService . dequeue ( deployment . id , this . _prisma ) ;
289+ }
233290
234291 throw error ;
235292 }
0 commit comments