11import sleep from '@hatchet/util/sleep' ;
2- import { V1TaskStatus } from '@hatchet/clients/rest/generated/data-contracts' ;
2+ import {
3+ V1DurableEventLogKind ,
4+ V1TaskStatus ,
5+ } from '@hatchet/clients/rest/generated/data-contracts' ;
36import Hatchet from '@hatchet/index' ;
47import { makeE2EClient , poll , checkDurableEvictionSupport } from '../__e2e__/harness' ;
58import {
@@ -17,6 +20,7 @@ import {
1720 EVENT_KEY ,
1821 evictableSleepForGracefulTermination ,
1922} from './workflow' ;
23+ import { DURABLE_WAIT_MARKED } from './graceful-worker' ;
2024
2125const TS_NODE_BIN = require . resolve ( 'ts-node/dist/bin.js' ) ;
2226
@@ -114,6 +118,37 @@ describe('durable-eviction-e2e', () => {
114118 ) ;
115119 }
116120
121+ // Shutting the worker down evicts only the runs it already knows are waiting,
122+ // and a run counts as waiting once the engine has acked its waitFor. A run
123+ // reports RUNNING as soon as it is assigned, so terminating the worker on
124+ // RUNNING can land before the sleep is registered and evict nothing. Wait for
125+ // the unsatisfied WAIT_FOR entry instead. Also wait for startedAt, which is
126+ // set from the STARTED event in OLAP: the eviction's DURABLE_EVICTED event is
127+ // written to OLAP on a separate path, and a STARTED event that lands after it
128+ // flips the task back to RUNNING and clears isEvicted.
129+ async function pollUntilDurableWaitRegistered ( runId : string ) {
130+ return poll (
131+ async ( ) => {
132+ const details = await hatchet . runs . get ( runId ) ;
133+ const [ task ] = details . tasks || [ ] ;
134+ if ( ! task ?. startedAt ) return undefined ;
135+ const taskId = getTaskExternalId ( details ) ;
136+ if ( ! taskId ) return undefined ;
137+ const { data } = await hatchet . api . v1DurableTaskEventLogList ( hatchet . tenantId , taskId ) ;
138+ return data ;
139+ } ,
140+ {
141+ timeoutMs : 30_000 ,
142+ intervalMs : 500 ,
143+ shouldStop : ( entries ) =>
144+ ( entries || [ ] ) . some (
145+ ( entry ) => entry . kind === V1DurableEventLogKind . WAIT_FOR && ! entry . isSatisfied
146+ ) ,
147+ label : 'durable wait registered' ,
148+ }
149+ ) ;
150+ }
151+
117152 it ( 'non-evictable task completes normally' , async ( ) => {
118153 if ( requireEviction ( ) ) return ;
119154 const start = Date . now ( ) ;
@@ -485,14 +520,26 @@ describe('durable-eviction-e2e', () => {
485520 namespace,
486521 } ) ;
487522
488- const workerProc = await spawnWorker ( 'src/v1/examples/durable_eviction/worker.ts' , {
523+ const workerProc = await spawnWorker ( 'src/v1/examples/durable_eviction/graceful- worker.ts' , {
489524 ...process . env ,
490525 HATCHET_CLIENT_WORKER_HEALTHCHECK_ENABLED : 'true' ,
491526 HATCHET_CLIENT_WORKER_HEALTHCHECK_PORT : '8104' ,
492527 HATCHET_CLIENT_NAMESPACE : 'graceful-termination-evicts-waiting-runs' ,
493528 } ) ;
494529
495- workerProc . stdout ?. on ( 'data' , ( ) => { } ) ;
530+ // The engine's WAIT_FOR entry appears before the worker has processed the
531+ // ack, and only a run the SDK has marked as waiting is evicted on
532+ // shutdown, so the worker prints a line when that happens and the test
533+ // waits for it as well.
534+ let resolveWaitMarked : ( ) => void = ( ) => { } ;
535+ const waitMarked = new Promise < void > ( ( resolve ) => {
536+ resolveWaitMarked = resolve ;
537+ } ) ;
538+ let workerStdout = '' ;
539+ workerProc . stdout ?. on ( 'data' , ( chunk : Buffer ) => {
540+ workerStdout += chunk . toString ( ) ;
541+ if ( workerStdout . includes ( DURABLE_WAIT_MARKED ) ) resolveWaitMarked ( ) ;
542+ } ) ;
496543 workerProc . stderr ?. on ( 'data' , ( ) => { } ) ;
497544
498545 try {
@@ -520,7 +567,13 @@ describe('durable-eviction-e2e', () => {
520567
521568 const runId = await ref . getWorkflowRunId ( ) ;
522569
523- await pollUntilStatus ( runId , V1TaskStatus . RUNNING ) ;
570+ await pollUntilDurableWaitRegistered ( runId ) ;
571+ await Promise . race ( [
572+ waitMarked ,
573+ sleep ( 30_000 ) . then ( ( ) => {
574+ throw new Error ( 'worker did not mark the durable wait within 30000ms' ) ;
575+ } ) ,
576+ ] ) ;
524577
525578 workerProc . kill ( 'SIGTERM' ) ;
526579
0 commit comments