11import { parseNaturalLanguageDuration } from "@trigger.dev/core/v3/isomorphic" ;
22import { TaskRunError } from "@trigger.dev/core/v3/schemas" ;
3- import { PrismaClientOrTransaction , TaskRunStatus } from "@trigger.dev/database" ;
3+ import { Prisma , PrismaClientOrTransaction , TaskRunStatus } from "@trigger.dev/database" ;
44import { isExecuting } from "../statuses.js" ;
55import { getLatestExecutionSnapshot } from "./executionSnapshotSystem.js" ;
66import { SystemResources } from "./systems.js" ;
77import { WaitpointSystem } from "./waitpointSystem.js" ;
88import { startSpan } from "@internal/tracing" ;
9+ import pMap from "p-map" ;
910
1011export type TtlSystemOptions = {
1112 resources : SystemResources ;
@@ -169,7 +170,7 @@ export class TtlSystem {
169170 const expired : string [ ] = [ ] ;
170171 const skipped : { runId : string ; reason : string } [ ] = [ ] ;
171172
172- // Fetch all runs with their snapshots in a single query
173+ // Fetch all runs in a single query (no snapshot data needed)
173174 const runs = await this . $ . prisma . taskRun . findMany ( {
174175 where : { id : { in : runIds } } ,
175176 select : {
@@ -188,36 +189,13 @@ export class TtlSystem {
188189 projectId : true ,
189190 } ,
190191 } ,
191- executionSnapshots : {
192- orderBy : { createdAt : "desc" } ,
193- take : 1 ,
194- select : {
195- executionStatus : true ,
196- environmentId : true ,
197- environmentType : true ,
198- projectId : true ,
199- organizationId : true ,
200- } ,
201- } ,
202192 } ,
203193 } ) ;
204194
205195 // Filter runs that can be expired
206196 const runsToExpire : typeof runs = [ ] ;
207197
208198 for ( const run of runs ) {
209- const latestSnapshot = run . executionSnapshots [ 0 ] ;
210-
211- if ( ! latestSnapshot ) {
212- skipped . push ( { runId : run . id , reason : "no_snapshot" } ) ;
213- continue ;
214- }
215-
216- if ( isExecuting ( latestSnapshot . executionStatus ) ) {
217- skipped . push ( { runId : run . id , reason : "executing" } ) ;
218- continue ;
219- }
220-
221199 if ( run . status !== "PENDING" ) {
222200 skipped . push ( { runId : run . id , reason : `status_${ run . status } ` } ) ;
223201 continue ;
@@ -245,79 +223,70 @@ export class TtlSystem {
245223 return { expired, skipped } ;
246224 }
247225
248- // Update all runs in a single batch
226+ // Update all runs in a single SQL call (status, dates, and error JSON)
249227 const now = new Date ( ) ;
250228 const runIdsToExpire = runsToExpire . map ( ( r ) => r . id ) ;
251229
252- await this . $ . prisma . taskRun . updateMany ( {
253- where : { id : { in : runIdsToExpire } } ,
254- data : {
255- status : "EXPIRED" as TaskRunStatus ,
256- completedAt : now ,
257- expiredAt : now ,
258- // Note: updateMany doesn't support nested writes, so we handle error and snapshots separately
259- } ,
260- } ) ;
230+ const error : TaskRunError = {
231+ type : "STRING_ERROR" ,
232+ raw : "Run expired because the TTL was reached" ,
233+ } ;
234+
235+ await this . $ . prisma . $executeRaw `
236+ UPDATE "TaskRun"
237+ SET "status" = 'EXPIRED'::"TaskRunStatus",
238+ "completedAt" = ${ now } ,
239+ "expiredAt" = ${ now } ,
240+ "updatedAt" = ${ now } ,
241+ "error" = ${ JSON . stringify ( error ) } ::jsonb
242+ WHERE "id" IN (${ Prisma . join ( runIdsToExpire ) } )
243+ ` ;
244+
245+ // Process each run: enqueue waitpoint completion jobs and emit events
246+ await pMap (
247+ runsToExpire ,
248+ async ( run ) => {
249+ try {
250+ // Enqueue a finishWaitpoint worker job for resilient waitpoint completion
251+ if ( run . associatedWaitpoint ) {
252+ await this . $ . worker . enqueue ( {
253+ id : `finishWaitpoint.ttl.${ run . associatedWaitpoint . id } ` ,
254+ job : "finishWaitpoint" ,
255+ payload : {
256+ waitpointId : run . associatedWaitpoint . id ,
257+ error : JSON . stringify ( error ) ,
258+ } ,
259+ } ) ;
260+ }
261+
262+ // Emit event
263+ this . $ . eventBus . emit ( "runExpired" , {
264+ run : {
265+ id : run . id ,
266+ spanId : run . spanId ,
267+ ttl : run . ttl ,
268+ taskEventStore : run . taskEventStore ,
269+ createdAt : run . createdAt ,
270+ updatedAt : now ,
271+ completedAt : now ,
272+ expiredAt : now ,
273+ status : "EXPIRED" as TaskRunStatus ,
274+ } ,
275+ time : now ,
276+ organization : { id : run . runtimeEnvironment . organizationId } ,
277+ project : { id : run . runtimeEnvironment . projectId } ,
278+ environment : { id : run . runtimeEnvironment . id } ,
279+ } ) ;
261280
262- // Create snapshots and set errors for each run (these require individual updates)
263- await Promise . all (
264- runsToExpire . map ( async ( run ) => {
265- const latestSnapshot = run . executionSnapshots [ 0 ] ! ;
266- const error : TaskRunError = {
267- type : "STRING_ERROR" ,
268- raw : `Run expired because the TTL (${ run . ttl } ) was reached` ,
269- } ;
270-
271- // Update the error field (updateMany can't do JSON fields properly)
272- await this . $ . prisma . taskRun . update ( {
273- where : { id : run . id } ,
274- data : { error } ,
275- } ) ;
276-
277- // Create the snapshot
278- await this . $ . prisma . taskRunExecutionSnapshot . create ( {
279- data : {
281+ expired . push ( run . id ) ;
282+ } catch ( e ) {
283+ this . $ . logger . error ( "Failed to process expired run" , {
280284 runId : run . id ,
281- engine : "V2" ,
282- executionStatus : "FINISHED" ,
283- description : "Run was expired because the TTL was reached" ,
284- runStatus : "EXPIRED" ,
285- environmentId : latestSnapshot . environmentId ,
286- environmentType : latestSnapshot . environmentType ,
287- projectId : latestSnapshot . projectId ,
288- organizationId : latestSnapshot . organizationId ,
289- } ,
290- } ) ;
291-
292- // Complete the waitpoint
293- if ( run . associatedWaitpoint ) {
294- await this . waitpointSystem . completeWaitpoint ( {
295- id : run . associatedWaitpoint . id ,
296- output : { value : JSON . stringify ( error ) , isError : true } ,
285+ error : e ,
297286 } ) ;
298287 }
299-
300- // Emit event
301- this . $ . eventBus . emit ( "runExpired" , {
302- run : {
303- id : run . id ,
304- spanId : run . spanId ,
305- ttl : run . ttl ,
306- taskEventStore : run . taskEventStore ,
307- createdAt : run . createdAt ,
308- updatedAt : now ,
309- completedAt : now ,
310- expiredAt : now ,
311- status : "EXPIRED" as TaskRunStatus ,
312- } ,
313- time : now ,
314- organization : { id : run . runtimeEnvironment . organizationId } ,
315- project : { id : run . runtimeEnvironment . projectId } ,
316- environment : { id : run . runtimeEnvironment . id } ,
317- } ) ;
318-
319- expired . push ( run . id ) ;
320- } )
288+ } ,
289+ { concurrency : 10 , stopOnError : false }
321290 ) ;
322291
323292 span . setAttribute ( "expiredCount" , expired . length ) ;
0 commit comments