@openfn/ws-worker 0.2.9 → 0.2.10
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 +11 -0
- package/dist/index.d.ts +17 -10
- package/dist/index.js +104 -35
- package/dist/start.js +106 -36
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# ws-worker
|
|
2
2
|
|
|
3
|
+
## 0.2.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 30da946: Better conversion of edge conditions to only take the upstream job into account
|
|
8
|
+
- c1aa9b3: Leave attempt queue channel on disconnect
|
|
9
|
+
Allow outstanding work to finish before closing down on SIGTERM
|
|
10
|
+
- 60b6fba: Add a healthcheck at /livez and respond with 200 at root
|
|
11
|
+
- Updated dependencies [a6dd44b]
|
|
12
|
+
- @openfn/engine-multi@0.2.1
|
|
13
|
+
|
|
3
14
|
## 0.2.9
|
|
4
15
|
|
|
5
16
|
### 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
|
}
|
|
@@ -446,6 +497,11 @@ async function loadCredential(channel, credentialId) {
|
|
|
446
497
|
return get_with_reply_default(channel, GET_CREDENTIAL, { id: credentialId });
|
|
447
498
|
}
|
|
448
499
|
|
|
500
|
+
// src/middleware/healthcheck.ts
|
|
501
|
+
var healthcheck_default = (ctx) => {
|
|
502
|
+
ctx.status = 200;
|
|
503
|
+
};
|
|
504
|
+
|
|
449
505
|
// src/channels/attempt.ts
|
|
450
506
|
var joinAttemptChannel = (socket, token, attemptId, logger) => {
|
|
451
507
|
return new Promise((resolve, reject) => {
|
|
@@ -481,14 +537,14 @@ import { WebSocket } from "ws";
|
|
|
481
537
|
// src/util/worker-token.ts
|
|
482
538
|
import * as jose from "jose";
|
|
483
539
|
var alg = "HS256";
|
|
484
|
-
var generateWorkerToken = async (secret, workerId) => {
|
|
540
|
+
var generateWorkerToken = async (secret, workerId, logger) => {
|
|
485
541
|
if (!secret) {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
542
|
+
logger.warn();
|
|
543
|
+
logger.warn("WARNING: Worker Secret not provided!");
|
|
544
|
+
logger.warn(
|
|
489
545
|
"This worker will attempt to connect to Lightning with default secret"
|
|
490
546
|
);
|
|
491
|
-
|
|
547
|
+
logger.warn();
|
|
492
548
|
}
|
|
493
549
|
const encodedSecret = new TextEncoder().encode(secret || "<secret>");
|
|
494
550
|
const claims = {
|
|
@@ -502,7 +558,7 @@ var worker_token_default = generateWorkerToken;
|
|
|
502
558
|
// src/channels/worker-queue.ts
|
|
503
559
|
var connectToWorkerQueue = (endpoint, serverId, secret, logger, SocketConstructor = PhxSocket) => {
|
|
504
560
|
const events = new EventEmitter();
|
|
505
|
-
worker_token_default(secret, serverId).then((token) => {
|
|
561
|
+
worker_token_default(secret, serverId, logger).then((token) => {
|
|
506
562
|
const socket = new SocketConstructor(endpoint, {
|
|
507
563
|
params: { token },
|
|
508
564
|
transport: WebSocket
|
|
@@ -545,7 +601,7 @@ function connect(app, logger, options = {}) {
|
|
|
545
601
|
const onConnect = ({ socket, channel }) => {
|
|
546
602
|
logger.success("Connected to Lightning at", options.lightning);
|
|
547
603
|
app.socket = socket;
|
|
548
|
-
app.
|
|
604
|
+
app.queueChannel = channel;
|
|
549
605
|
if (!options.noLoop) {
|
|
550
606
|
logger.info("Starting workloop");
|
|
551
607
|
app.killWorkloop = workloop_default(
|
|
@@ -592,6 +648,8 @@ function createServer(engine, options = {}) {
|
|
|
592
648
|
const app = new Koa();
|
|
593
649
|
app.id = humanId({ separator: "-", capitalize: false });
|
|
594
650
|
const router = new Router();
|
|
651
|
+
app.events = new EventEmitter2();
|
|
652
|
+
app.engine = engine;
|
|
595
653
|
app.use(bodyParser());
|
|
596
654
|
app.use(
|
|
597
655
|
koaLogger((str, _args) => {
|
|
@@ -599,8 +657,12 @@ function createServer(engine, options = {}) {
|
|
|
599
657
|
})
|
|
600
658
|
);
|
|
601
659
|
app.workflows = {};
|
|
602
|
-
|
|
660
|
+
app.destroyed = false;
|
|
661
|
+
app.server = app.listen(port);
|
|
603
662
|
logger.success(`ws-worker ${app.id} listening on ${port}`);
|
|
663
|
+
process.send?.("READY");
|
|
664
|
+
router.get("/livez", healthcheck_default);
|
|
665
|
+
router.get("/", healthcheck_default);
|
|
604
666
|
app.execute = async ({ id, token }) => {
|
|
605
667
|
if (app.socket) {
|
|
606
668
|
app.workflows[id] = true;
|
|
@@ -612,6 +674,7 @@ function createServer(engine, options = {}) {
|
|
|
612
674
|
const onFinish = () => {
|
|
613
675
|
delete app.workflows[id];
|
|
614
676
|
attemptChannel.leave();
|
|
677
|
+
app.events.emit(INTERNAL_ATTEMPT_COMPLETE);
|
|
615
678
|
};
|
|
616
679
|
const context = execute(
|
|
617
680
|
attemptChannel,
|
|
@@ -638,19 +701,24 @@ function createServer(engine, options = {}) {
|
|
|
638
701
|
ctx.status = 204;
|
|
639
702
|
});
|
|
640
703
|
});
|
|
641
|
-
app.destroy =
|
|
642
|
-
logger.info("Closing server...");
|
|
643
|
-
server.close();
|
|
644
|
-
await engine.destroy();
|
|
645
|
-
app.killWorkloop?.();
|
|
646
|
-
logger.success("Server closed");
|
|
647
|
-
};
|
|
704
|
+
app.destroy = () => destroy_default(app, logger);
|
|
648
705
|
app.use(router.routes());
|
|
649
706
|
if (options.lightning) {
|
|
650
707
|
connect(app, logger, options);
|
|
651
708
|
} else {
|
|
652
709
|
logger.warn("No lightning URL provided");
|
|
653
710
|
}
|
|
711
|
+
let shutdown = false;
|
|
712
|
+
const exit = async (signal) => {
|
|
713
|
+
if (!shutdown) {
|
|
714
|
+
shutdown = true;
|
|
715
|
+
logger.always(`${signal} RECEIVED: CLOSING SERVER`);
|
|
716
|
+
await app.destroy();
|
|
717
|
+
process.exit();
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
process.on("SIGINT", () => exit("SIGINT"));
|
|
721
|
+
process.on("SIGTERM", () => exit("SIGTERM"));
|
|
654
722
|
app.on = (...args) => {
|
|
655
723
|
return engine.on(...args);
|
|
656
724
|
};
|
|
@@ -668,6 +736,7 @@ export {
|
|
|
668
736
|
GET_ATTEMPT,
|
|
669
737
|
GET_CREDENTIAL,
|
|
670
738
|
GET_DATACLIP,
|
|
739
|
+
INTERNAL_ATTEMPT_COMPLETE,
|
|
671
740
|
RUN_COMPLETE,
|
|
672
741
|
RUN_START,
|
|
673
742
|
src_default as default
|
package/dist/start.js
CHANGED
|
@@ -4966,17 +4966,20 @@ async function createMock() {
|
|
|
4966
4966
|
active: Object.keys(activeWorkflows).length
|
|
4967
4967
|
};
|
|
4968
4968
|
};
|
|
4969
|
+
const destroy2 = async () => true;
|
|
4969
4970
|
return {
|
|
4970
4971
|
on,
|
|
4971
4972
|
once,
|
|
4972
4973
|
execute: execute2,
|
|
4973
4974
|
getStatus,
|
|
4974
|
-
listen
|
|
4975
|
+
listen,
|
|
4976
|
+
destroy: destroy2
|
|
4975
4977
|
};
|
|
4976
4978
|
}
|
|
4977
4979
|
var runtime_engine_default = createMock;
|
|
4978
4980
|
|
|
4979
4981
|
// src/server.ts
|
|
4982
|
+
import { EventEmitter as EventEmitter3 } from "node:events";
|
|
4980
4983
|
import Koa from "koa";
|
|
4981
4984
|
import bodyParser from "koa-bodyparser";
|
|
4982
4985
|
import koaLogger from "koa-logger";
|
|
@@ -4984,6 +4987,63 @@ import Router from "@koa/router";
|
|
|
4984
4987
|
import { humanId } from "human-id";
|
|
4985
4988
|
import { createMockLogger as createMockLogger2 } from "@openfn/logger";
|
|
4986
4989
|
|
|
4990
|
+
// src/events.ts
|
|
4991
|
+
var CLAIM = "claim";
|
|
4992
|
+
var GET_ATTEMPT = "fetch:attempt";
|
|
4993
|
+
var GET_CREDENTIAL = "fetch:credential";
|
|
4994
|
+
var GET_DATACLIP = "fetch:dataclip";
|
|
4995
|
+
var ATTEMPT_START = "attempt:start";
|
|
4996
|
+
var ATTEMPT_COMPLETE = "attempt:complete";
|
|
4997
|
+
var ATTEMPT_LOG = "attempt:log";
|
|
4998
|
+
var RUN_START = "run:start";
|
|
4999
|
+
var RUN_COMPLETE = "run:complete";
|
|
5000
|
+
var INTERNAL_ATTEMPT_COMPLETE = "server:attempt-complete";
|
|
5001
|
+
|
|
5002
|
+
// src/api/destroy.ts
|
|
5003
|
+
var destroy = async (app, logger2) => {
|
|
5004
|
+
logger2.info("Closing server...");
|
|
5005
|
+
await Promise.all([
|
|
5006
|
+
new Promise((resolve5) => {
|
|
5007
|
+
app.destroyed = true;
|
|
5008
|
+
app.killWorkloop?.();
|
|
5009
|
+
app.queueChannel?.leave();
|
|
5010
|
+
app.server.close(async () => {
|
|
5011
|
+
resolve5();
|
|
5012
|
+
});
|
|
5013
|
+
}),
|
|
5014
|
+
new Promise(async (resolve5) => {
|
|
5015
|
+
await waitForAttempts(app, logger2);
|
|
5016
|
+
await app.engine.destroy();
|
|
5017
|
+
app.socket?.disconnect();
|
|
5018
|
+
resolve5();
|
|
5019
|
+
})
|
|
5020
|
+
]);
|
|
5021
|
+
logger2.success("Server closed");
|
|
5022
|
+
};
|
|
5023
|
+
var waitForAttempts = (app, logger2) => new Promise((resolve5) => {
|
|
5024
|
+
const log = () => {
|
|
5025
|
+
logger2.debug(
|
|
5026
|
+
`Waiting for ${Object.keys(app.workflows).length} attempts to complete...`
|
|
5027
|
+
);
|
|
5028
|
+
};
|
|
5029
|
+
const onAttemptComplete = () => {
|
|
5030
|
+
if (Object.keys(app.workflows).length === 0) {
|
|
5031
|
+
logger2.debug("All attempts completed!");
|
|
5032
|
+
app.events.off(INTERNAL_ATTEMPT_COMPLETE, onAttemptComplete);
|
|
5033
|
+
resolve5();
|
|
5034
|
+
} else {
|
|
5035
|
+
log();
|
|
5036
|
+
}
|
|
5037
|
+
};
|
|
5038
|
+
if (Object.keys(app.workflows).length) {
|
|
5039
|
+
log();
|
|
5040
|
+
app.events.on(INTERNAL_ATTEMPT_COMPLETE, onAttemptComplete);
|
|
5041
|
+
} else {
|
|
5042
|
+
resolve5();
|
|
5043
|
+
}
|
|
5044
|
+
});
|
|
5045
|
+
var destroy_default = destroy;
|
|
5046
|
+
|
|
4987
5047
|
// src/util/try-with-backoff.ts
|
|
4988
5048
|
var BACKOFF_MULTIPLIER = 1.15;
|
|
4989
5049
|
var tryWithBackoff = (fn, opts = {}) => {
|
|
@@ -5027,19 +5087,6 @@ var try_with_backoff_default = tryWithBackoff;
|
|
|
5027
5087
|
|
|
5028
5088
|
// src/api/claim.ts
|
|
5029
5089
|
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
5090
|
var mockLogger = createMockLogger();
|
|
5044
5091
|
var claim = (app, logger2 = mockLogger, maxWorkers = 5) => {
|
|
5045
5092
|
return new Promise((resolve5, reject) => {
|
|
@@ -5047,8 +5094,11 @@ var claim = (app, logger2 = mockLogger, maxWorkers = 5) => {
|
|
|
5047
5094
|
if (activeWorkers >= maxWorkers) {
|
|
5048
5095
|
return reject(new Error("Server at capacity"));
|
|
5049
5096
|
}
|
|
5097
|
+
if (!app.queueChannel) {
|
|
5098
|
+
return reject(new Error("No websocket available"));
|
|
5099
|
+
}
|
|
5050
5100
|
logger2.debug("requesting attempt...");
|
|
5051
|
-
app.
|
|
5101
|
+
app.queueChannel.push(CLAIM, { demand: 1 }).receive("ok", ({ attempts }) => {
|
|
5052
5102
|
logger2.debug(`pulled ${attempts.length} attempts`);
|
|
5053
5103
|
if (!attempts?.length) {
|
|
5054
5104
|
return reject(new Error("No attempts returned"));
|
|
@@ -5090,6 +5140,7 @@ var startWorkloop = (app, logger2, minBackoff2, maxBackoff2, maxWorkers) => {
|
|
|
5090
5140
|
logger2.debug("cancelling workloop");
|
|
5091
5141
|
cancelled = true;
|
|
5092
5142
|
promise.cancel();
|
|
5143
|
+
app.queueChannel?.leave();
|
|
5093
5144
|
};
|
|
5094
5145
|
};
|
|
5095
5146
|
var workloop_default = startWorkloop;
|
|
@@ -5100,13 +5151,15 @@ import crypto2 from "node:crypto";
|
|
|
5100
5151
|
// src/util/convert-attempt.ts
|
|
5101
5152
|
import crypto from "node:crypto";
|
|
5102
5153
|
var conditions = {
|
|
5103
|
-
on_job_success:
|
|
5104
|
-
on_job_failure:
|
|
5105
|
-
always: null
|
|
5154
|
+
on_job_success: (upstreamId) => `Boolean(!state.errors?.["${upstreamId}"] ?? true)`,
|
|
5155
|
+
on_job_failure: (upstreamId) => `Boolean(state.errors && state.errors["${upstreamId}"])`,
|
|
5156
|
+
always: (_upstreamId) => null
|
|
5106
5157
|
};
|
|
5107
|
-
var mapEdgeCondition = (
|
|
5158
|
+
var mapEdgeCondition = (edge) => {
|
|
5159
|
+
const { condition } = edge;
|
|
5108
5160
|
if (condition && condition in conditions) {
|
|
5109
|
-
|
|
5161
|
+
const upstream = edge.source_job_id || edge.source_trigger_id;
|
|
5162
|
+
return conditions[condition](upstream);
|
|
5110
5163
|
}
|
|
5111
5164
|
return condition;
|
|
5112
5165
|
};
|
|
@@ -5155,7 +5208,7 @@ var convert_attempt_default = (attempt) => {
|
|
|
5155
5208
|
}
|
|
5156
5209
|
const next = edges.filter((e) => e.source_job_id === id).reduce((obj, edge) => {
|
|
5157
5210
|
const newEdge = {};
|
|
5158
|
-
const condition = mapEdgeCondition(edge
|
|
5211
|
+
const condition = mapEdgeCondition(edge);
|
|
5159
5212
|
if (condition) {
|
|
5160
5213
|
newEdge.condition = condition;
|
|
5161
5214
|
}
|
|
@@ -5424,6 +5477,11 @@ async function loadCredential(channel, credentialId) {
|
|
|
5424
5477
|
return get_with_reply_default(channel, GET_CREDENTIAL, { id: credentialId });
|
|
5425
5478
|
}
|
|
5426
5479
|
|
|
5480
|
+
// src/middleware/healthcheck.ts
|
|
5481
|
+
var healthcheck_default = (ctx) => {
|
|
5482
|
+
ctx.status = 200;
|
|
5483
|
+
};
|
|
5484
|
+
|
|
5427
5485
|
// src/channels/attempt.ts
|
|
5428
5486
|
var joinAttemptChannel = (socket, token, attemptId, logger2) => {
|
|
5429
5487
|
return new Promise((resolve5, reject) => {
|
|
@@ -5459,14 +5517,14 @@ import { WebSocket } from "ws";
|
|
|
5459
5517
|
// src/util/worker-token.ts
|
|
5460
5518
|
import * as jose from "jose";
|
|
5461
5519
|
var alg = "HS256";
|
|
5462
|
-
var generateWorkerToken = async (secret, workerId) => {
|
|
5520
|
+
var generateWorkerToken = async (secret, workerId, logger2) => {
|
|
5463
5521
|
if (!secret) {
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5522
|
+
logger2.warn();
|
|
5523
|
+
logger2.warn("WARNING: Worker Secret not provided!");
|
|
5524
|
+
logger2.warn(
|
|
5467
5525
|
"This worker will attempt to connect to Lightning with default secret"
|
|
5468
5526
|
);
|
|
5469
|
-
|
|
5527
|
+
logger2.warn();
|
|
5470
5528
|
}
|
|
5471
5529
|
const encodedSecret = new TextEncoder().encode(secret || "<secret>");
|
|
5472
5530
|
const claims = {
|
|
@@ -5480,7 +5538,7 @@ var worker_token_default = generateWorkerToken;
|
|
|
5480
5538
|
// src/channels/worker-queue.ts
|
|
5481
5539
|
var connectToWorkerQueue = (endpoint, serverId, secret, logger2, SocketConstructor = PhxSocket) => {
|
|
5482
5540
|
const events = new EventEmitter2();
|
|
5483
|
-
worker_token_default(secret, serverId).then((token) => {
|
|
5541
|
+
worker_token_default(secret, serverId, logger2).then((token) => {
|
|
5484
5542
|
const socket = new SocketConstructor(endpoint, {
|
|
5485
5543
|
params: { token },
|
|
5486
5544
|
transport: WebSocket
|
|
@@ -5523,7 +5581,7 @@ function connect(app, logger2, options = {}) {
|
|
|
5523
5581
|
const onConnect = ({ socket, channel }) => {
|
|
5524
5582
|
logger2.success("Connected to Lightning at", options.lightning);
|
|
5525
5583
|
app.socket = socket;
|
|
5526
|
-
app.
|
|
5584
|
+
app.queueChannel = channel;
|
|
5527
5585
|
if (!options.noLoop) {
|
|
5528
5586
|
logger2.info("Starting workloop");
|
|
5529
5587
|
app.killWorkloop = workloop_default(
|
|
@@ -5570,6 +5628,8 @@ function createServer(engine, options = {}) {
|
|
|
5570
5628
|
const app = new Koa();
|
|
5571
5629
|
app.id = humanId({ separator: "-", capitalize: false });
|
|
5572
5630
|
const router = new Router();
|
|
5631
|
+
app.events = new EventEmitter3();
|
|
5632
|
+
app.engine = engine;
|
|
5573
5633
|
app.use(bodyParser());
|
|
5574
5634
|
app.use(
|
|
5575
5635
|
koaLogger((str, _args) => {
|
|
@@ -5577,8 +5637,12 @@ function createServer(engine, options = {}) {
|
|
|
5577
5637
|
})
|
|
5578
5638
|
);
|
|
5579
5639
|
app.workflows = {};
|
|
5580
|
-
|
|
5640
|
+
app.destroyed = false;
|
|
5641
|
+
app.server = app.listen(port);
|
|
5581
5642
|
logger2.success(`ws-worker ${app.id} listening on ${port}`);
|
|
5643
|
+
process.send?.("READY");
|
|
5644
|
+
router.get("/livez", healthcheck_default);
|
|
5645
|
+
router.get("/", healthcheck_default);
|
|
5582
5646
|
app.execute = async ({ id, token }) => {
|
|
5583
5647
|
if (app.socket) {
|
|
5584
5648
|
app.workflows[id] = true;
|
|
@@ -5590,6 +5654,7 @@ function createServer(engine, options = {}) {
|
|
|
5590
5654
|
const onFinish = () => {
|
|
5591
5655
|
delete app.workflows[id];
|
|
5592
5656
|
attemptChannel.leave();
|
|
5657
|
+
app.events.emit(INTERNAL_ATTEMPT_COMPLETE);
|
|
5593
5658
|
};
|
|
5594
5659
|
const context = execute(
|
|
5595
5660
|
attemptChannel,
|
|
@@ -5616,19 +5681,24 @@ function createServer(engine, options = {}) {
|
|
|
5616
5681
|
ctx.status = 204;
|
|
5617
5682
|
});
|
|
5618
5683
|
});
|
|
5619
|
-
app.destroy =
|
|
5620
|
-
logger2.info("Closing server...");
|
|
5621
|
-
server.close();
|
|
5622
|
-
await engine.destroy();
|
|
5623
|
-
app.killWorkloop?.();
|
|
5624
|
-
logger2.success("Server closed");
|
|
5625
|
-
};
|
|
5684
|
+
app.destroy = () => destroy_default(app, logger2);
|
|
5626
5685
|
app.use(router.routes());
|
|
5627
5686
|
if (options.lightning) {
|
|
5628
5687
|
connect(app, logger2, options);
|
|
5629
5688
|
} else {
|
|
5630
5689
|
logger2.warn("No lightning URL provided");
|
|
5631
5690
|
}
|
|
5691
|
+
let shutdown = false;
|
|
5692
|
+
const exit = async (signal) => {
|
|
5693
|
+
if (!shutdown) {
|
|
5694
|
+
shutdown = true;
|
|
5695
|
+
logger2.always(`${signal} RECEIVED: CLOSING SERVER`);
|
|
5696
|
+
await app.destroy();
|
|
5697
|
+
process.exit();
|
|
5698
|
+
}
|
|
5699
|
+
};
|
|
5700
|
+
process.on("SIGINT", () => exit("SIGINT"));
|
|
5701
|
+
process.on("SIGTERM", () => exit("SIGTERM"));
|
|
5632
5702
|
app.on = (...args2) => {
|
|
5633
5703
|
return engine.on(...args2);
|
|
5634
5704
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openfn/ws-worker",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "A Websocket Worker to connect Lightning to a Runtime Engine",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -21,7 +21,7 @@
|
|
|
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.1",
|
|
25
25
|
"@openfn/logger": "0.0.19",
|
|
26
26
|
"@openfn/runtime": "0.2.0"
|
|
27
27
|
},
|
|
@@ -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.3"
|
|
44
44
|
},
|
|
45
45
|
"files": [
|
|
46
46
|
"dist",
|