@openfn/ws-worker 0.2.9 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/index.d.ts +17 -10
- package/dist/index.js +110 -38
- package/dist/start.js +135 -51
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# ws-worker
|
|
2
2
|
|
|
3
|
+
## 0.2.11
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 22339c6: Add MAX_RUN_MEMORY env var and option to limit the memory available to each run
|
|
8
|
+
- 04ac3cc: Include duration and threadid in run-complete
|
|
9
|
+
- 340b96e: Send memory usage to lightning on run:complete
|
|
10
|
+
- Updated dependencies
|
|
11
|
+
- @openfn/engine-multi@0.2.2
|
|
12
|
+
- @openfn/runtime@0.2.1
|
|
13
|
+
|
|
14
|
+
## 0.2.10
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 30da946: Better conversion of edge conditions to only take the upstream job into account
|
|
19
|
+
- c1aa9b3: Leave attempt queue channel on disconnect
|
|
20
|
+
Allow outstanding work to finish before closing down on SIGTERM
|
|
21
|
+
- 60b6fba: Add a healthcheck at /livez and respond with 200 at root
|
|
22
|
+
- Updated dependencies [a6dd44b]
|
|
23
|
+
- @openfn/engine-multi@0.2.1
|
|
24
|
+
|
|
3
25
|
## 0.2.9
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
1
2
|
import Koa from 'koa';
|
|
2
3
|
import { SanitizePolicies, Logger } from '@openfn/logger';
|
|
3
4
|
import { Channel as Channel$1 } from 'phoenix';
|
|
5
|
+
import { Server } from 'http';
|
|
4
6
|
import { RuntimeEngine } from '@openfn/engine-multi';
|
|
5
7
|
|
|
6
8
|
type ExitReasonStrings =
|
|
@@ -87,13 +89,6 @@ interface Channel extends Channel$1 {
|
|
|
87
89
|
// join: () => ReceiveHook;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
declare type Context = {
|
|
91
|
-
channel: Channel;
|
|
92
|
-
state: AttemptState;
|
|
93
|
-
logger: Logger;
|
|
94
|
-
onFinish: (result: any) => void;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
92
|
declare const CLAIM = "claim";
|
|
98
93
|
declare type ClaimPayload = {
|
|
99
94
|
demand?: number;
|
|
@@ -154,6 +149,14 @@ declare type RunCompletePayload = ExitReason & {
|
|
|
154
149
|
output_dataclip_id?: string;
|
|
155
150
|
};
|
|
156
151
|
declare type RunCompleteReply = void;
|
|
152
|
+
declare const INTERNAL_ATTEMPT_COMPLETE = "server:attempt-complete";
|
|
153
|
+
|
|
154
|
+
declare type Context = {
|
|
155
|
+
channel: Channel;
|
|
156
|
+
state: AttemptState;
|
|
157
|
+
logger: Logger;
|
|
158
|
+
onFinish: (result: any) => void;
|
|
159
|
+
};
|
|
157
160
|
|
|
158
161
|
declare type ServerOptions = {
|
|
159
162
|
maxWorkflows?: number;
|
|
@@ -169,13 +172,17 @@ declare type ServerOptions = {
|
|
|
169
172
|
};
|
|
170
173
|
interface ServerApp extends Koa {
|
|
171
174
|
id: string;
|
|
172
|
-
socket
|
|
173
|
-
|
|
175
|
+
socket?: any;
|
|
176
|
+
queueChannel?: Channel;
|
|
174
177
|
workflows: Record<string, true | Context>;
|
|
178
|
+
destroyed: boolean;
|
|
179
|
+
events: EventEmitter;
|
|
180
|
+
server: Server;
|
|
181
|
+
engine: RuntimeEngine;
|
|
175
182
|
execute: ({ id, token }: ClaimAttempt) => Promise<void>;
|
|
176
183
|
destroy: () => void;
|
|
177
184
|
killWorkloop?: () => void;
|
|
178
185
|
}
|
|
179
186
|
declare function createServer(engine: RuntimeEngine, options?: ServerOptions): ServerApp;
|
|
180
187
|
|
|
181
|
-
export { ATTEMPT_COMPLETE, ATTEMPT_LOG, ATTEMPT_START, AttemptCompletePayload, AttemptCompleteReply, AttemptLogPayload, AttemptLogReply, AttemptStartPayload, AttemptStartReply, CLAIM, ClaimAttempt, ClaimPayload, ClaimReply, GET_ATTEMPT, GET_CREDENTIAL, GET_DATACLIP, GetAttemptPayload, GetAttemptReply, GetCredentialPayload, GetCredentialReply, GetDataClipReply, GetDataclipPayload, RUN_COMPLETE, RUN_START, RunCompletePayload, RunCompleteReply, RunStartPayload, RunStartReply, createServer as default };
|
|
188
|
+
export { ATTEMPT_COMPLETE, ATTEMPT_LOG, ATTEMPT_START, AttemptCompletePayload, AttemptCompleteReply, AttemptLogPayload, AttemptLogReply, AttemptStartPayload, AttemptStartReply, CLAIM, ClaimAttempt, ClaimPayload, ClaimReply, GET_ATTEMPT, GET_CREDENTIAL, GET_DATACLIP, GetAttemptPayload, GetAttemptReply, GetCredentialPayload, GetCredentialReply, GetDataClipReply, GetDataclipPayload, INTERNAL_ATTEMPT_COMPLETE, RUN_COMPLETE, RUN_START, RunCompletePayload, RunCompleteReply, RunStartPayload, RunStartReply, createServer as default };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/server.ts
|
|
2
|
+
import { EventEmitter as EventEmitter2 } from "node:events";
|
|
2
3
|
import Koa from "koa";
|
|
3
4
|
import bodyParser from "koa-bodyparser";
|
|
4
5
|
import koaLogger from "koa-logger";
|
|
@@ -6,6 +7,63 @@ import Router from "@koa/router";
|
|
|
6
7
|
import { humanId } from "human-id";
|
|
7
8
|
import { createMockLogger as createMockLogger2 } from "@openfn/logger";
|
|
8
9
|
|
|
10
|
+
// src/events.ts
|
|
11
|
+
var CLAIM = "claim";
|
|
12
|
+
var GET_ATTEMPT = "fetch:attempt";
|
|
13
|
+
var GET_CREDENTIAL = "fetch:credential";
|
|
14
|
+
var GET_DATACLIP = "fetch:dataclip";
|
|
15
|
+
var ATTEMPT_START = "attempt:start";
|
|
16
|
+
var ATTEMPT_COMPLETE = "attempt:complete";
|
|
17
|
+
var ATTEMPT_LOG = "attempt:log";
|
|
18
|
+
var RUN_START = "run:start";
|
|
19
|
+
var RUN_COMPLETE = "run:complete";
|
|
20
|
+
var INTERNAL_ATTEMPT_COMPLETE = "server:attempt-complete";
|
|
21
|
+
|
|
22
|
+
// src/api/destroy.ts
|
|
23
|
+
var destroy = async (app, logger) => {
|
|
24
|
+
logger.info("Closing server...");
|
|
25
|
+
await Promise.all([
|
|
26
|
+
new Promise((resolve) => {
|
|
27
|
+
app.destroyed = true;
|
|
28
|
+
app.killWorkloop?.();
|
|
29
|
+
app.queueChannel?.leave();
|
|
30
|
+
app.server.close(async () => {
|
|
31
|
+
resolve();
|
|
32
|
+
});
|
|
33
|
+
}),
|
|
34
|
+
new Promise(async (resolve) => {
|
|
35
|
+
await waitForAttempts(app, logger);
|
|
36
|
+
await app.engine.destroy();
|
|
37
|
+
app.socket?.disconnect();
|
|
38
|
+
resolve();
|
|
39
|
+
})
|
|
40
|
+
]);
|
|
41
|
+
logger.success("Server closed");
|
|
42
|
+
};
|
|
43
|
+
var waitForAttempts = (app, logger) => new Promise((resolve) => {
|
|
44
|
+
const log = () => {
|
|
45
|
+
logger.debug(
|
|
46
|
+
`Waiting for ${Object.keys(app.workflows).length} attempts to complete...`
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
const onAttemptComplete = () => {
|
|
50
|
+
if (Object.keys(app.workflows).length === 0) {
|
|
51
|
+
logger.debug("All attempts completed!");
|
|
52
|
+
app.events.off(INTERNAL_ATTEMPT_COMPLETE, onAttemptComplete);
|
|
53
|
+
resolve();
|
|
54
|
+
} else {
|
|
55
|
+
log();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
if (Object.keys(app.workflows).length) {
|
|
59
|
+
log();
|
|
60
|
+
app.events.on(INTERNAL_ATTEMPT_COMPLETE, onAttemptComplete);
|
|
61
|
+
} else {
|
|
62
|
+
resolve();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
var destroy_default = destroy;
|
|
66
|
+
|
|
9
67
|
// src/util/try-with-backoff.ts
|
|
10
68
|
var BACKOFF_MULTIPLIER = 1.15;
|
|
11
69
|
var tryWithBackoff = (fn, opts = {}) => {
|
|
@@ -49,19 +107,6 @@ var try_with_backoff_default = tryWithBackoff;
|
|
|
49
107
|
|
|
50
108
|
// src/api/claim.ts
|
|
51
109
|
import { createMockLogger } from "@openfn/logger";
|
|
52
|
-
|
|
53
|
-
// src/events.ts
|
|
54
|
-
var CLAIM = "claim";
|
|
55
|
-
var GET_ATTEMPT = "fetch:attempt";
|
|
56
|
-
var GET_CREDENTIAL = "fetch:credential";
|
|
57
|
-
var GET_DATACLIP = "fetch:dataclip";
|
|
58
|
-
var ATTEMPT_START = "attempt:start";
|
|
59
|
-
var ATTEMPT_COMPLETE = "attempt:complete";
|
|
60
|
-
var ATTEMPT_LOG = "attempt:log";
|
|
61
|
-
var RUN_START = "run:start";
|
|
62
|
-
var RUN_COMPLETE = "run:complete";
|
|
63
|
-
|
|
64
|
-
// src/api/claim.ts
|
|
65
110
|
var mockLogger = createMockLogger();
|
|
66
111
|
var claim = (app, logger = mockLogger, maxWorkers = 5) => {
|
|
67
112
|
return new Promise((resolve, reject) => {
|
|
@@ -69,8 +114,11 @@ var claim = (app, logger = mockLogger, maxWorkers = 5) => {
|
|
|
69
114
|
if (activeWorkers >= maxWorkers) {
|
|
70
115
|
return reject(new Error("Server at capacity"));
|
|
71
116
|
}
|
|
117
|
+
if (!app.queueChannel) {
|
|
118
|
+
return reject(new Error("No websocket available"));
|
|
119
|
+
}
|
|
72
120
|
logger.debug("requesting attempt...");
|
|
73
|
-
app.
|
|
121
|
+
app.queueChannel.push(CLAIM, { demand: 1 }).receive("ok", ({ attempts }) => {
|
|
74
122
|
logger.debug(`pulled ${attempts.length} attempts`);
|
|
75
123
|
if (!attempts?.length) {
|
|
76
124
|
return reject(new Error("No attempts returned"));
|
|
@@ -112,6 +160,7 @@ var startWorkloop = (app, logger, minBackoff, maxBackoff, maxWorkers) => {
|
|
|
112
160
|
logger.debug("cancelling workloop");
|
|
113
161
|
cancelled = true;
|
|
114
162
|
promise.cancel();
|
|
163
|
+
app.queueChannel?.leave();
|
|
115
164
|
};
|
|
116
165
|
};
|
|
117
166
|
var workloop_default = startWorkloop;
|
|
@@ -122,13 +171,15 @@ import crypto2 from "node:crypto";
|
|
|
122
171
|
// src/util/convert-attempt.ts
|
|
123
172
|
import crypto from "node:crypto";
|
|
124
173
|
var conditions = {
|
|
125
|
-
on_job_success:
|
|
126
|
-
on_job_failure:
|
|
127
|
-
always: null
|
|
174
|
+
on_job_success: (upstreamId) => `Boolean(!state.errors?.["${upstreamId}"] ?? true)`,
|
|
175
|
+
on_job_failure: (upstreamId) => `Boolean(state.errors && state.errors["${upstreamId}"])`,
|
|
176
|
+
always: (_upstreamId) => null
|
|
128
177
|
};
|
|
129
|
-
var mapEdgeCondition = (
|
|
178
|
+
var mapEdgeCondition = (edge) => {
|
|
179
|
+
const { condition } = edge;
|
|
130
180
|
if (condition && condition in conditions) {
|
|
131
|
-
|
|
181
|
+
const upstream = edge.source_job_id || edge.source_trigger_id;
|
|
182
|
+
return conditions[condition](upstream);
|
|
132
183
|
}
|
|
133
184
|
return condition;
|
|
134
185
|
};
|
|
@@ -177,7 +228,7 @@ var convert_attempt_default = (attempt) => {
|
|
|
177
228
|
}
|
|
178
229
|
const next = edges.filter((e) => e.source_job_id === id).reduce((obj, edge) => {
|
|
179
230
|
const newEdge = {};
|
|
180
|
-
const condition = mapEdgeCondition(edge
|
|
231
|
+
const condition = mapEdgeCondition(edge);
|
|
181
232
|
if (condition) {
|
|
182
233
|
newEdge.condition = condition;
|
|
183
234
|
}
|
|
@@ -373,12 +424,12 @@ function onJobComplete({ channel, state }, event, error) {
|
|
|
373
424
|
state.dataclips = {};
|
|
374
425
|
}
|
|
375
426
|
state.dataclips[dataclipId] = event.state;
|
|
427
|
+
delete state.activeRun;
|
|
428
|
+
delete state.activeJob;
|
|
376
429
|
state.lastDataclipId = dataclipId;
|
|
377
430
|
event.next?.forEach((nextJobId) => {
|
|
378
431
|
state.inputDataclips[nextJobId] = dataclipId;
|
|
379
432
|
});
|
|
380
|
-
delete state.activeRun;
|
|
381
|
-
delete state.activeJob;
|
|
382
433
|
const { reason, error_message, error_type } = calculateJobExitReason(
|
|
383
434
|
job_id,
|
|
384
435
|
event.state,
|
|
@@ -392,7 +443,10 @@ function onJobComplete({ channel, state }, event, error) {
|
|
|
392
443
|
output_dataclip: stringify_default(event.state),
|
|
393
444
|
reason,
|
|
394
445
|
error_message,
|
|
395
|
-
error_type
|
|
446
|
+
error_type,
|
|
447
|
+
mem: event.mem,
|
|
448
|
+
duration: event.duration,
|
|
449
|
+
thread_id: event.threadId
|
|
396
450
|
};
|
|
397
451
|
return sendEvent(channel, RUN_COMPLETE, evt);
|
|
398
452
|
}
|
|
@@ -446,6 +500,11 @@ async function loadCredential(channel, credentialId) {
|
|
|
446
500
|
return get_with_reply_default(channel, GET_CREDENTIAL, { id: credentialId });
|
|
447
501
|
}
|
|
448
502
|
|
|
503
|
+
// src/middleware/healthcheck.ts
|
|
504
|
+
var healthcheck_default = (ctx) => {
|
|
505
|
+
ctx.status = 200;
|
|
506
|
+
};
|
|
507
|
+
|
|
449
508
|
// src/channels/attempt.ts
|
|
450
509
|
var joinAttemptChannel = (socket, token, attemptId, logger) => {
|
|
451
510
|
return new Promise((resolve, reject) => {
|
|
@@ -481,14 +540,14 @@ import { WebSocket } from "ws";
|
|
|
481
540
|
// src/util/worker-token.ts
|
|
482
541
|
import * as jose from "jose";
|
|
483
542
|
var alg = "HS256";
|
|
484
|
-
var generateWorkerToken = async (secret, workerId) => {
|
|
543
|
+
var generateWorkerToken = async (secret, workerId, logger) => {
|
|
485
544
|
if (!secret) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
545
|
+
logger.warn();
|
|
546
|
+
logger.warn("WARNING: Worker Secret not provided!");
|
|
547
|
+
logger.warn(
|
|
489
548
|
"This worker will attempt to connect to Lightning with default secret"
|
|
490
549
|
);
|
|
491
|
-
|
|
550
|
+
logger.warn();
|
|
492
551
|
}
|
|
493
552
|
const encodedSecret = new TextEncoder().encode(secret || "<secret>");
|
|
494
553
|
const claims = {
|
|
@@ -502,7 +561,7 @@ var worker_token_default = generateWorkerToken;
|
|
|
502
561
|
// src/channels/worker-queue.ts
|
|
503
562
|
var connectToWorkerQueue = (endpoint, serverId, secret, logger, SocketConstructor = PhxSocket) => {
|
|
504
563
|
const events = new EventEmitter();
|
|
505
|
-
worker_token_default(secret, serverId).then((token) => {
|
|
564
|
+
worker_token_default(secret, serverId, logger).then((token) => {
|
|
506
565
|
const socket = new SocketConstructor(endpoint, {
|
|
507
566
|
params: { token },
|
|
508
567
|
transport: WebSocket
|
|
@@ -545,7 +604,7 @@ function connect(app, logger, options = {}) {
|
|
|
545
604
|
const onConnect = ({ socket, channel }) => {
|
|
546
605
|
logger.success("Connected to Lightning at", options.lightning);
|
|
547
606
|
app.socket = socket;
|
|
548
|
-
app.
|
|
607
|
+
app.queueChannel = channel;
|
|
549
608
|
if (!options.noLoop) {
|
|
550
609
|
logger.info("Starting workloop");
|
|
551
610
|
app.killWorkloop = workloop_default(
|
|
@@ -592,6 +651,8 @@ function createServer(engine, options = {}) {
|
|
|
592
651
|
const app = new Koa();
|
|
593
652
|
app.id = humanId({ separator: "-", capitalize: false });
|
|
594
653
|
const router = new Router();
|
|
654
|
+
app.events = new EventEmitter2();
|
|
655
|
+
app.engine = engine;
|
|
595
656
|
app.use(bodyParser());
|
|
596
657
|
app.use(
|
|
597
658
|
koaLogger((str, _args) => {
|
|
@@ -599,8 +660,12 @@ function createServer(engine, options = {}) {
|
|
|
599
660
|
})
|
|
600
661
|
);
|
|
601
662
|
app.workflows = {};
|
|
602
|
-
|
|
663
|
+
app.destroyed = false;
|
|
664
|
+
app.server = app.listen(port);
|
|
603
665
|
logger.success(`ws-worker ${app.id} listening on ${port}`);
|
|
666
|
+
process.send?.("READY");
|
|
667
|
+
router.get("/livez", healthcheck_default);
|
|
668
|
+
router.get("/", healthcheck_default);
|
|
604
669
|
app.execute = async ({ id, token }) => {
|
|
605
670
|
if (app.socket) {
|
|
606
671
|
app.workflows[id] = true;
|
|
@@ -612,6 +677,7 @@ function createServer(engine, options = {}) {
|
|
|
612
677
|
const onFinish = () => {
|
|
613
678
|
delete app.workflows[id];
|
|
614
679
|
attemptChannel.leave();
|
|
680
|
+
app.events.emit(INTERNAL_ATTEMPT_COMPLETE);
|
|
615
681
|
};
|
|
616
682
|
const context = execute(
|
|
617
683
|
attemptChannel,
|
|
@@ -638,19 +704,24 @@ function createServer(engine, options = {}) {
|
|
|
638
704
|
ctx.status = 204;
|
|
639
705
|
});
|
|
640
706
|
});
|
|
641
|
-
app.destroy =
|
|
642
|
-
logger.info("Closing server...");
|
|
643
|
-
server.close();
|
|
644
|
-
await engine.destroy();
|
|
645
|
-
app.killWorkloop?.();
|
|
646
|
-
logger.success("Server closed");
|
|
647
|
-
};
|
|
707
|
+
app.destroy = () => destroy_default(app, logger);
|
|
648
708
|
app.use(router.routes());
|
|
649
709
|
if (options.lightning) {
|
|
650
710
|
connect(app, logger, options);
|
|
651
711
|
} else {
|
|
652
712
|
logger.warn("No lightning URL provided");
|
|
653
713
|
}
|
|
714
|
+
let shutdown = false;
|
|
715
|
+
const exit = async (signal) => {
|
|
716
|
+
if (!shutdown) {
|
|
717
|
+
shutdown = true;
|
|
718
|
+
logger.always(`${signal} RECEIVED: CLOSING SERVER`);
|
|
719
|
+
await app.destroy();
|
|
720
|
+
process.exit();
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
process.on("SIGINT", () => exit("SIGINT"));
|
|
724
|
+
process.on("SIGTERM", () => exit("SIGTERM"));
|
|
654
725
|
app.on = (...args) => {
|
|
655
726
|
return engine.on(...args);
|
|
656
727
|
};
|
|
@@ -668,6 +739,7 @@ export {
|
|
|
668
739
|
GET_ATTEMPT,
|
|
669
740
|
GET_CREDENTIAL,
|
|
670
741
|
GET_DATACLIP,
|
|
742
|
+
INTERNAL_ATTEMPT_COMPLETE,
|
|
671
743
|
RUN_COMPLETE,
|
|
672
744
|
RUN_START,
|
|
673
745
|
src_default as default
|
package/dist/start.js
CHANGED
|
@@ -4865,6 +4865,7 @@ import createRTE from "@openfn/engine-multi";
|
|
|
4865
4865
|
|
|
4866
4866
|
// src/mock/runtime-engine.ts
|
|
4867
4867
|
import { EventEmitter } from "node:events";
|
|
4868
|
+
import crypto from "node:crypto";
|
|
4868
4869
|
import run from "@openfn/runtime";
|
|
4869
4870
|
|
|
4870
4871
|
// src/mock/resolvers.ts
|
|
@@ -4911,6 +4912,7 @@ async function createMock() {
|
|
|
4911
4912
|
}) => {
|
|
4912
4913
|
const { id, jobs } = xplan;
|
|
4913
4914
|
activeWorkflows[id] = true;
|
|
4915
|
+
const threadId = crypto.randomUUID();
|
|
4914
4916
|
for (const job of jobs) {
|
|
4915
4917
|
if (typeof job.configuration === "string") {
|
|
4916
4918
|
job.configuration = await options.resolvers?.credential?.(
|
|
@@ -4925,6 +4927,7 @@ async function createMock() {
|
|
|
4925
4927
|
log: (...args2) => {
|
|
4926
4928
|
dispatch("workflow-log", {
|
|
4927
4929
|
workflowId: id,
|
|
4930
|
+
threadId,
|
|
4928
4931
|
level: "info",
|
|
4929
4932
|
json: true,
|
|
4930
4933
|
message: args2,
|
|
@@ -4941,24 +4944,26 @@ async function createMock() {
|
|
|
4941
4944
|
notify: (name, payload) => {
|
|
4942
4945
|
dispatch(name, {
|
|
4943
4946
|
workflowId: id,
|
|
4947
|
+
threadId,
|
|
4944
4948
|
...payload
|
|
4945
4949
|
});
|
|
4946
4950
|
}
|
|
4947
4951
|
}
|
|
4948
4952
|
};
|
|
4949
4953
|
setTimeout(async () => {
|
|
4950
|
-
dispatch("workflow-start", { workflowId: id });
|
|
4954
|
+
dispatch("workflow-start", { workflowId: id, threadId });
|
|
4951
4955
|
try {
|
|
4952
4956
|
await run(xplan, void 0, opts);
|
|
4953
4957
|
} catch (e) {
|
|
4954
4958
|
dispatch("workflow-error", {
|
|
4959
|
+
threadId,
|
|
4955
4960
|
workflowId: id,
|
|
4956
4961
|
type: e.name,
|
|
4957
4962
|
message: e.message
|
|
4958
4963
|
});
|
|
4959
4964
|
}
|
|
4960
4965
|
delete activeWorkflows[id];
|
|
4961
|
-
dispatch("workflow-complete", { workflowId: id });
|
|
4966
|
+
dispatch("workflow-complete", { workflowId: id, threadId });
|
|
4962
4967
|
}, 1);
|
|
4963
4968
|
};
|
|
4964
4969
|
const getStatus = () => {
|
|
@@ -4966,17 +4971,20 @@ async function createMock() {
|
|
|
4966
4971
|
active: Object.keys(activeWorkflows).length
|
|
4967
4972
|
};
|
|
4968
4973
|
};
|
|
4974
|
+
const destroy2 = async () => true;
|
|
4969
4975
|
return {
|
|
4970
4976
|
on,
|
|
4971
4977
|
once,
|
|
4972
4978
|
execute: execute2,
|
|
4973
4979
|
getStatus,
|
|
4974
|
-
listen
|
|
4980
|
+
listen,
|
|
4981
|
+
destroy: destroy2
|
|
4975
4982
|
};
|
|
4976
4983
|
}
|
|
4977
4984
|
var runtime_engine_default = createMock;
|
|
4978
4985
|
|
|
4979
4986
|
// src/server.ts
|
|
4987
|
+
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
4980
4988
|
import Koa from "koa";
|
|
4981
4989
|
import bodyParser from "koa-bodyparser";
|
|
4982
4990
|
import koaLogger from "koa-logger";
|
|
@@ -4984,6 +4992,63 @@ import Router from "@koa/router";
|
|
|
4984
4992
|
import { humanId } from "human-id";
|
|
4985
4993
|
import { createMockLogger as createMockLogger2 } from "@openfn/logger";
|
|
4986
4994
|
|
|
4995
|
+
// src/events.ts
|
|
4996
|
+
var CLAIM = "claim";
|
|
4997
|
+
var GET_ATTEMPT = "fetch:attempt";
|
|
4998
|
+
var GET_CREDENTIAL = "fetch:credential";
|
|
4999
|
+
var GET_DATACLIP = "fetch:dataclip";
|
|
5000
|
+
var ATTEMPT_START = "attempt:start";
|
|
5001
|
+
var ATTEMPT_COMPLETE = "attempt:complete";
|
|
5002
|
+
var ATTEMPT_LOG = "attempt:log";
|
|
5003
|
+
var RUN_START = "run:start";
|
|
5004
|
+
var RUN_COMPLETE = "run:complete";
|
|
5005
|
+
var INTERNAL_ATTEMPT_COMPLETE = "server:attempt-complete";
|
|
5006
|
+
|
|
5007
|
+
// src/api/destroy.ts
|
|
5008
|
+
var destroy = async (app, logger2) => {
|
|
5009
|
+
logger2.info("Closing server...");
|
|
5010
|
+
await Promise.all([
|
|
5011
|
+
new Promise((resolve5) => {
|
|
5012
|
+
app.destroyed = true;
|
|
5013
|
+
app.killWorkloop?.();
|
|
5014
|
+
app.queueChannel?.leave();
|
|
5015
|
+
app.server.close(async () => {
|
|
5016
|
+
resolve5();
|
|
5017
|
+
});
|
|
5018
|
+
}),
|
|
5019
|
+
new Promise(async (resolve5) => {
|
|
5020
|
+
await waitForAttempts(app, logger2);
|
|
5021
|
+
await app.engine.destroy();
|
|
5022
|
+
app.socket?.disconnect();
|
|
5023
|
+
resolve5();
|
|
5024
|
+
})
|
|
5025
|
+
]);
|
|
5026
|
+
logger2.success("Server closed");
|
|
5027
|
+
};
|
|
5028
|
+
var waitForAttempts = (app, logger2) => new Promise((resolve5) => {
|
|
5029
|
+
const log = () => {
|
|
5030
|
+
logger2.debug(
|
|
5031
|
+
`Waiting for ${Object.keys(app.workflows).length} attempts to complete...`
|
|
5032
|
+
);
|
|
5033
|
+
};
|
|
5034
|
+
const onAttemptComplete = () => {
|
|
5035
|
+
if (Object.keys(app.workflows).length === 0) {
|
|
5036
|
+
logger2.debug("All attempts completed!");
|
|
5037
|
+
app.events.off(INTERNAL_ATTEMPT_COMPLETE, onAttemptComplete);
|
|
5038
|
+
resolve5();
|
|
5039
|
+
} else {
|
|
5040
|
+
log();
|
|
5041
|
+
}
|
|
5042
|
+
};
|
|
5043
|
+
if (Object.keys(app.workflows).length) {
|
|
5044
|
+
log();
|
|
5045
|
+
app.events.on(INTERNAL_ATTEMPT_COMPLETE, onAttemptComplete);
|
|
5046
|
+
} else {
|
|
5047
|
+
resolve5();
|
|
5048
|
+
}
|
|
5049
|
+
});
|
|
5050
|
+
var destroy_default = destroy;
|
|
5051
|
+
|
|
4987
5052
|
// src/util/try-with-backoff.ts
|
|
4988
5053
|
var BACKOFF_MULTIPLIER = 1.15;
|
|
4989
5054
|
var tryWithBackoff = (fn, opts = {}) => {
|
|
@@ -5027,19 +5092,6 @@ var try_with_backoff_default = tryWithBackoff;
|
|
|
5027
5092
|
|
|
5028
5093
|
// src/api/claim.ts
|
|
5029
5094
|
import { createMockLogger } from "@openfn/logger";
|
|
5030
|
-
|
|
5031
|
-
// src/events.ts
|
|
5032
|
-
var CLAIM = "claim";
|
|
5033
|
-
var GET_ATTEMPT = "fetch:attempt";
|
|
5034
|
-
var GET_CREDENTIAL = "fetch:credential";
|
|
5035
|
-
var GET_DATACLIP = "fetch:dataclip";
|
|
5036
|
-
var ATTEMPT_START = "attempt:start";
|
|
5037
|
-
var ATTEMPT_COMPLETE = "attempt:complete";
|
|
5038
|
-
var ATTEMPT_LOG = "attempt:log";
|
|
5039
|
-
var RUN_START = "run:start";
|
|
5040
|
-
var RUN_COMPLETE = "run:complete";
|
|
5041
|
-
|
|
5042
|
-
// src/api/claim.ts
|
|
5043
5095
|
var mockLogger = createMockLogger();
|
|
5044
5096
|
var claim = (app, logger2 = mockLogger, maxWorkers = 5) => {
|
|
5045
5097
|
return new Promise((resolve5, reject) => {
|
|
@@ -5047,8 +5099,11 @@ var claim = (app, logger2 = mockLogger, maxWorkers = 5) => {
|
|
|
5047
5099
|
if (activeWorkers >= maxWorkers) {
|
|
5048
5100
|
return reject(new Error("Server at capacity"));
|
|
5049
5101
|
}
|
|
5102
|
+
if (!app.queueChannel) {
|
|
5103
|
+
return reject(new Error("No websocket available"));
|
|
5104
|
+
}
|
|
5050
5105
|
logger2.debug("requesting attempt...");
|
|
5051
|
-
app.
|
|
5106
|
+
app.queueChannel.push(CLAIM, { demand: 1 }).receive("ok", ({ attempts }) => {
|
|
5052
5107
|
logger2.debug(`pulled ${attempts.length} attempts`);
|
|
5053
5108
|
if (!attempts?.length) {
|
|
5054
5109
|
return reject(new Error("No attempts returned"));
|
|
@@ -5090,23 +5145,26 @@ var startWorkloop = (app, logger2, minBackoff2, maxBackoff2, maxWorkers) => {
|
|
|
5090
5145
|
logger2.debug("cancelling workloop");
|
|
5091
5146
|
cancelled = true;
|
|
5092
5147
|
promise.cancel();
|
|
5148
|
+
app.queueChannel?.leave();
|
|
5093
5149
|
};
|
|
5094
5150
|
};
|
|
5095
5151
|
var workloop_default = startWorkloop;
|
|
5096
5152
|
|
|
5097
5153
|
// src/api/execute.ts
|
|
5098
|
-
import
|
|
5154
|
+
import crypto3 from "node:crypto";
|
|
5099
5155
|
|
|
5100
5156
|
// src/util/convert-attempt.ts
|
|
5101
|
-
import
|
|
5157
|
+
import crypto2 from "node:crypto";
|
|
5102
5158
|
var conditions = {
|
|
5103
|
-
on_job_success:
|
|
5104
|
-
on_job_failure:
|
|
5105
|
-
always: null
|
|
5159
|
+
on_job_success: (upstreamId) => `Boolean(!state.errors?.["${upstreamId}"] ?? true)`,
|
|
5160
|
+
on_job_failure: (upstreamId) => `Boolean(state.errors && state.errors["${upstreamId}"])`,
|
|
5161
|
+
always: (_upstreamId) => null
|
|
5106
5162
|
};
|
|
5107
|
-
var mapEdgeCondition = (
|
|
5163
|
+
var mapEdgeCondition = (edge) => {
|
|
5164
|
+
const { condition } = edge;
|
|
5108
5165
|
if (condition && condition in conditions) {
|
|
5109
|
-
|
|
5166
|
+
const upstream = edge.source_job_id || edge.source_trigger_id;
|
|
5167
|
+
return conditions[condition](upstream);
|
|
5110
5168
|
}
|
|
5111
5169
|
return condition;
|
|
5112
5170
|
};
|
|
@@ -5143,7 +5201,7 @@ var convert_attempt_default = (attempt) => {
|
|
|
5143
5201
|
}
|
|
5144
5202
|
if (attempt.jobs?.length) {
|
|
5145
5203
|
attempt.jobs.forEach((job) => {
|
|
5146
|
-
const id = job.id ||
|
|
5204
|
+
const id = job.id || crypto2.randomUUID();
|
|
5147
5205
|
nodes[id] = {
|
|
5148
5206
|
id,
|
|
5149
5207
|
configuration: job.credential_id,
|
|
@@ -5155,7 +5213,7 @@ var convert_attempt_default = (attempt) => {
|
|
|
5155
5213
|
}
|
|
5156
5214
|
const next = edges.filter((e) => e.source_job_id === id).reduce((obj, edge) => {
|
|
5157
5215
|
const newEdge = {};
|
|
5158
|
-
const condition = mapEdgeCondition(edge
|
|
5216
|
+
const condition = mapEdgeCondition(edge);
|
|
5159
5217
|
if (condition) {
|
|
5160
5218
|
newEdge.condition = condition;
|
|
5161
5219
|
}
|
|
@@ -5326,7 +5384,7 @@ var sendEvent = (channel, event, payload) => new Promise((resolve5, reject) => {
|
|
|
5326
5384
|
channel.push(event, payload).receive("error", reject).receive("timeout", () => reject(new Error("timeout"))).receive("ok", resolve5);
|
|
5327
5385
|
});
|
|
5328
5386
|
function onJobStart({ channel, state }, event) {
|
|
5329
|
-
state.activeRun =
|
|
5387
|
+
state.activeRun = crypto3.randomUUID();
|
|
5330
5388
|
state.activeJob = event.jobId;
|
|
5331
5389
|
const input_dataclip_id = state.inputDataclips[event.jobId];
|
|
5332
5390
|
return sendEvent(channel, RUN_START, {
|
|
@@ -5344,19 +5402,19 @@ function onJobError(context, event) {
|
|
|
5344
5402
|
}
|
|
5345
5403
|
}
|
|
5346
5404
|
function onJobComplete({ channel, state }, event, error) {
|
|
5347
|
-
const dataclipId =
|
|
5405
|
+
const dataclipId = crypto3.randomUUID();
|
|
5348
5406
|
const run_id = state.activeRun;
|
|
5349
5407
|
const job_id = state.activeJob;
|
|
5350
5408
|
if (!state.dataclips) {
|
|
5351
5409
|
state.dataclips = {};
|
|
5352
5410
|
}
|
|
5353
5411
|
state.dataclips[dataclipId] = event.state;
|
|
5412
|
+
delete state.activeRun;
|
|
5413
|
+
delete state.activeJob;
|
|
5354
5414
|
state.lastDataclipId = dataclipId;
|
|
5355
5415
|
event.next?.forEach((nextJobId) => {
|
|
5356
5416
|
state.inputDataclips[nextJobId] = dataclipId;
|
|
5357
5417
|
});
|
|
5358
|
-
delete state.activeRun;
|
|
5359
|
-
delete state.activeJob;
|
|
5360
5418
|
const { reason, error_message, error_type } = calculateJobExitReason(
|
|
5361
5419
|
job_id,
|
|
5362
5420
|
event.state,
|
|
@@ -5370,7 +5428,10 @@ function onJobComplete({ channel, state }, event, error) {
|
|
|
5370
5428
|
output_dataclip: stringify_default(event.state),
|
|
5371
5429
|
reason,
|
|
5372
5430
|
error_message,
|
|
5373
|
-
error_type
|
|
5431
|
+
error_type,
|
|
5432
|
+
mem: event.mem,
|
|
5433
|
+
duration: event.duration,
|
|
5434
|
+
thread_id: event.threadId
|
|
5374
5435
|
};
|
|
5375
5436
|
return sendEvent(channel, RUN_COMPLETE, evt);
|
|
5376
5437
|
}
|
|
@@ -5424,6 +5485,11 @@ async function loadCredential(channel, credentialId) {
|
|
|
5424
5485
|
return get_with_reply_default(channel, GET_CREDENTIAL, { id: credentialId });
|
|
5425
5486
|
}
|
|
5426
5487
|
|
|
5488
|
+
// src/middleware/healthcheck.ts
|
|
5489
|
+
var healthcheck_default = (ctx) => {
|
|
5490
|
+
ctx.status = 200;
|
|
5491
|
+
};
|
|
5492
|
+
|
|
5427
5493
|
// src/channels/attempt.ts
|
|
5428
5494
|
var joinAttemptChannel = (socket, token, attemptId, logger2) => {
|
|
5429
5495
|
return new Promise((resolve5, reject) => {
|
|
@@ -5459,14 +5525,14 @@ import { WebSocket } from "ws";
|
|
|
5459
5525
|
// src/util/worker-token.ts
|
|
5460
5526
|
import * as jose from "jose";
|
|
5461
5527
|
var alg = "HS256";
|
|
5462
|
-
var generateWorkerToken = async (secret, workerId) => {
|
|
5528
|
+
var generateWorkerToken = async (secret, workerId, logger2) => {
|
|
5463
5529
|
if (!secret) {
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5530
|
+
logger2.warn();
|
|
5531
|
+
logger2.warn("WARNING: Worker Secret not provided!");
|
|
5532
|
+
logger2.warn(
|
|
5467
5533
|
"This worker will attempt to connect to Lightning with default secret"
|
|
5468
5534
|
);
|
|
5469
|
-
|
|
5535
|
+
logger2.warn();
|
|
5470
5536
|
}
|
|
5471
5537
|
const encodedSecret = new TextEncoder().encode(secret || "<secret>");
|
|
5472
5538
|
const claims = {
|
|
@@ -5480,7 +5546,7 @@ var worker_token_default = generateWorkerToken;
|
|
|
5480
5546
|
// src/channels/worker-queue.ts
|
|
5481
5547
|
var connectToWorkerQueue = (endpoint, serverId, secret, logger2, SocketConstructor = PhxSocket) => {
|
|
5482
5548
|
const events = new EventEmitter2();
|
|
5483
|
-
worker_token_default(secret, serverId).then((token) => {
|
|
5549
|
+
worker_token_default(secret, serverId, logger2).then((token) => {
|
|
5484
5550
|
const socket = new SocketConstructor(endpoint, {
|
|
5485
5551
|
params: { token },
|
|
5486
5552
|
transport: WebSocket
|
|
@@ -5523,7 +5589,7 @@ function connect(app, logger2, options = {}) {
|
|
|
5523
5589
|
const onConnect = ({ socket, channel }) => {
|
|
5524
5590
|
logger2.success("Connected to Lightning at", options.lightning);
|
|
5525
5591
|
app.socket = socket;
|
|
5526
|
-
app.
|
|
5592
|
+
app.queueChannel = channel;
|
|
5527
5593
|
if (!options.noLoop) {
|
|
5528
5594
|
logger2.info("Starting workloop");
|
|
5529
5595
|
app.killWorkloop = workloop_default(
|
|
@@ -5570,6 +5636,8 @@ function createServer(engine, options = {}) {
|
|
|
5570
5636
|
const app = new Koa();
|
|
5571
5637
|
app.id = humanId({ separator: "-", capitalize: false });
|
|
5572
5638
|
const router = new Router();
|
|
5639
|
+
app.events = new EventEmitter3();
|
|
5640
|
+
app.engine = engine;
|
|
5573
5641
|
app.use(bodyParser());
|
|
5574
5642
|
app.use(
|
|
5575
5643
|
koaLogger((str, _args) => {
|
|
@@ -5577,8 +5645,12 @@ function createServer(engine, options = {}) {
|
|
|
5577
5645
|
})
|
|
5578
5646
|
);
|
|
5579
5647
|
app.workflows = {};
|
|
5580
|
-
|
|
5648
|
+
app.destroyed = false;
|
|
5649
|
+
app.server = app.listen(port);
|
|
5581
5650
|
logger2.success(`ws-worker ${app.id} listening on ${port}`);
|
|
5651
|
+
process.send?.("READY");
|
|
5652
|
+
router.get("/livez", healthcheck_default);
|
|
5653
|
+
router.get("/", healthcheck_default);
|
|
5582
5654
|
app.execute = async ({ id, token }) => {
|
|
5583
5655
|
if (app.socket) {
|
|
5584
5656
|
app.workflows[id] = true;
|
|
@@ -5590,6 +5662,7 @@ function createServer(engine, options = {}) {
|
|
|
5590
5662
|
const onFinish = () => {
|
|
5591
5663
|
delete app.workflows[id];
|
|
5592
5664
|
attemptChannel.leave();
|
|
5665
|
+
app.events.emit(INTERNAL_ATTEMPT_COMPLETE);
|
|
5593
5666
|
};
|
|
5594
5667
|
const context = execute(
|
|
5595
5668
|
attemptChannel,
|
|
@@ -5616,19 +5689,24 @@ function createServer(engine, options = {}) {
|
|
|
5616
5689
|
ctx.status = 204;
|
|
5617
5690
|
});
|
|
5618
5691
|
});
|
|
5619
|
-
app.destroy =
|
|
5620
|
-
logger2.info("Closing server...");
|
|
5621
|
-
server.close();
|
|
5622
|
-
await engine.destroy();
|
|
5623
|
-
app.killWorkloop?.();
|
|
5624
|
-
logger2.success("Server closed");
|
|
5625
|
-
};
|
|
5692
|
+
app.destroy = () => destroy_default(app, logger2);
|
|
5626
5693
|
app.use(router.routes());
|
|
5627
5694
|
if (options.lightning) {
|
|
5628
5695
|
connect(app, logger2, options);
|
|
5629
5696
|
} else {
|
|
5630
5697
|
logger2.warn("No lightning URL provided");
|
|
5631
5698
|
}
|
|
5699
|
+
let shutdown = false;
|
|
5700
|
+
const exit = async (signal) => {
|
|
5701
|
+
if (!shutdown) {
|
|
5702
|
+
shutdown = true;
|
|
5703
|
+
logger2.always(`${signal} RECEIVED: CLOSING SERVER`);
|
|
5704
|
+
await app.destroy();
|
|
5705
|
+
process.exit();
|
|
5706
|
+
}
|
|
5707
|
+
};
|
|
5708
|
+
process.on("SIGINT", () => exit("SIGINT"));
|
|
5709
|
+
process.on("SIGTERM", () => exit("SIGTERM"));
|
|
5632
5710
|
app.on = (...args2) => {
|
|
5633
5711
|
return engine.on(...args2);
|
|
5634
5712
|
};
|
|
@@ -5637,7 +5715,7 @@ function createServer(engine, options = {}) {
|
|
|
5637
5715
|
var server_default = createServer;
|
|
5638
5716
|
|
|
5639
5717
|
// src/start.ts
|
|
5640
|
-
var { WORKER_REPO_DIR, WORKER_SECRET } = process.env;
|
|
5718
|
+
var { WORKER_REPO_DIR, WORKER_SECRET, MAX_RUN_MEMORY } = process.env;
|
|
5641
5719
|
var args = yargs_default(hideBin(process.argv)).command("server", "Start a ws-worker server").option("port", {
|
|
5642
5720
|
alias: "p",
|
|
5643
5721
|
description: "Port to run the server on",
|
|
@@ -5673,6 +5751,10 @@ var args = yargs_default(hideBin(process.argv)).command("server", "Start a ws-wo
|
|
|
5673
5751
|
description: "max concurrent workers",
|
|
5674
5752
|
default: 5,
|
|
5675
5753
|
type: "number"
|
|
5754
|
+
}).option("run-memory", {
|
|
5755
|
+
description: "Maximum memory allocated to a single run, in mb",
|
|
5756
|
+
type: "number",
|
|
5757
|
+
default: MAX_RUN_MEMORY ? parseInt(MAX_RUN_MEMORY) : 500
|
|
5676
5758
|
}).parse();
|
|
5677
5759
|
var logger = createLogger("SRV", { level: args.log });
|
|
5678
5760
|
if (args.lightning === "mock") {
|
|
@@ -5708,10 +5790,12 @@ if (args.mock) {
|
|
|
5708
5790
|
engineReady(engine);
|
|
5709
5791
|
});
|
|
5710
5792
|
} else {
|
|
5711
|
-
createRTE({ repoDir: args.repoDir }).then(
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5793
|
+
createRTE({ repoDir: args.repoDir, memoryLimitMb: args.runMemory }).then(
|
|
5794
|
+
(engine) => {
|
|
5795
|
+
logger.debug("engine created");
|
|
5796
|
+
engineReady(engine);
|
|
5797
|
+
}
|
|
5798
|
+
);
|
|
5715
5799
|
}
|
|
5716
5800
|
/**
|
|
5717
5801
|
* @fileoverview Main entrypoint for libraries using yargs-parser in Node.js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfn/ws-worker",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "A Websocket Worker to connect Lightning to a Runtime Engine",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
"koa-logger": "^3.2.1",
|
|
22
22
|
"phoenix": "^1.7.7",
|
|
23
23
|
"ws": "^8.14.1",
|
|
24
|
-
"@openfn/engine-multi": "0.2.
|
|
24
|
+
"@openfn/engine-multi": "0.2.2",
|
|
25
25
|
"@openfn/logger": "0.0.19",
|
|
26
|
-
"@openfn/runtime": "0.2.
|
|
26
|
+
"@openfn/runtime": "0.2.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/koa": "^2.13.5",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"tsup": "^6.2.3",
|
|
41
41
|
"typescript": "^4.6.4",
|
|
42
42
|
"yargs": "^17.6.2",
|
|
43
|
-
"@openfn/lightning-mock": "1.1.
|
|
43
|
+
"@openfn/lightning-mock": "1.1.4"
|
|
44
44
|
},
|
|
45
45
|
"files": [
|
|
46
46
|
"dist",
|