@@ -799,9 +799,7 @@ export class SystemDatabase {
799799 async recordWorkflowOutput ( workflowID : string , status : WorkflowStatusInternal ) : Promise < void > {
800800 const client = await this . pool . connect ( ) ;
801801 try {
802- await this . updateWorkflowStatus ( client , workflowID , StatusString . SUCCESS , {
803- update : { output : status . output , resetDeduplicationID : true , setCompletedAt : true } ,
804- } ) ;
802+ await this . #recordWorkflowOutcome( client , workflowID , StatusString . SUCCESS , { output : status . output } ) ;
805803 } finally {
806804 client . release ( ) ;
807805 }
@@ -811,14 +809,42 @@ export class SystemDatabase {
811809 async recordWorkflowError ( workflowID : string , status : WorkflowStatusInternal ) : Promise < void > {
812810 const client = await this . pool . connect ( ) ;
813811 try {
814- await this . updateWorkflowStatus ( client , workflowID , StatusString . ERROR , {
815- update : { error : status . error , resetDeduplicationID : true , setCompletedAt : true } ,
816- } ) ;
812+ await this . #recordWorkflowOutcome( client , workflowID , StatusString . ERROR , { error : status . error } ) ;
817813 } finally {
818814 client . release ( ) ;
819815 }
820816 }
821817
818+ // Record a workflow's terminal outcome (SUCCESS or ERROR), but never overwrite
819+ // the terminal CANCELLED status: a workflow can be cancelled during its final
820+ // step, and if so it must not be able to subsequently complete. If the
821+ // workflow is cancelled, abort the function so it does not complete. This
822+ // mirrors the cancellation check done before each step.
823+ async #recordWorkflowOutcome(
824+ client : PoolClient ,
825+ workflowID : string ,
826+ status : ( typeof StatusString ) [ keyof typeof StatusString ] ,
827+ outcome : { output ?: string | null ; error ?: string | null } ,
828+ ) : Promise < void > {
829+ let cancelled = false ;
830+ try {
831+ await client . query ( 'BEGIN' ) ;
832+ await this . updateWorkflowStatus ( client , workflowID , status , {
833+ update : { ...outcome , resetDeduplicationID : true , setCompletedAt : true } ,
834+ where : { notStatus : StatusString . CANCELLED } ,
835+ throwOnFailure : false ,
836+ } ) ;
837+ cancelled = ( await this . getWorkflowStatusValue ( client , workflowID ) ) === StatusString . CANCELLED ;
838+ await client . query ( 'COMMIT' ) ;
839+ } catch ( e ) {
840+ await client . query ( 'ROLLBACK' ) ;
841+ throw e ;
842+ }
843+ if ( cancelled ) {
844+ throw new DBOSWorkflowCancelledError ( workflowID ) ;
845+ }
846+ }
847+
822848 async getPendingWorkflows ( executorID : string , appVersion : string ) : Promise < GetPendingWorkflowsOutput [ ] > {
823849 const getWorkflows = await this . pool . query < workflow_status > (
824850 `SELECT workflow_uuid, queue_name
@@ -3675,6 +3701,7 @@ export class SystemDatabase {
36753701 } ;
36763702 where ?: {
36773703 status ?: ( typeof StatusString ) [ keyof typeof StatusString ] ;
3704+ notStatus ?: ( typeof StatusString ) [ keyof typeof StatusString ] ;
36783705 } ;
36793706 throwOnFailure ?: boolean ;
36803707 } = { } ,
@@ -3739,6 +3766,10 @@ export class SystemDatabase {
37393766 const param = args . push ( where . status ) ;
37403767 whereClause += ` AND status=$${ param } ` ;
37413768 }
3769+ if ( where . notStatus ) {
3770+ const param = args . push ( where . notStatus ) ;
3771+ whereClause += ` AND status!=$${ param } ` ;
3772+ }
37423773
37433774 const result = await client . query < workflow_status > (
37443775 `UPDATE "${ this . schemaName } ".workflow_status ${ setClause } ${ whereClause } ` ,
0 commit comments