@openfn/ws-worker 1.6.4 → 1.6.7

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,26 @@
1
1
  # ws-worker
2
2
 
3
+ ## 1.6.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 42883f8: Better handliung of claim backoffs when at capacity
8
+
9
+ ## 1.6.6
10
+
11
+ ### Patch Changes
12
+
13
+ - Log claim event duration
14
+
15
+ ## 1.6.5
16
+
17
+ ### Patch Changes
18
+
19
+ - 5db5862: Dont log compiled job code
20
+ - f581c6b: log duration of runs and server capacity
21
+ - 3e6eba2: Trap errors coming out of the websocket
22
+ - @openfn/engine-multi@1.2.5
23
+
3
24
  ## 1.6.4
4
25
 
5
26
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -8,6 +8,11 @@ import { ExecutionPlan, Lazy, State } from '@openfn/lexicon';
8
8
  import { Channel as Channel$1 } from 'phoenix';
9
9
  import { Server } from 'http';
10
10
 
11
+ declare type Workloop = {
12
+ stop: (reason?: string) => void;
13
+ isStopped: () => boolean;
14
+ };
15
+
11
16
  // Internal server state for each run
12
17
  type RunState = {
13
18
  activeStep?: string;
@@ -80,9 +85,10 @@ interface ServerApp extends Koa {
80
85
  server: Server;
81
86
  engine: RuntimeEngine;
82
87
  options: ServerOptions;
88
+ workloop?: Workloop;
83
89
  execute: ({ id, token }: ClaimRun) => Promise<void>;
84
90
  destroy: () => void;
85
- killWorkloop?: () => void;
91
+ resumeWorkloop: () => void;
86
92
  }
87
93
  declare function createServer(engine: RuntimeEngine, options?: ServerOptions): ServerApp;
88
94
 
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ var name, version, description, main, type, scripts, bin, author, license, depen
29
29
  var init_package = __esm({
30
30
  "package.json"() {
31
31
  name = "@openfn/ws-worker";
32
- version = "1.6.4";
32
+ version = "1.6.7";
33
33
  description = "A Websocket Worker to connect Lightning to a Runtime Engine";
34
34
  main = "dist/index.js";
35
35
  type = "module";
@@ -132,7 +132,7 @@ var destroy = async (app, logger) => {
132
132
  await Promise.all([
133
133
  new Promise((resolve) => {
134
134
  app.destroyed = true;
135
- app.killWorkloop?.();
135
+ app.workloop?.stop("server closed");
136
136
  app.queueChannel?.leave();
137
137
  app.server.close(async () => {
138
138
  resolve();
@@ -232,18 +232,19 @@ var claim = (app, logger = mockLogger, options = {}) => {
232
232
  const { maxWorkers = 5 } = options;
233
233
  const activeWorkers = Object.keys(app.workflows).length;
234
234
  if (activeWorkers >= maxWorkers) {
235
- logger.debug("skipping claim attempt: server at capacity");
235
+ app.workloop?.stop(`server at capacity (${activeWorkers}/${maxWorkers})`);
236
236
  return reject(new Error("Server at capacity"));
237
237
  }
238
238
  if (!app.queueChannel) {
239
239
  logger.debug("skipping claim attempt: websocket unavailable");
240
240
  return reject(new Error("No websocket available"));
241
241
  }
242
- logger.debug("requesting run...");
242
+ logger.debug(`requesting run (capacity ${activeWorkers}/${maxWorkers})`);
243
+ const start = Date.now();
243
244
  app.queueChannel.push(CLAIM, { demand: 1 }).receive("ok", ({ runs }) => {
245
+ const duration = Date.now() - start;
244
246
  logger.debug(
245
- `claimed ${runs.length} runs: `,
246
- runs.map((r) => r.id).join(",")
247
+ `claimed ${runs.length} runs in ${duration}ms (${runs.length ? runs.map((r) => r.id).join(",") : "-"})`
247
248
  );
248
249
  if (!runs?.length) {
249
250
  return reject(new Error("No runs returned"));
@@ -300,11 +301,15 @@ var startWorkloop = (app, logger, minBackoff, maxBackoff, maxWorkers) => {
300
301
  }
301
302
  };
302
303
  workLoop();
303
- return () => {
304
- logger.debug("cancelling workloop");
305
- cancelled = true;
306
- promise.cancel();
307
- app.queueChannel?.leave();
304
+ return {
305
+ stop: (reason = "reason unknown") => {
306
+ if (!cancelled) {
307
+ logger.info(`cancelling workloop: ${reason}`);
308
+ cancelled = true;
309
+ promise.cancel();
310
+ }
311
+ },
312
+ isStopped: () => cancelled
308
313
  };
309
314
  };
310
315
  var workloop_default = startWorkloop;
@@ -1015,16 +1020,7 @@ function connect(app, logger, options = {}) {
1015
1020
  logger.success("Connected to Lightning at", options.lightning);
1016
1021
  app.socket = socket;
1017
1022
  app.queueChannel = channel;
1018
- if (!options.noLoop) {
1019
- logger.info("Starting workloop");
1020
- app.killWorkloop = workloop_default(
1021
- app,
1022
- logger,
1023
- options.backoff?.min || MIN_BACKOFF,
1024
- options.backoff?.max || MAX_BACKOFF,
1025
- options.maxWorkflows
1026
- );
1027
- } else {
1023
+ if (options.noLoop) {
1028
1024
  const port = app.server?.address().port;
1029
1025
  logger.break();
1030
1026
  logger.warn("Noloop active: workloop has not started");
@@ -1033,17 +1029,17 @@ function connect(app, logger, options = {}) {
1033
1029
  logger.info(` curl -X POST http://localhost:${port}/claim`);
1034
1030
  logger.break();
1035
1031
  }
1032
+ app.resumeWorkloop();
1036
1033
  };
1037
1034
  const onDisconnect = () => {
1038
- if (app.killWorkloop) {
1039
- app.killWorkloop();
1040
- delete app.killWorkloop;
1041
- if (!app.destroyed) {
1042
- logger.info("Connection to lightning lost");
1043
- logger.info(
1044
- "Worker will automatically reconnect when lightning is back online"
1045
- );
1046
- }
1035
+ if (!app.workloop?.isStopped()) {
1036
+ app.workloop?.stop("Socket disconnected unexpectedly");
1037
+ }
1038
+ if (!app.destroyed) {
1039
+ logger.info("Connection to lightning lost");
1040
+ logger.info(
1041
+ "Worker will automatically reconnect when lightning is back online"
1042
+ );
1047
1043
  }
1048
1044
  };
1049
1045
  const onError = (e) => {
@@ -1086,34 +1082,59 @@ function createServer(engine, options = {}) {
1086
1082
  router.get("/livez", healthcheck_default);
1087
1083
  router.get("/", healthcheck_default);
1088
1084
  app.options = options;
1085
+ app.resumeWorkloop = () => {
1086
+ if (options.noLoop) {
1087
+ return;
1088
+ }
1089
+ if (!app.workloop || app.workloop?.isStopped()) {
1090
+ logger.info("Starting workloop");
1091
+ app.workloop = workloop_default(
1092
+ app,
1093
+ logger,
1094
+ options.backoff?.min || MIN_BACKOFF,
1095
+ options.backoff?.max || MAX_BACKOFF,
1096
+ options.maxWorkflows
1097
+ );
1098
+ }
1099
+ };
1089
1100
  app.execute = async ({ id, token }) => {
1090
1101
  if (app.socket) {
1091
- app.workflows[id] = true;
1092
- const {
1093
- channel: runChannel,
1094
- plan,
1095
- options: options2 = {},
1096
- input
1097
- } = await run_default(app.socket, token, id, logger);
1098
- if (!("payloadLimitMb" in options2)) {
1099
- options2.payloadLimitMb = app.options.payloadLimitMb;
1102
+ try {
1103
+ const start = Date.now();
1104
+ app.workflows[id] = true;
1105
+ const {
1106
+ channel: runChannel,
1107
+ plan,
1108
+ options: options2 = {},
1109
+ input
1110
+ } = await run_default(app.socket, token, id, logger);
1111
+ if (!("payloadLimitMb" in options2)) {
1112
+ options2.payloadLimitMb = app.options.payloadLimitMb;
1113
+ }
1114
+ const onFinish = () => {
1115
+ const duration = (Date.now() - start) / 1e3;
1116
+ logger.debug(
1117
+ `workflow ${id} complete in ${duration}s: releasing worker`
1118
+ );
1119
+ delete app.workflows[id];
1120
+ runChannel.leave();
1121
+ app.events.emit(INTERNAL_RUN_COMPLETE);
1122
+ app.resumeWorkloop();
1123
+ };
1124
+ const context = execute(
1125
+ runChannel,
1126
+ engine,
1127
+ logger,
1128
+ plan,
1129
+ input,
1130
+ options2,
1131
+ onFinish
1132
+ );
1133
+ app.workflows[id] = context;
1134
+ } catch (e) {
1135
+ logger.error(`Unexpected error executing ${id}`);
1136
+ logger.error(e);
1100
1137
  }
1101
- const onFinish = () => {
1102
- logger.debug(`workflow ${id} complete: releasing worker`);
1103
- delete app.workflows[id];
1104
- runChannel.leave();
1105
- app.events.emit(INTERNAL_RUN_COMPLETE);
1106
- };
1107
- const context = execute(
1108
- runChannel,
1109
- engine,
1110
- logger,
1111
- plan,
1112
- input,
1113
- options2,
1114
- onFinish
1115
- );
1116
- app.workflows[id] = context;
1117
1138
  } else {
1118
1139
  logger.error("No lightning socket established");
1119
1140
  }
package/dist/start.js CHANGED
@@ -37,7 +37,7 @@ var name, version, description, main, type, scripts, bin, author, license, depen
37
37
  var init_package = __esm({
38
38
  "package.json"() {
39
39
  name = "@openfn/ws-worker";
40
- version = "1.6.4";
40
+ version = "1.6.7";
41
41
  description = "A Websocket Worker to connect Lightning to a Runtime Engine";
42
42
  main = "dist/index.js";
43
43
  type = "module";
@@ -271,7 +271,7 @@ var destroy = async (app, logger2) => {
271
271
  await Promise.all([
272
272
  new Promise((resolve5) => {
273
273
  app.destroyed = true;
274
- app.killWorkloop?.();
274
+ app.workloop?.stop("server closed");
275
275
  app.queueChannel?.leave();
276
276
  app.server.close(async () => {
277
277
  resolve5();
@@ -371,18 +371,19 @@ var claim = (app, logger2 = mockLogger, options = {}) => {
371
371
  const { maxWorkers = 5 } = options;
372
372
  const activeWorkers = Object.keys(app.workflows).length;
373
373
  if (activeWorkers >= maxWorkers) {
374
- logger2.debug("skipping claim attempt: server at capacity");
374
+ app.workloop?.stop(`server at capacity (${activeWorkers}/${maxWorkers})`);
375
375
  return reject(new Error("Server at capacity"));
376
376
  }
377
377
  if (!app.queueChannel) {
378
378
  logger2.debug("skipping claim attempt: websocket unavailable");
379
379
  return reject(new Error("No websocket available"));
380
380
  }
381
- logger2.debug("requesting run...");
381
+ logger2.debug(`requesting run (capacity ${activeWorkers}/${maxWorkers})`);
382
+ const start = Date.now();
382
383
  app.queueChannel.push(CLAIM, { demand: 1 }).receive("ok", ({ runs }) => {
384
+ const duration = Date.now() - start;
383
385
  logger2.debug(
384
- `claimed ${runs.length} runs: `,
385
- runs.map((r) => r.id).join(",")
386
+ `claimed ${runs.length} runs in ${duration}ms (${runs.length ? runs.map((r) => r.id).join(",") : "-"})`
386
387
  );
387
388
  if (!runs?.length) {
388
389
  return reject(new Error("No runs returned"));
@@ -439,11 +440,15 @@ var startWorkloop = (app, logger2, minBackoff2, maxBackoff2, maxWorkers) => {
439
440
  }
440
441
  };
441
442
  workLoop();
442
- return () => {
443
- logger2.debug("cancelling workloop");
444
- cancelled = true;
445
- promise.cancel();
446
- app.queueChannel?.leave();
443
+ return {
444
+ stop: (reason = "reason unknown") => {
445
+ if (!cancelled) {
446
+ logger2.info(`cancelling workloop: ${reason}`);
447
+ cancelled = true;
448
+ promise.cancel();
449
+ }
450
+ },
451
+ isStopped: () => cancelled
447
452
  };
448
453
  };
449
454
  var workloop_default = startWorkloop;
@@ -1154,16 +1159,7 @@ function connect(app, logger2, options = {}) {
1154
1159
  logger2.success("Connected to Lightning at", options.lightning);
1155
1160
  app.socket = socket;
1156
1161
  app.queueChannel = channel;
1157
- if (!options.noLoop) {
1158
- logger2.info("Starting workloop");
1159
- app.killWorkloop = workloop_default(
1160
- app,
1161
- logger2,
1162
- options.backoff?.min || MIN_BACKOFF,
1163
- options.backoff?.max || MAX_BACKOFF,
1164
- options.maxWorkflows
1165
- );
1166
- } else {
1162
+ if (options.noLoop) {
1167
1163
  const port = app.server?.address().port;
1168
1164
  logger2.break();
1169
1165
  logger2.warn("Noloop active: workloop has not started");
@@ -1172,17 +1168,17 @@ function connect(app, logger2, options = {}) {
1172
1168
  logger2.info(` curl -X POST http://localhost:${port}/claim`);
1173
1169
  logger2.break();
1174
1170
  }
1171
+ app.resumeWorkloop();
1175
1172
  };
1176
1173
  const onDisconnect = () => {
1177
- if (app.killWorkloop) {
1178
- app.killWorkloop();
1179
- delete app.killWorkloop;
1180
- if (!app.destroyed) {
1181
- logger2.info("Connection to lightning lost");
1182
- logger2.info(
1183
- "Worker will automatically reconnect when lightning is back online"
1184
- );
1185
- }
1174
+ if (!app.workloop?.isStopped()) {
1175
+ app.workloop?.stop("Socket disconnected unexpectedly");
1176
+ }
1177
+ if (!app.destroyed) {
1178
+ logger2.info("Connection to lightning lost");
1179
+ logger2.info(
1180
+ "Worker will automatically reconnect when lightning is back online"
1181
+ );
1186
1182
  }
1187
1183
  };
1188
1184
  const onError = (e) => {
@@ -1225,34 +1221,59 @@ function createServer(engine, options = {}) {
1225
1221
  router.get("/livez", healthcheck_default);
1226
1222
  router.get("/", healthcheck_default);
1227
1223
  app.options = options;
1228
- app.execute = async ({ id, token }) => {
1229
- if (app.socket) {
1230
- app.workflows[id] = true;
1231
- const {
1232
- channel: runChannel,
1233
- plan,
1234
- options: options2 = {},
1235
- input
1236
- } = await run_default(app.socket, token, id, logger2);
1237
- if (!("payloadLimitMb" in options2)) {
1238
- options2.payloadLimitMb = app.options.payloadLimitMb;
1239
- }
1240
- const onFinish = () => {
1241
- logger2.debug(`workflow ${id} complete: releasing worker`);
1242
- delete app.workflows[id];
1243
- runChannel.leave();
1244
- app.events.emit(INTERNAL_RUN_COMPLETE);
1245
- };
1246
- const context = execute(
1247
- runChannel,
1248
- engine,
1224
+ app.resumeWorkloop = () => {
1225
+ if (options.noLoop) {
1226
+ return;
1227
+ }
1228
+ if (!app.workloop || app.workloop?.isStopped()) {
1229
+ logger2.info("Starting workloop");
1230
+ app.workloop = workloop_default(
1231
+ app,
1249
1232
  logger2,
1250
- plan,
1251
- input,
1252
- options2,
1253
- onFinish
1233
+ options.backoff?.min || MIN_BACKOFF,
1234
+ options.backoff?.max || MAX_BACKOFF,
1235
+ options.maxWorkflows
1254
1236
  );
1255
- app.workflows[id] = context;
1237
+ }
1238
+ };
1239
+ app.execute = async ({ id, token }) => {
1240
+ if (app.socket) {
1241
+ try {
1242
+ const start = Date.now();
1243
+ app.workflows[id] = true;
1244
+ const {
1245
+ channel: runChannel,
1246
+ plan,
1247
+ options: options2 = {},
1248
+ input
1249
+ } = await run_default(app.socket, token, id, logger2);
1250
+ if (!("payloadLimitMb" in options2)) {
1251
+ options2.payloadLimitMb = app.options.payloadLimitMb;
1252
+ }
1253
+ const onFinish = () => {
1254
+ const duration = (Date.now() - start) / 1e3;
1255
+ logger2.debug(
1256
+ `workflow ${id} complete in ${duration}s: releasing worker`
1257
+ );
1258
+ delete app.workflows[id];
1259
+ runChannel.leave();
1260
+ app.events.emit(INTERNAL_RUN_COMPLETE);
1261
+ app.resumeWorkloop();
1262
+ };
1263
+ const context = execute(
1264
+ runChannel,
1265
+ engine,
1266
+ logger2,
1267
+ plan,
1268
+ input,
1269
+ options2,
1270
+ onFinish
1271
+ );
1272
+ app.workflows[id] = context;
1273
+ } catch (e) {
1274
+ logger2.error(`Unexpected error executing ${id}`);
1275
+ logger2.error(e);
1276
+ }
1256
1277
  } else {
1257
1278
  logger2.error("No lightning socket established");
1258
1279
  }
@@ -6264,6 +6285,7 @@ function parseArgs(argv) {
6264
6285
  // src/start.ts
6265
6286
  var args = parseArgs(process.argv);
6266
6287
  var logger = createLogger("SRV", { level: args.log });
6288
+ logger.info("Starting worker server...");
6267
6289
  if (args.lightning === "mock") {
6268
6290
  args.lightning = "ws://localhost:8888/worker";
6269
6291
  if (!args.secret) {
@@ -6275,7 +6297,7 @@ if (args.lightning === "mock") {
6275
6297
  }
6276
6298
  var [minBackoff, maxBackoff] = args.backoff.split("/").map((n) => parseInt(n, 10) * 1e3);
6277
6299
  function engineReady(engine) {
6278
- logger.debug("Creating worker server...");
6300
+ logger.debug("Creating worker instance");
6279
6301
  const workerOptions = {
6280
6302
  port: args.port,
6281
6303
  lightning: args.lightning,
@@ -6306,6 +6328,7 @@ function engineReady(engine) {
6306
6328
  } = workerOptions;
6307
6329
  logger.debug("Worker options:", humanOptions);
6308
6330
  server_default(engine, workerOptions);
6331
+ logger.success("Worker started OK");
6309
6332
  }
6310
6333
  if (args.mock) {
6311
6334
  runtime_engine_default().then((engine) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfn/ws-worker",
3
- "version": "1.6.4",
3
+ "version": "1.6.7",
4
4
  "description": "A Websocket Worker to connect Lightning to a Runtime Engine",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -22,10 +22,10 @@
22
22
  "koa-logger": "^3.2.1",
23
23
  "phoenix": "1.7.10",
24
24
  "ws": "^8.18.0",
25
- "@openfn/engine-multi": "1.2.4",
25
+ "@openfn/engine-multi": "1.2.5",
26
26
  "@openfn/lexicon": "^1.1.0",
27
- "@openfn/runtime": "1.4.2",
28
- "@openfn/logger": "1.0.2"
27
+ "@openfn/logger": "1.0.2",
28
+ "@openfn/runtime": "1.4.2"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/koa": "^2.13.5",
@@ -42,7 +42,7 @@
42
42
  "tsup": "^6.2.3",
43
43
  "typescript": "^4.6.4",
44
44
  "yargs": "^17.6.2",
45
- "@openfn/lightning-mock": "2.0.18"
45
+ "@openfn/lightning-mock": "2.0.19"
46
46
  },
47
47
  "files": [
48
48
  "dist",