@@ -13,7 +13,6 @@ import {
1313import type { PoolTask } from './types' ;
1414
1515const WORKER_START_TIMEOUT_MS = 90_000 ;
16- const WORKER_STOP_TIMEOUT_MS = 60_000 ;
1716const MAX_STDERR_MESSAGE_BYTES = 64 * 1024 ;
1817
1918function formatCapturedStderr ( text : string ) : string {
@@ -90,8 +89,6 @@ export class PoolRunner {
9089 private startDeferred : Deferred | undefined ;
9190 private stopDeferred : Deferred | undefined ;
9291 private startTimer : NodeJS . Timeout | undefined ;
93- private stopTimer : NodeJS . Timeout | undefined ;
94- private forceKillTimer : NodeJS . Timeout | undefined ;
9592 private lastFatalError : Error | undefined ;
9693 /**
9794 * Set when the worker reports `fatal_error` or a transport error. The
@@ -180,14 +177,23 @@ export class PoolRunner {
180177 return this . runTaskInternal ( 'collect' , task ) as Promise < CollectTaskResult > ;
181178 }
182179
180+ /**
181+ * Host owns termination — no IPC handshake. Per-task teardown runs in
182+ * `runInPool`'s own `finally` before `runFinished`, so by `stop()` there
183+ * is nothing process-level to drain. Relying on the worker's own
184+ * `process.exit()` was the rstest#1275 hang.
185+ */
183186 stop ( options ?: { force ?: boolean } ) : Promise < void > {
184187 return this . runOperation ( async ( ) => {
185188 switch ( this . state ) {
186189 case 'STOPPED' :
187190 case 'IDLE' :
188191 return ;
189192 case 'STOPPING' : {
190- // Wait for the in-flight stop, then optionally force.
193+ // Wait for the in-flight stop to settle. If the caller asks for
194+ // `force` and the prior stop was graceful, escalate to SIGKILL —
195+ // the prior `await` may have resolved without actually killing
196+ // the child (e.g. SIGTERM masked).
191197 if ( this . stopDeferred ) {
192198 await this . stopDeferred . promise ;
193199 }
@@ -216,34 +222,7 @@ export class PoolRunner {
216222 this . state = 'STOPPING' ;
217223 this . stopDeferred = createDeferred ( ) ;
218224
219- // Best-effort graceful stop. The worker defers its own exit until
220- // after any in-flight task completes teardown.
221- try {
222- this . worker . send ( { type : 'stop' } ) ;
223- } catch {
224- // ignore: worker may already be down
225- }
226-
227- // Escalate to SIGTERM after the budget expires, then SIGKILL shortly
228- // after. On Windows, SIGTERM is unconditionally fatal (the process
229- // cannot trap it), so sending it immediately would kill workers with
230- // in-flight tasks. Instead, rely on the IPC `stop` message for
231- // graceful shutdown and only signal when the worker fails to exit in
232- // time.
233- this . stopTimer = setTimeout ( ( ) => {
234- void this . worker . stop ( ) . catch ( ( ) => undefined ) ;
235- } , WORKER_STOP_TIMEOUT_MS ) ;
236- this . stopTimer . unref ( ) ;
237- // Arm a hard SIGKILL fallback independently so it fires even if
238- // SIGTERM is trapped/ignored and `worker.stop()` never resolves.
239- this . forceKillTimer = setTimeout ( ( ) => {
240- void this . worker . stop ( { force : true } ) . catch ( ( ) => undefined ) ;
241- } , WORKER_STOP_TIMEOUT_MS + 5_000 ) ;
242- this . forceKillTimer . unref ( ) ;
243-
244- if ( options ?. force ) {
245- await this . worker . stop ( { force : true } ) ;
246- }
225+ await this . worker . stop ( { force : options ?. force ?? false } ) ;
247226 await this . stopDeferred . promise ;
248227 } ) ;
249228 }
@@ -351,10 +330,6 @@ export class PoolRunner {
351330 case 'collectFinished' :
352331 this . resolveTask ( 'collect' , response . taskId , response . result ) ;
353332 return ;
354- case 'stopped' :
355- // Worker acknowledged graceful shutdown — actual transition happens
356- // in `handleExit`.
357- return ;
358333 case 'fatal_error' : {
359334 const error = deserializeError ( response . error ) ;
360335 // Mark as crashed BEFORE rejecting. The host's dispatch unwinds via
@@ -384,7 +359,6 @@ export class PoolRunner {
384359 }
385360
386361 private handleExit ( code : number | null , signal : NodeJS . Signals | null ) : void {
387- this . clearStopTimer ( ) ;
388362 this . clearStartTimer ( ) ;
389363
390364 const wasStopping = this . state === 'STOPPING' ;
@@ -470,15 +444,4 @@ export class PoolRunner {
470444 clearTimeout ( this . startTimer ) ;
471445 this . startTimer = undefined ;
472446 }
473-
474- private clearStopTimer ( ) : void {
475- if ( this . stopTimer ) {
476- clearTimeout ( this . stopTimer ) ;
477- this . stopTimer = undefined ;
478- }
479- if ( this . forceKillTimer ) {
480- clearTimeout ( this . forceKillTimer ) ;
481- this . forceKillTimer = undefined ;
482- }
483- }
484447}
0 commit comments