@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 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: any;
173
- channel: Channel;
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.channel.push(CLAIM, { demand: 1 }).receive("ok", ({ attempts }) => {
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: "!state.errors",
126
- on_job_failure: "state.errors",
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 = (condition) => {
178
+ var mapEdgeCondition = (edge) => {
179
+ const { condition } = edge;
130
180
  if (condition && condition in conditions) {
131
- return conditions[condition];
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.condition);
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
- console.warn();
487
- console.warn("WARNING: Worker Secret not provided!");
488
- console.warn(
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
- console.warn();
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.channel = channel;
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
- const server = app.listen(port);
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 = async () => {
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.channel.push(CLAIM, { demand: 1 }).receive("ok", ({ attempts }) => {
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 crypto2 from "node:crypto";
5154
+ import crypto3 from "node:crypto";
5099
5155
 
5100
5156
  // src/util/convert-attempt.ts
5101
- import crypto from "node:crypto";
5157
+ import crypto2 from "node:crypto";
5102
5158
  var conditions = {
5103
- on_job_success: "!state.errors",
5104
- on_job_failure: "state.errors",
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 = (condition) => {
5163
+ var mapEdgeCondition = (edge) => {
5164
+ const { condition } = edge;
5108
5165
  if (condition && condition in conditions) {
5109
- return conditions[condition];
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 || crypto.randomUUID();
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.condition);
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 = crypto2.randomUUID();
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 = crypto2.randomUUID();
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
- console.warn();
5465
- console.warn("WARNING: Worker Secret not provided!");
5466
- console.warn(
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
- console.warn();
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.channel = channel;
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
- const server = app.listen(port);
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 = async () => {
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((engine) => {
5712
- logger.debug("engine created");
5713
- engineReady(engine);
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.9",
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.0",
24
+ "@openfn/engine-multi": "0.2.2",
25
25
  "@openfn/logger": "0.0.19",
26
- "@openfn/runtime": "0.2.0"
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.2"
43
+ "@openfn/lightning-mock": "1.1.4"
44
44
  },
45
45
  "files": [
46
46
  "dist",