Skip to content

Commit aaaed2e

Browse files
authored
Fix flaky tests maybe (#1591)
* run worker integratio n tests wuth clean new instances each time * worker: add immediate destroy for unit tests
1 parent 1f74134 commit aaaed2e

6 files changed

Lines changed: 115 additions & 24 deletions

File tree

‎.changeset/quick-pens-close.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@openfn/ws-worker': minor
3+
---
4+
5+
Add an `immediate` option to worker destroy to tear down without waiting for runs

‎integration-tests/worker/src/init.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export const initWorker = async (
5353
lightning: `ws://localhost:${lightningPort}/worker`,
5454
secret: crypto.randomUUID(),
5555
collectionsVersion: '1.0.0',
56-
messageTimeoutSeconds: 0.01,
56+
messageTimeoutSeconds: 0.1,
5757
batchLogs: true,
5858
...workerArgs,
5959
});

‎integration-tests/worker/test/integration.test.ts‎

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ let engine;
1212
let engineLogger;
1313
let lightningPort;
1414

15-
test.before(async () => {
15+
test.beforeEach(async () => {
1616
const keys = await generateKeys();
1717
lightningPort = nextPort();
1818
lightning = initLightning(lightningPort, keys.private);
@@ -33,13 +33,10 @@ test.before(async () => {
3333
));
3434
});
3535

36-
test.afterEach(() => {
36+
test.afterEach(async () => {
3737
engineLogger._reset();
38-
});
39-
40-
test.after(async () => {
4138
lightning.destroy();
42-
await worker.destroy();
39+
await worker.destroy({ immediate: true });
4340
});
4441

4542
const createDummyWorker = () => {
@@ -653,7 +650,7 @@ test.serial('use local adaptor versions from monorepo', (t) => {
653650
test.serial("Don't send adaptor logs to stdout", (t) => {
654651
return new Promise(async (done) => {
655652
// We have to create a new worker with a different repo for this one
656-
await worker.destroy();
653+
await worker.destroy({ immediate: true });
657654
({ worker, engineLogger } = await createDummyWorker());
658655

659656
const message = 've have been expecting you meester bond';
@@ -692,7 +689,7 @@ test.serial(
692689
(t) => {
693690
return new Promise(async (done) => {
694691
// We want to create our own special worker here
695-
await worker.destroy();
692+
await worker.destroy({ immediate: true });
696693
({ worker, engineLogger } = await createDummyWorker());
697694

698695
const attempt1 = {
@@ -739,7 +736,7 @@ test.serial(
739736
test.serial('worker should exit if it has an invalid key', (t) => {
740737
return new Promise(async (done) => {
741738
if (!worker.destroyed) {
742-
await worker.destroy();
739+
await worker.destroy({ immediate: true });
743740
}
744741

745742
// generate a new, invalid, public key
@@ -792,7 +789,7 @@ test.serial('worker should exit if it has an invalid key', (t) => {
792789
test.serial('set a default timeout on the worker', (t) => {
793790
return new Promise(async (done) => {
794791
if (!worker.destroyed) {
795-
await worker.destroy();
792+
await worker.destroy({ immediate: true });
796793
}
797794

798795
({ worker } = await initWorker(lightningPort, {
@@ -842,7 +839,7 @@ test.serial('set a default timeout on the worker', (t) => {
842839
test.serial('set a timeout on a run', (t) => {
843840
return new Promise(async (done) => {
844841
if (!worker.destroyed) {
845-
await worker.destroy();
842+
await worker.destroy({ immediate: true });
846843
}
847844

848845
({ worker } = await initWorker(lightningPort, {
@@ -882,7 +879,7 @@ test.serial('set a timeout on a run', (t) => {
882879
test.serial('set a default payload limit on the worker', (t) => {
883880
return new Promise(async (done) => {
884881
if (!worker.destroyed) {
885-
await worker.destroy();
882+
await worker.destroy({ immediate: true });
886883
}
887884

888885
({ worker } = await initWorker(
@@ -923,7 +920,7 @@ test.serial('set a default payload limit on the worker', (t) => {
923920
test.serial('override the worker payload through run options', (t) => {
924921
return new Promise(async (done) => {
925922
if (!worker.destroyed) {
926-
await worker.destroy();
923+
await worker.destroy({ immediate: true });
927924
}
928925

929926
({ worker } = await initWorker(
@@ -965,7 +962,7 @@ test.serial('override the worker payload through run options', (t) => {
965962
test.serial('Redact logs which exceed the payload limit', (t) => {
966963
return new Promise(async (done) => {
967964
if (!worker.destroyed) {
968-
await worker.destroy();
965+
await worker.destroy({ immediate: true });
969966
}
970967

971968
({ worker } = await initWorker(lightningPort, {
@@ -1006,7 +1003,7 @@ test.serial('Redact logs which exceed the payload limit', (t) => {
10061003
test.serial("Don't return dataclips which exceed the payload limit", (t) => {
10071004
return new Promise(async (done) => {
10081005
if (!worker.destroyed) {
1009-
await worker.destroy();
1006+
await worker.destroy({ immediate: true });
10101007
}
10111008

10121009
({ worker } = await initWorker(lightningPort, {
@@ -1046,7 +1043,7 @@ test.serial(
10461043
"Don't send job logs to stdout when job_log_level is set to none",
10471044
(t) => {
10481045
return new Promise(async (done) => {
1049-
await worker.destroy();
1046+
await worker.destroy({ immediate: true });
10501047
({ worker, engineLogger } = await createDummyWorker());
10511048

10521049
const message = 'log that will never exist';

‎packages/ws-worker/src/api/destroy.ts‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,20 @@ import { INTERNAL_CLAIM_COMPLETE, INTERNAL_RUN_COMPLETE } from '../events';
33

44
import type { Logger } from '@openfn/logger';
55

6-
const destroy = async (app: ServerApp, logger: Logger) => {
7-
logger.info('Closing server...');
6+
export type DestroyOptions = {
7+
// Tear everything down now: don't wait for active runs or claims to complete
8+
immediate?: boolean;
9+
};
10+
11+
const destroy = async (
12+
app: ServerApp,
13+
logger: Logger,
14+
options: DestroyOptions = {}
15+
) => {
16+
const { immediate = false } = options;
17+
logger.info(
18+
immediate ? 'Closing server immediately...' : 'Closing server...'
19+
);
820

921
// Close the server AND wait for runs to complete in parallel
1022
// The http server can take a moment to close and the two
@@ -22,14 +34,21 @@ const destroy = async (app: ServerApp, logger: Logger) => {
2234
app.server.close(async () => {
2335
resolve();
2436
});
37+
38+
if (immediate) {
39+
// Don't wait for open connections to drain
40+
app.server.closeAllConnections?.();
41+
}
2542
}),
2643
(async () => {
27-
// Let any active runs complete
28-
await waitForRunsAndClaims(app, logger);
44+
if (!immediate) {
45+
// Let any active runs complete
46+
await waitForRunsAndClaims(app, logger);
47+
}
2948

3049
app.queueChannel?.leave();
3150
// Kill the engine and socket
32-
await app.engine.destroy();
51+
await app.engine.destroy(immediate);
3352
app.socket?.disconnect();
3453
})(),
3554
]);

‎packages/ws-worker/src/server.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
WORK_AVAILABLE,
1818
} from './events';
1919
import destroy from './api/destroy';
20+
import type { DestroyOptions } from './api/destroy';
2021
import { Workloop } from './api/workloop';
2122
import claim from './api/claim';
2223
import { Context, execute } from './api/execute';
@@ -87,7 +88,7 @@ export interface ServerApp extends Koa {
8788
runWorkloopMap: Record<string, Workloop>;
8889

8990
execute: ({ id, token }: ClaimRun) => Promise<void>;
90-
destroy: () => void;
91+
destroy: (options?: DestroyOptions) => Promise<void>;
9192
resumeWorkloop: (workloop?: Workloop) => void;
9293
pendingClaims: () => number;
9394

@@ -469,7 +470,7 @@ function createServer(engine: RuntimeEngine, options: ServerOptions = {}) {
469470
app.pendingClaims = () =>
470471
app.workloops.reduce((sum, w) => sum + Object.keys(w.openClaims).length, 0);
471472

472-
app.destroy = () => destroy(app, logger);
473+
app.destroy = (options?: DestroyOptions) => destroy(app, logger, options);
473474

474475
app.use(router.routes());
475476

‎packages/ws-worker/test/api/destroy.test.ts‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,3 +337,72 @@ test("don't claim after destroy", async (t) => {
337337
lightning.enqueueRun(createRun());
338338
});
339339
});
340+
341+
test.serial(
342+
'immediate destroy does not wait for an outstanding claim',
343+
async (t) => {
344+
// Delay the claim response so it is still outstanding when destroy returns
345+
initLightning({ socketDelay: 200 });
346+
await initWorker({ noLoop: true });
347+
348+
return new Promise<void>((done) => {
349+
const doDestroy = async () => {
350+
t.is(worker.pendingClaims(), 1);
351+
352+
await destroy(worker, logger, { immediate: true });
353+
354+
// A graceful destroy would block until the claim completed
355+
t.is(worker.pendingClaims(), 1);
356+
t.true(worker.destroyed);
357+
t.false(await ping());
358+
359+
done();
360+
};
361+
362+
worker.events.once(INTERNAL_CLAIM_START, () => {
363+
doDestroy();
364+
});
365+
366+
lightning.enqueueRun(createRun());
367+
worker.claim().catch(() => {});
368+
});
369+
}
370+
);
371+
372+
test.serial(
373+
'immediate destroy tells the engine to destroy instantly',
374+
async (t) => {
375+
initLightning();
376+
await initWorker();
377+
378+
let instant: boolean | undefined;
379+
const originalDestroy = worker.engine.destroy;
380+
worker.engine.destroy = async (arg?: boolean) => {
381+
instant = arg;
382+
return originalDestroy(arg);
383+
};
384+
385+
await worker.destroy({ immediate: true });
386+
387+
t.true(instant);
388+
}
389+
);
390+
391+
test.serial(
392+
'a normal destroy does not ask the engine to destroy instantly',
393+
async (t) => {
394+
initLightning();
395+
await initWorker();
396+
397+
let instant: boolean | undefined = true;
398+
const originalDestroy = worker.engine.destroy;
399+
worker.engine.destroy = async (arg?: boolean) => {
400+
instant = arg;
401+
return originalDestroy(arg);
402+
};
403+
404+
await worker.destroy();
405+
406+
t.falsy(instant);
407+
}
408+
);

0 commit comments

Comments
 (0)