Skip to content

Commit b80da9a

Browse files
authored
test(typescript): deflake durable eviction test (#5246)
1 parent 9f572df commit b80da9a

3 files changed

Lines changed: 137 additions & 4 deletions

File tree

‎examples/typescript/durable_eviction/graceful-worker.ts‎

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎sdks/typescript/src/v1/examples/durable_eviction/durable_eviction.e2e.ts‎

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import 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';
36
import Hatchet from '@hatchet/index';
47
import { makeE2EClient, poll, checkDurableEvictionSupport } from '../__e2e__/harness';
58
import {
@@ -17,6 +20,7 @@ import {
1720
EVENT_KEY,
1821
evictableSleepForGracefulTermination,
1922
} from './workflow';
23+
import { DURABLE_WAIT_MARKED } from './graceful-worker';
2024

2125
const 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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { DurableEvictionManager } from '@hatchet/v1/client/worker/eviction/eviction-manager';
2+
import { hatchet } from '../hatchet-client';
3+
import { evictableSleepForGracefulTermination } from './workflow';
4+
5+
export const DURABLE_WAIT_MARKED = '[e2e] durable wait marked';
6+
7+
// Worker for the graceful termination e2e test. The test must not terminate
8+
// this worker before the SDK has recorded the run as waiting, and nothing
9+
// observable outside the process marks that moment: the engine writes the
10+
// WAIT_FOR entry before it acks, and the SDK only calls markWaiting once the
11+
// ack arrives. The eviction manager is created lazily on the worker's
12+
// internal state, so it is wrapped on assignment to print a line the test can
13+
// wait for.
14+
async function main() {
15+
const worker = await hatchet.worker('graceful-eviction-worker', {
16+
workflows: [evictableSleepForGracefulTermination],
17+
});
18+
19+
let manager: DurableEvictionManager | undefined;
20+
Object.defineProperty(worker._internal, 'evictionManager', {
21+
configurable: true,
22+
enumerable: true,
23+
get: () => manager,
24+
set: (next: DurableEvictionManager | undefined) => {
25+
manager = next;
26+
if (!next) return;
27+
const markWaiting = next.markWaiting.bind(next);
28+
next.markWaiting = (key, waitKind, resourceId) => {
29+
markWaiting(key, waitKind, resourceId);
30+
console.log(`${DURABLE_WAIT_MARKED} key=${key} kind=${waitKind} resource=${resourceId}`);
31+
};
32+
},
33+
});
34+
35+
await worker.start();
36+
}
37+
38+
if (require.main === module) {
39+
main();
40+
}

0 commit comments

Comments
 (0)