11import { createLogger , type Logger } from '@sim/logger'
22import { toError } from '@sim/utils/errors'
3- import { isExecutionCancelled , isRedisCancellationEnabled } from '@/lib/execution/cancellation'
3+ import {
4+ getCancellationChannel ,
5+ isExecutionCancelled ,
6+ isRedisCancellationEnabled ,
7+ } from '@/lib/execution/cancellation'
48import { BlockType } from '@/executor/constants'
59import type { DAG } from '@/executor/dag/builder'
610import type { EdgeManager } from '@/executor/execution/edge-manager'
@@ -31,11 +35,9 @@ export class ExecutionEngine {
3135 private errorFlag = false
3236 private stoppedEarlyFlag = false
3337 private executionError : Error | null = null
34- private lastCancellationCheck = 0
35- private readonly useRedisCancellation : boolean
36- private readonly CANCELLATION_CHECK_INTERVAL_MS = 500
37- private abortPromise : Promise < void > | null = null
38- private abortResolve : ( ( ) => void ) | null = null
38+ private abortPromise ! : Promise < void >
39+ private abortResolve ! : ( ) => void
40+ private cancellationUnsubscribe : ( ( ) => void ) | null = null
3941 private execLogger : Logger
4042
4143 constructor (
@@ -45,7 +47,6 @@ export class ExecutionEngine {
4547 private nodeOrchestrator : NodeExecutionOrchestrator
4648 ) {
4749 this . allowResumeTriggers = this . context . metadata . resumeFromSnapshot === true
48- this . useRedisCancellation = isRedisCancellationEnabled ( ) && ! ! this . context . executionId
4950 this . execLogger = logger . withMetadata ( {
5051 workflowId : this . context . workflowId ,
5152 workspaceId : this . context . workspaceId ,
@@ -54,72 +55,64 @@ export class ExecutionEngine {
5455 requestId : this . context . metadata . requestId ,
5556 } )
5657 this . initializeAbortHandler ( )
58+ this . subscribeToCancellationChannel ( )
59+ }
60+
61+ private subscribeToCancellationChannel ( ) : void {
62+ if ( ! this . context . executionId ) return
63+ const executionId = this . context . executionId
64+ this . cancellationUnsubscribe = getCancellationChannel ( ) . subscribe ( ( event ) => {
65+ if ( event . executionId !== executionId ) return
66+ this . execLogger . info ( 'Execution cancelled via pub/sub' , { executionId } )
67+ this . signalCancelled ( )
68+ } )
5769 }
5870
59- /**
60- * Sets up a single abort promise that can be reused throughout execution.
61- * This avoids creating multiple event listeners and potential memory leaks.
62- */
6371 private initializeAbortHandler ( ) : void {
72+ this . abortPromise = new Promise < void > ( ( resolve ) => {
73+ this . abortResolve = resolve
74+ } )
75+
6476 if ( ! this . context . abortSignal ) return
6577
6678 if ( this . context . abortSignal . aborted ) {
67- this . cancelledFlag = true
68- this . abortPromise = Promise . resolve ( )
79+ this . signalCancelled ( )
6980 return
7081 }
7182
72- this . abortPromise = new Promise < void > ( ( resolve ) => {
73- this . abortResolve = resolve
74- } )
75-
76- this . context . abortSignal . addEventListener (
77- 'abort' ,
78- ( ) => {
79- this . cancelledFlag = true
80- this . abortResolve ?.( )
81- } ,
82- { once : true }
83- )
83+ this . context . abortSignal . addEventListener ( 'abort' , ( ) => this . signalCancelled ( ) , { once : true } )
8484 }
8585
86- private async checkCancellation ( ) : Promise < boolean > {
87- if ( this . cancelledFlag ) {
88- return true
89- }
90-
91- if ( this . useRedisCancellation ) {
92- const now = Date . now ( )
93- if ( now - this . lastCancellationCheck < this . CANCELLATION_CHECK_INTERVAL_MS ) {
94- return false
95- }
96- this . lastCancellationCheck = now
86+ private signalCancelled ( ) : void {
87+ if ( this . cancelledFlag ) return
88+ this . cancelledFlag = true
89+ this . abortResolve ( )
90+ }
9791
98- const cancelled = await isExecutionCancelled ( this . context . executionId ! )
99- if ( cancelled ) {
100- this . cancelledFlag = true
101- this . execLogger . info ( 'Execution cancelled via Redis' , {
102- executionId : this . context . executionId ,
103- } )
104- }
105- return cancelled
106- }
92+ private checkCancellation ( ) : boolean {
93+ return this . cancelledFlag
94+ }
10795
108- if ( this . context . abortSignal ?. aborted ) {
109- this . cancelledFlag = true
110- return true
96+ /** Catches cancellations published before this engine subscribed (e.g. resume from snapshot). */
97+ private async checkCancellationBackstop ( ) : Promise < void > {
98+ if ( ! this . context . executionId || ! isRedisCancellationEnabled ( ) ) return
99+ const cancelled = await isExecutionCancelled ( this . context . executionId )
100+ if ( cancelled ) {
101+ this . execLogger . info ( 'Execution already cancelled at engine start (Redis backstop)' , {
102+ executionId : this . context . executionId ,
103+ } )
104+ this . signalCancelled ( )
111105 }
112-
113- return false
114106 }
115107
116108 async run ( triggerBlockId ?: string ) : Promise < ExecutionResult > {
117109 const startTime = performance . now ( )
118110 try {
119111 this . initializeQueue ( triggerBlockId )
112+ await this . checkCancellationBackstop ( )
120113
121114 while ( this . hasWork ( ) ) {
122- if ( ( await this . checkCancellation ( ) ) || this . errorFlag || this . stoppedEarlyFlag ) {
115+ if ( this . checkCancellation ( ) || this . errorFlag || this . stoppedEarlyFlag ) {
123116 break
124117 }
125118 await this . processQueue ( )
@@ -194,6 +187,15 @@ export class ExecutionEngine {
194187 attachExecutionResult ( error , executionResult )
195188 }
196189 throw error
190+ } finally {
191+ this . cleanup ( )
192+ }
193+ }
194+
195+ private cleanup ( ) : void {
196+ if ( this . cancellationUnsubscribe ) {
197+ this . cancellationUnsubscribe ( )
198+ this . cancellationUnsubscribe = null
197199 }
198200 }
199201
@@ -238,32 +240,17 @@ export class ExecutionEngine {
238240
239241 private async waitForAnyExecution ( ) : Promise < void > {
240242 if ( this . executing . size > 0 ) {
241- const abortPromise = this . getAbortPromise ( )
242- if ( abortPromise ) {
243- await Promise . race ( [ ...this . executing , abortPromise ] )
244- } else {
245- await Promise . race ( this . executing )
246- }
243+ await Promise . race ( [ ...this . executing , this . abortPromise ] )
247244 }
248245 }
249246
250247 private async waitForAllExecutions ( ) : Promise < void > {
251- const abortPromise = this . getAbortPromise ( )
252- if ( abortPromise ) {
253- await Promise . race ( [ Promise . all ( this . executing ) , abortPromise ] )
254- } else {
255- await Promise . all ( this . executing )
248+ await Promise . race ( [ Promise . all ( this . executing ) , this . abortPromise ] )
249+ if ( this . executing . size > 0 ) {
250+ await Promise . allSettled ( this . executing )
256251 }
257252 }
258253
259- /**
260- * Returns the cached abort promise. This is safe to call multiple times
261- * as it reuses the same promise instance created during initialization.
262- */
263- private getAbortPromise ( ) : Promise < void > | null {
264- return this . abortPromise
265- }
266-
267254 private async withQueueLock < T > ( fn : ( ) => Promise < T > | T ) : Promise < T > {
268255 const prevLock = this . queueLock
269256 let resolveLock : ( ) => void
@@ -363,7 +350,7 @@ export class ExecutionEngine {
363350
364351 private async processQueue ( ) : Promise < void > {
365352 while ( this . readyQueue . length > 0 ) {
366- if ( ( await this . checkCancellation ( ) ) || this . errorFlag ) {
353+ if ( this . checkCancellation ( ) || this . errorFlag ) {
367354 break
368355 }
369356 const nodeId = this . dequeue ( )
0 commit comments