1- import { sanitizeError , TaskRunFailedExecutionResult } from "@trigger.dev/core/v3" ;
1+ import {
2+ calculateNextRetryDelay ,
3+ RetryOptions ,
4+ sanitizeError ,
5+ TaskRunExecution ,
6+ TaskRunExecutionRetry ,
7+ TaskRunFailedExecutionResult ,
8+ } from "@trigger.dev/core/v3" ;
29import { logger } from "~/services/logger.server" ;
310import { createExceptionPropertiesFromError , eventRepository } from "./eventRepository.server" ;
411import { BaseService } from "./services/baseService.server" ;
512import { FinalizeTaskRunService } from "./services/finalizeTaskRun.server" ;
6- import { FAILABLE_RUN_STATUSES } from "./taskStatus" ;
13+ import { isFailableRunStatus , isFinalAttemptStatus } from "./taskStatus" ;
14+ import { Prisma } from "@trigger.dev/database" ;
15+ import { CompleteAttemptService } from "./services/completeAttempt.server" ;
16+ import { CreateTaskRunAttemptService } from "./services/createTaskRunAttempt.server" ;
17+ import { sharedQueueTasks } from "./marqs/sharedQueueConsumer.server" ;
18+
19+ const includeAttempts = {
20+ attempts : {
21+ orderBy : {
22+ createdAt : "desc" ,
23+ } ,
24+ take : 1 ,
25+ } ,
26+ lockedBy : true ,
27+ } satisfies Prisma . TaskRunInclude ;
28+
29+ type TaskRunWithAttempts = Prisma . TaskRunGetPayload < {
30+ include : typeof includeAttempts ;
31+ } > ;
732
8- /**
9- *
10- */
1133export class FailedTaskRunService extends BaseService {
1234 public async call ( anyRunId : string , completion : TaskRunFailedExecutionResult ) {
35+ logger . debug ( "[FailedTaskRunService] Handling failed task run" , { anyRunId, completion } ) ;
36+
1337 const isFriendlyId = anyRunId . startsWith ( "run_" ) ;
1438
1539 const taskRun = await this . _prisma . taskRun . findUnique ( {
1640 where : {
1741 friendlyId : isFriendlyId ? anyRunId : undefined ,
1842 id : ! isFriendlyId ? anyRunId : undefined ,
1943 } ,
44+ include : includeAttempts ,
2045 } ) ;
2146
2247 if ( ! taskRun ) {
@@ -28,7 +53,7 @@ export class FailedTaskRunService extends BaseService {
2853 return ;
2954 }
3055
31- if ( ! FAILABLE_RUN_STATUSES . includes ( taskRun . status ) ) {
56+ if ( ! isFailableRunStatus ( taskRun . status ) ) {
3257 logger . error ( "[FailedTaskRunService] Task run is not in a failable state" , {
3358 taskRun,
3459 completion,
@@ -37,7 +62,28 @@ export class FailedTaskRunService extends BaseService {
3762 return ;
3863 }
3964
40- // No more retries, we need to fail the task run
65+ const retriableExecution = await this . #getRetriableAttemptExecution( taskRun , completion ) ;
66+
67+ if ( retriableExecution ) {
68+ logger . debug ( "[FailedTaskRunService] Completing attempt" , { taskRun, completion } ) ;
69+
70+ const executionRetry =
71+ completion . retry ?? ( await this . #getExecutionRetry( taskRun , retriableExecution ) ) ;
72+
73+ const completeAttempt = new CompleteAttemptService ( this . _prisma ) ;
74+ await completeAttempt . call ( {
75+ completion : {
76+ ...completion ,
77+ retry : executionRetry ,
78+ } ,
79+ execution : retriableExecution ,
80+ isSystemFailure : true ,
81+ } ) ;
82+
83+ return ;
84+ }
85+
86+ // No retriable execution, so we need to fail the task run
4187 logger . debug ( "[FailedTaskRunService] Failing task run" , { taskRun, completion } ) ;
4288
4389 const finalizeService = new FinalizeTaskRunService ( ) ;
@@ -66,4 +112,87 @@ export class FailedTaskRunService extends BaseService {
66112 ] ,
67113 } ) ;
68114 }
115+
116+ async #getRetriableAttemptExecution(
117+ run : TaskRunWithAttempts ,
118+ completion : TaskRunFailedExecutionResult
119+ ) : Promise < TaskRunExecution | undefined > {
120+ let attempt = run . attempts [ 0 ] ;
121+
122+ // We need to create an attempt if:
123+ // - None exists yet
124+ // - The last attempt has a final status, e.g. we failed between attempts
125+ if ( ! attempt || isFinalAttemptStatus ( attempt . status ) ) {
126+ logger . error ( "[FailedTaskRunService] No attempts found" , {
127+ run,
128+ completion,
129+ } ) ;
130+
131+ const createAttempt = new CreateTaskRunAttemptService ( this . _prisma ) ;
132+
133+ try {
134+ const { execution } = await createAttempt . call ( run . id ) ;
135+ return execution ;
136+ } catch ( error ) {
137+ logger . error ( "[FailedTaskRunService] Failed to create attempt" , {
138+ run,
139+ completion,
140+ error,
141+ } ) ;
142+
143+ return ;
144+ }
145+ }
146+
147+ // We already have an attempt with non-final status, let's use it
148+ try {
149+ const executionPayload = await sharedQueueTasks . getExecutionPayloadFromAttempt (
150+ attempt . id ,
151+ undefined ,
152+ undefined ,
153+ true
154+ ) ;
155+ return executionPayload ?. execution ;
156+ } catch ( error ) {
157+ logger . error ( "[FailedTaskRunService] Failed to get execution payload" , {
158+ run,
159+ completion,
160+ error,
161+ } ) ;
162+
163+ return ;
164+ }
165+ }
166+
167+ async #getExecutionRetry(
168+ run : TaskRunWithAttempts ,
169+ execution : TaskRunExecution
170+ ) : Promise < TaskRunExecutionRetry | undefined > {
171+ const parsedRetryConfig = RetryOptions . safeParse ( run . lockedBy ?. retryConfig ) ;
172+
173+ if ( ! parsedRetryConfig . success ) {
174+ logger . error ( "[FailedTaskRunService] Invalid retry config" , {
175+ run,
176+ execution,
177+ } ) ;
178+
179+ return ;
180+ }
181+
182+ const delay = calculateNextRetryDelay ( parsedRetryConfig . data , execution . attempt . number ) ;
183+
184+ if ( ! delay ) {
185+ logger . debug ( "[FailedTaskRunService] No more retries" , {
186+ run,
187+ execution,
188+ } ) ;
189+
190+ return ;
191+ }
192+
193+ return {
194+ timestamp : Date . now ( ) + delay ,
195+ delay,
196+ } ;
197+ }
69198}
0 commit comments