@openfn/ws-worker 1.21.5 → 1.22.0
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 +10 -0
- package/README.md +79 -0
- package/dist/index.d.ts +25 -8
- package/dist/index.js +276 -124
- package/dist/start.js +310 -129
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -33,7 +33,9 @@ var destroy = async (app, logger) => {
|
|
|
33
33
|
await Promise.all([
|
|
34
34
|
new Promise((resolve) => {
|
|
35
35
|
app.destroyed = true;
|
|
36
|
-
app.
|
|
36
|
+
for (const w of app.workloops) {
|
|
37
|
+
w.stop("server closed");
|
|
38
|
+
}
|
|
37
39
|
app.server.close(async () => {
|
|
38
40
|
resolve();
|
|
39
41
|
});
|
|
@@ -51,11 +53,11 @@ var destroy = async (app, logger) => {
|
|
|
51
53
|
var waitForRunsAndClaims = (app, logger) => new Promise((resolve) => {
|
|
52
54
|
const log = () => {
|
|
53
55
|
logger.debug(
|
|
54
|
-
`Waiting for ${Object.keys(app.workflows).length} runs and ${
|
|
56
|
+
`Waiting for ${Object.keys(app.workflows).length} runs and ${app.pendingClaims()} claims to complete...`
|
|
55
57
|
);
|
|
56
58
|
};
|
|
57
59
|
const checkAllClear = () => {
|
|
58
|
-
if (Object.keys(app.workflows).length +
|
|
60
|
+
if (Object.keys(app.workflows).length + app.pendingClaims() === 0) {
|
|
59
61
|
logger.debug("All runs completed!");
|
|
60
62
|
app.events.off(INTERNAL_RUN_COMPLETE, checkAllClear);
|
|
61
63
|
app.events.off(INTERNAL_CLAIM_COMPLETE, checkAllClear);
|
|
@@ -64,7 +66,7 @@ var waitForRunsAndClaims = (app, logger) => new Promise((resolve) => {
|
|
|
64
66
|
log();
|
|
65
67
|
}
|
|
66
68
|
};
|
|
67
|
-
if (Object.keys(app.workflows).length ||
|
|
69
|
+
if (Object.keys(app.workflows).length || app.pendingClaims()) {
|
|
68
70
|
log();
|
|
69
71
|
app.events.on(INTERNAL_RUN_COMPLETE, checkAllClear);
|
|
70
72
|
app.events.on(INTERNAL_CLAIM_COMPLETE, checkAllClear);
|
|
@@ -75,51 +77,6 @@ var waitForRunsAndClaims = (app, logger) => new Promise((resolve) => {
|
|
|
75
77
|
});
|
|
76
78
|
var destroy_default = destroy;
|
|
77
79
|
|
|
78
|
-
// src/util/try-with-backoff.ts
|
|
79
|
-
var BACKOFF_MULTIPLIER = 1.15;
|
|
80
|
-
var tryWithBackoff = (fn, opts = {}) => {
|
|
81
|
-
const { min = 1e3, max = 1e4, maxRuns, runs = 1 } = opts;
|
|
82
|
-
let cancelled = false;
|
|
83
|
-
if (!opts.isCancelled) {
|
|
84
|
-
opts.isCancelled = () => cancelled;
|
|
85
|
-
}
|
|
86
|
-
const promise = new Promise(async (resolve, reject) => {
|
|
87
|
-
try {
|
|
88
|
-
await fn();
|
|
89
|
-
resolve();
|
|
90
|
-
} catch (e) {
|
|
91
|
-
if (e?.abort) {
|
|
92
|
-
cancelled = true;
|
|
93
|
-
return reject();
|
|
94
|
-
}
|
|
95
|
-
if (opts.isCancelled()) {
|
|
96
|
-
return resolve();
|
|
97
|
-
}
|
|
98
|
-
if (!isNaN(maxRuns) && runs >= maxRuns) {
|
|
99
|
-
return reject(new Error("max runs exceeded"));
|
|
100
|
-
}
|
|
101
|
-
setTimeout(() => {
|
|
102
|
-
if (opts.isCancelled()) {
|
|
103
|
-
return resolve();
|
|
104
|
-
}
|
|
105
|
-
const nextOpts = {
|
|
106
|
-
maxRuns,
|
|
107
|
-
runs: runs + 1,
|
|
108
|
-
min: Math.min(max, min * BACKOFF_MULTIPLIER),
|
|
109
|
-
max,
|
|
110
|
-
isCancelled: opts.isCancelled
|
|
111
|
-
};
|
|
112
|
-
tryWithBackoff(fn, nextOpts).then(resolve).catch(reject);
|
|
113
|
-
}, min);
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
promise.cancel = () => {
|
|
117
|
-
cancelled = true;
|
|
118
|
-
};
|
|
119
|
-
return promise;
|
|
120
|
-
};
|
|
121
|
-
var try_with_backoff_default = tryWithBackoff;
|
|
122
|
-
|
|
123
80
|
// src/api/claim.ts
|
|
124
81
|
import v8 from "node:v8";
|
|
125
82
|
import * as Sentry from "@sentry/node";
|
|
@@ -148,24 +105,26 @@ var ClaimError = class extends Error {
|
|
|
148
105
|
}
|
|
149
106
|
};
|
|
150
107
|
var claimIdGen = 0;
|
|
151
|
-
var claim = (app, logger = mockLogger, options = {}) => {
|
|
108
|
+
var claim = (app, workloop, logger = mockLogger, options = {}) => {
|
|
152
109
|
return new Promise((resolve, reject) => {
|
|
153
|
-
|
|
154
|
-
const { maxWorkers = 5, demand = 1 } = options;
|
|
110
|
+
const { demand = 1 } = options;
|
|
155
111
|
const podName = NAME ? `[${NAME}] ` : "";
|
|
156
|
-
const
|
|
157
|
-
const
|
|
112
|
+
const activeInWorkloop = workloop.activeRuns.size;
|
|
113
|
+
const capacity = workloop.capacity;
|
|
114
|
+
const pendingWorkloopClaims = Object.values(workloop.openClaims).reduce(
|
|
158
115
|
(a, b) => a + b,
|
|
159
116
|
0
|
|
160
117
|
);
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
} else if (activeWorkers + pendingClaims >= maxWorkers) {
|
|
165
|
-
app.workloop?.stop(
|
|
166
|
-
`server at capacity (${activeWorkers}/${maxWorkers}, ${pendingClaims} pending)`
|
|
118
|
+
if (activeInWorkloop >= capacity) {
|
|
119
|
+
workloop.stop(
|
|
120
|
+
`workloop ${workloop.id} at capacity (${activeInWorkloop}/${capacity})`
|
|
167
121
|
);
|
|
168
|
-
return reject(new ClaimError("
|
|
122
|
+
return reject(new ClaimError("Workloop at capacity"));
|
|
123
|
+
} else if (activeInWorkloop + pendingWorkloopClaims >= capacity) {
|
|
124
|
+
workloop.stop(
|
|
125
|
+
`workloop ${workloop.id} at capacity (${activeInWorkloop}/${capacity}, ${pendingWorkloopClaims} pending)`
|
|
126
|
+
);
|
|
127
|
+
return reject(new ClaimError("Workloop at capacity"));
|
|
169
128
|
}
|
|
170
129
|
if (!app.queueChannel) {
|
|
171
130
|
logger.warn("skipping claim attempt: websocket unavailable");
|
|
@@ -178,21 +137,22 @@ var claim = (app, logger = mockLogger, options = {}) => {
|
|
|
178
137
|
return reject(e);
|
|
179
138
|
}
|
|
180
139
|
const claimId = ++claimIdGen;
|
|
181
|
-
|
|
140
|
+
workloop.openClaims[claimId] = demand;
|
|
182
141
|
const { used_heap_size, heap_size_limit } = v8.getHeapStatistics();
|
|
183
142
|
const usedHeapMb = Math.round(used_heap_size / 1024 / 1024);
|
|
184
143
|
const totalHeapMb = Math.round(heap_size_limit / 1024 / 1024);
|
|
185
144
|
const memPercent = Math.round(usedHeapMb / totalHeapMb * 100);
|
|
186
145
|
logger.debug(
|
|
187
|
-
`Claiming runs :: demand ${demand} | capacity ${
|
|
146
|
+
`Claiming runs [${workloop.id}] :: demand ${demand} | capacity ${activeInWorkloop}/${capacity} | memory ${memPercent}% (${usedHeapMb}/${totalHeapMb}mb)`
|
|
188
147
|
);
|
|
189
148
|
app.events.emit(INTERNAL_CLAIM_START);
|
|
190
149
|
const start = Date.now();
|
|
191
150
|
app.queueChannel.push(CLAIM, {
|
|
192
151
|
demand,
|
|
193
|
-
worker_name: NAME || null
|
|
152
|
+
worker_name: NAME || null,
|
|
153
|
+
queues: workloop.queues
|
|
194
154
|
}).receive("ok", async ({ runs }) => {
|
|
195
|
-
delete
|
|
155
|
+
delete workloop.openClaims[claimId];
|
|
196
156
|
const duration = Date.now() - start;
|
|
197
157
|
logger.debug(
|
|
198
158
|
`${podName}claimed ${runs.length} runs in ${duration}ms (${runs.length ? runs.map((r) => r.id).join(",") : "-"})`
|
|
@@ -216,17 +176,19 @@ var claim = (app, logger = mockLogger, options = {}) => {
|
|
|
216
176
|
} else {
|
|
217
177
|
logger.debug("skipping run token validation for", run.id);
|
|
218
178
|
}
|
|
179
|
+
workloop.activeRuns.add(run.id);
|
|
180
|
+
app.runWorkloopMap[run.id] = workloop;
|
|
219
181
|
logger.debug(`${podName} starting run ${run.id}`);
|
|
220
182
|
app.execute(run);
|
|
221
183
|
}
|
|
222
184
|
resolve();
|
|
223
185
|
app.events.emit(INTERNAL_CLAIM_COMPLETE, { runs });
|
|
224
186
|
}).receive("error", (e) => {
|
|
225
|
-
delete
|
|
187
|
+
delete workloop.openClaims[claimId];
|
|
226
188
|
logger.error("Error on claim", e);
|
|
227
189
|
reject(new Error("claim error"));
|
|
228
190
|
}).receive("timeout", () => {
|
|
229
|
-
delete
|
|
191
|
+
delete workloop.openClaims[claimId];
|
|
230
192
|
logger.error("TIMEOUT on claim. Runs may be lost.");
|
|
231
193
|
reject(new Error("timeout"));
|
|
232
194
|
});
|
|
@@ -234,43 +196,6 @@ var claim = (app, logger = mockLogger, options = {}) => {
|
|
|
234
196
|
};
|
|
235
197
|
var claim_default = claim;
|
|
236
198
|
|
|
237
|
-
// src/api/workloop.ts
|
|
238
|
-
var startWorkloop = (app, logger, minBackoff, maxBackoff, maxWorkers) => {
|
|
239
|
-
let promise;
|
|
240
|
-
let cancelled = false;
|
|
241
|
-
const workLoop = () => {
|
|
242
|
-
if (!cancelled) {
|
|
243
|
-
promise = try_with_backoff_default(
|
|
244
|
-
() => claim_default(app, logger, {
|
|
245
|
-
maxWorkers
|
|
246
|
-
}),
|
|
247
|
-
{
|
|
248
|
-
min: minBackoff,
|
|
249
|
-
max: maxBackoff
|
|
250
|
-
}
|
|
251
|
-
);
|
|
252
|
-
promise.then(() => {
|
|
253
|
-
if (!cancelled) {
|
|
254
|
-
setTimeout(workLoop, minBackoff);
|
|
255
|
-
}
|
|
256
|
-
}).catch(() => {
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
};
|
|
260
|
-
workLoop();
|
|
261
|
-
return {
|
|
262
|
-
stop: (reason = "reason unknown") => {
|
|
263
|
-
if (!cancelled) {
|
|
264
|
-
logger.info(`cancelling workloop: ${reason}`);
|
|
265
|
-
cancelled = true;
|
|
266
|
-
promise.cancel();
|
|
267
|
-
}
|
|
268
|
-
},
|
|
269
|
-
isStopped: () => cancelled
|
|
270
|
-
};
|
|
271
|
-
};
|
|
272
|
-
var workloop_default = startWorkloop;
|
|
273
|
-
|
|
274
199
|
// src/api/execute.ts
|
|
275
200
|
import * as Sentry4 from "@sentry/node";
|
|
276
201
|
import {
|
|
@@ -575,6 +500,51 @@ var stringify_default = (obj) => stringify(obj, (_key, value) => {
|
|
|
575
500
|
return value;
|
|
576
501
|
});
|
|
577
502
|
|
|
503
|
+
// src/util/try-with-backoff.ts
|
|
504
|
+
var BACKOFF_MULTIPLIER = 1.15;
|
|
505
|
+
var tryWithBackoff = (fn, opts = {}) => {
|
|
506
|
+
const { min = 1e3, max = 1e4, maxRuns, runs = 1 } = opts;
|
|
507
|
+
let cancelled = false;
|
|
508
|
+
if (!opts.isCancelled) {
|
|
509
|
+
opts.isCancelled = () => cancelled;
|
|
510
|
+
}
|
|
511
|
+
const promise = new Promise(async (resolve, reject) => {
|
|
512
|
+
try {
|
|
513
|
+
await fn();
|
|
514
|
+
resolve();
|
|
515
|
+
} catch (e) {
|
|
516
|
+
if (e?.abort) {
|
|
517
|
+
cancelled = true;
|
|
518
|
+
return reject();
|
|
519
|
+
}
|
|
520
|
+
if (opts.isCancelled()) {
|
|
521
|
+
return resolve();
|
|
522
|
+
}
|
|
523
|
+
if (!isNaN(maxRuns) && runs >= maxRuns) {
|
|
524
|
+
return reject(new Error("max runs exceeded"));
|
|
525
|
+
}
|
|
526
|
+
setTimeout(() => {
|
|
527
|
+
if (opts.isCancelled()) {
|
|
528
|
+
return resolve();
|
|
529
|
+
}
|
|
530
|
+
const nextOpts = {
|
|
531
|
+
maxRuns,
|
|
532
|
+
runs: runs + 1,
|
|
533
|
+
min: Math.min(max, min * BACKOFF_MULTIPLIER),
|
|
534
|
+
max,
|
|
535
|
+
isCancelled: opts.isCancelled
|
|
536
|
+
};
|
|
537
|
+
tryWithBackoff(fn, nextOpts).then(resolve).catch(reject);
|
|
538
|
+
}, min);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
promise.cancel = () => {
|
|
542
|
+
cancelled = true;
|
|
543
|
+
};
|
|
544
|
+
return promise;
|
|
545
|
+
};
|
|
546
|
+
var try_with_backoff_default = tryWithBackoff;
|
|
547
|
+
|
|
578
548
|
// src/util/timestamp.ts
|
|
579
549
|
var timeInMicroseconds = (time) => time && (BigInt(time) / BigInt(1e3)).toString();
|
|
580
550
|
|
|
@@ -1235,6 +1205,7 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger, options) => {
|
|
|
1235
1205
|
messageTimeout = DEFAULT_MESSAGE_TIMEOUT_SECONDS,
|
|
1236
1206
|
claimTimeout = DEFAULT_CLAIM_TIMEOUT_SECONDS,
|
|
1237
1207
|
capacity,
|
|
1208
|
+
queues,
|
|
1238
1209
|
SocketConstructor = PhxSocket
|
|
1239
1210
|
} = options;
|
|
1240
1211
|
const events = new EventEmitter();
|
|
@@ -1271,6 +1242,9 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger, options) => {
|
|
|
1271
1242
|
didOpen = true;
|
|
1272
1243
|
shouldReportConnectionError = true;
|
|
1273
1244
|
const joinPayload = { capacity };
|
|
1245
|
+
if (queues) {
|
|
1246
|
+
joinPayload.queues = queues;
|
|
1247
|
+
}
|
|
1274
1248
|
const channel = socket.channel("worker:queue", joinPayload);
|
|
1275
1249
|
channel.onMessage = (ev, load) => {
|
|
1276
1250
|
events.emit("message", ev, load);
|
|
@@ -1311,6 +1285,140 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger, options) => {
|
|
|
1311
1285
|
};
|
|
1312
1286
|
var worker_queue_default = connectToWorkerQueue;
|
|
1313
1287
|
|
|
1288
|
+
// src/api/workloop.ts
|
|
1289
|
+
var Workloop = class {
|
|
1290
|
+
constructor({
|
|
1291
|
+
id,
|
|
1292
|
+
queues,
|
|
1293
|
+
capacity
|
|
1294
|
+
}) {
|
|
1295
|
+
this.activeRuns = /* @__PURE__ */ new Set();
|
|
1296
|
+
this.openClaims = {};
|
|
1297
|
+
this.cancelled = true;
|
|
1298
|
+
this.id = id;
|
|
1299
|
+
this.queues = queues;
|
|
1300
|
+
this.capacity = capacity;
|
|
1301
|
+
}
|
|
1302
|
+
hasCapacity() {
|
|
1303
|
+
const pendingClaims = Object.values(this.openClaims).reduce(
|
|
1304
|
+
(a, b) => a + b,
|
|
1305
|
+
0
|
|
1306
|
+
);
|
|
1307
|
+
return this.activeRuns.size + pendingClaims < this.capacity;
|
|
1308
|
+
}
|
|
1309
|
+
start(app, logger, minBackoff, maxBackoff) {
|
|
1310
|
+
this.logger = logger;
|
|
1311
|
+
this.cancelled = false;
|
|
1312
|
+
const loop = () => {
|
|
1313
|
+
if (!this.cancelled) {
|
|
1314
|
+
this.promise = try_with_backoff_default(() => claim_default(app, this, logger), {
|
|
1315
|
+
min: minBackoff,
|
|
1316
|
+
max: maxBackoff
|
|
1317
|
+
});
|
|
1318
|
+
this.promise.then(() => {
|
|
1319
|
+
if (!this.cancelled) {
|
|
1320
|
+
setTimeout(loop, minBackoff);
|
|
1321
|
+
}
|
|
1322
|
+
}).catch(() => {
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
};
|
|
1326
|
+
loop();
|
|
1327
|
+
}
|
|
1328
|
+
stop(reason = "reason unknown") {
|
|
1329
|
+
if (!this.cancelled) {
|
|
1330
|
+
this.logger?.info(`cancelling workloop: ${reason}`);
|
|
1331
|
+
this.cancelled = true;
|
|
1332
|
+
this.promise?.cancel();
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
isStopped() {
|
|
1336
|
+
return this.cancelled;
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
// src/util/parse-workloops.ts
|
|
1341
|
+
var WorkloopValidationError = class extends Error {
|
|
1342
|
+
constructor(message) {
|
|
1343
|
+
super(message);
|
|
1344
|
+
this.name = "WorkloopValidationError";
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
var VALID_NAME = /^[a-zA-Z0-9_]+$/;
|
|
1348
|
+
function parseWorkloops(input) {
|
|
1349
|
+
const trimmed = input.trim();
|
|
1350
|
+
if (!trimmed) {
|
|
1351
|
+
throw new WorkloopValidationError("Workloop configuration cannot be empty");
|
|
1352
|
+
}
|
|
1353
|
+
const tokens = trimmed.split(/\s+/);
|
|
1354
|
+
const configs = tokens.map(parseToken);
|
|
1355
|
+
const seenConfigs = /* @__PURE__ */ new Map();
|
|
1356
|
+
for (let i = 0; i < configs.length; i++) {
|
|
1357
|
+
const key = JSON.stringify(configs[i].queues);
|
|
1358
|
+
if (seenConfigs.has(key)) {
|
|
1359
|
+
const prevIndex = seenConfigs.get(key);
|
|
1360
|
+
console.warn(
|
|
1361
|
+
`Warning: workloops at positions ${prevIndex} and ${i} have identical queue configurations: ${tokens[prevIndex]} and ${tokens[i]}`
|
|
1362
|
+
);
|
|
1363
|
+
} else {
|
|
1364
|
+
seenConfigs.set(key, i);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return configs;
|
|
1368
|
+
}
|
|
1369
|
+
function parseToken(token) {
|
|
1370
|
+
const lastColon = token.lastIndexOf(":");
|
|
1371
|
+
if (lastColon === -1) {
|
|
1372
|
+
throw new WorkloopValidationError(
|
|
1373
|
+
`Invalid token "${token}": missing :<count> suffix`
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
const prefStr = token.slice(0, lastColon);
|
|
1377
|
+
const countStr = token.slice(lastColon + 1);
|
|
1378
|
+
const count = Number(countStr);
|
|
1379
|
+
if (!Number.isInteger(count) || countStr !== String(Math.floor(count))) {
|
|
1380
|
+
throw new WorkloopValidationError(
|
|
1381
|
+
`Invalid count "${countStr}" in token "${token}": must be a positive integer`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
if (count < 1) {
|
|
1385
|
+
throw new WorkloopValidationError(
|
|
1386
|
+
`Invalid count "${countStr}" in token "${token}": must be >= 1`
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
const names = prefStr.split(">");
|
|
1390
|
+
for (const name of names) {
|
|
1391
|
+
if (name === "") {
|
|
1392
|
+
throw new WorkloopValidationError(`Empty queue name in token "${token}"`);
|
|
1393
|
+
}
|
|
1394
|
+
if (name !== "*" && !VALID_NAME.test(name)) {
|
|
1395
|
+
throw new WorkloopValidationError(
|
|
1396
|
+
`Invalid queue name "${name}" in token "${token}": must match /^[a-zA-Z0-9_]+$/ or be "*"`
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
const nonWildcardNames = names.filter((n) => n !== "*");
|
|
1401
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1402
|
+
for (const name of nonWildcardNames) {
|
|
1403
|
+
if (seen.has(name)) {
|
|
1404
|
+
console.warn(
|
|
1405
|
+
`Warning: duplicate queue name "${name}" in token "${token}"`
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
seen.add(name);
|
|
1409
|
+
}
|
|
1410
|
+
const wildcardIndex = names.indexOf("*");
|
|
1411
|
+
if (wildcardIndex !== -1 && wildcardIndex !== names.length - 1) {
|
|
1412
|
+
throw new WorkloopValidationError(
|
|
1413
|
+
`Wildcard "*" must be the last element in token "${token}"`
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
return new Workloop({ id: token, queues: names, capacity: count });
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// src/util/get-default-workloop-config.ts
|
|
1420
|
+
var get_default_workloop_config_default = (capacity = 5) => `manual>*:${capacity}`;
|
|
1421
|
+
|
|
1314
1422
|
// src/server.ts
|
|
1315
1423
|
var exec = promisify(_exec);
|
|
1316
1424
|
var DEFAULT_PORT = 2222;
|
|
@@ -1338,8 +1446,10 @@ function connect(app, logger, options = {}) {
|
|
|
1338
1446
|
app.resumeWorkloop();
|
|
1339
1447
|
};
|
|
1340
1448
|
const onDisconnect = () => {
|
|
1341
|
-
|
|
1342
|
-
|
|
1449
|
+
for (const w of app.workloops) {
|
|
1450
|
+
if (!w.isStopped()) {
|
|
1451
|
+
w.stop("Socket disconnected unexpectedly");
|
|
1452
|
+
}
|
|
1343
1453
|
}
|
|
1344
1454
|
if (!app.destroyed) {
|
|
1345
1455
|
logger.info("Connection to lightning lost");
|
|
@@ -1361,17 +1471,26 @@ function connect(app, logger, options = {}) {
|
|
|
1361
1471
|
const onMessage = (event) => {
|
|
1362
1472
|
if (event === WORK_AVAILABLE) {
|
|
1363
1473
|
if (!app.destroyed) {
|
|
1364
|
-
|
|
1365
|
-
|
|
1474
|
+
for (const w of app.workloops) {
|
|
1475
|
+
if (w.hasCapacity()) {
|
|
1476
|
+
claim_default(app, w, logger).catch(() => {
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1366
1480
|
}
|
|
1367
1481
|
}
|
|
1368
1482
|
};
|
|
1483
|
+
const queuesMap = {};
|
|
1484
|
+
for (const w of app.workloops) {
|
|
1485
|
+
queuesMap[w.queues.join(">")] = w.capacity;
|
|
1486
|
+
}
|
|
1369
1487
|
worker_queue_default(options.lightning, app.id, options.secret, logger, {
|
|
1370
1488
|
// TODO: options.socketTimeoutSeconds wins because this is what USED to be used
|
|
1371
1489
|
// But it's deprecated and should be removed soon
|
|
1372
1490
|
messageTimeout: options.socketTimeoutSeconds ?? options.messageTimeoutSeconds,
|
|
1373
1491
|
claimTimeout: options.claimTimeoutSeconds,
|
|
1374
|
-
capacity: options.maxWorkflows
|
|
1492
|
+
capacity: options.maxWorkflows,
|
|
1493
|
+
queues: queuesMap
|
|
1375
1494
|
}).on("connect", onConnect).on("disconnect", onDisconnect).on("error", onError).on("message", onMessage);
|
|
1376
1495
|
}
|
|
1377
1496
|
async function setupCollections(options, logger) {
|
|
@@ -1420,27 +1539,36 @@ function createServer(engine, options = {}) {
|
|
|
1420
1539
|
logger.debug(str);
|
|
1421
1540
|
})
|
|
1422
1541
|
);
|
|
1423
|
-
app.openClaims = {};
|
|
1424
1542
|
app.workflows = {};
|
|
1425
1543
|
app.destroyed = false;
|
|
1544
|
+
app.workloops = parseWorkloops(
|
|
1545
|
+
options.workloopConfigs ?? get_default_workloop_config_default(options.maxWorkflows)
|
|
1546
|
+
);
|
|
1547
|
+
app.runWorkloopMap = {};
|
|
1426
1548
|
app.server = app.listen(port);
|
|
1427
1549
|
logger.success(`Worker ${app.id} listening on ${port}`);
|
|
1428
1550
|
process.send?.("READY");
|
|
1429
1551
|
router.get("/livez", healthcheck_default);
|
|
1430
1552
|
router.get("/", healthcheck_default);
|
|
1431
1553
|
app.options = options;
|
|
1432
|
-
app.resumeWorkloop = () => {
|
|
1554
|
+
app.resumeWorkloop = (workloop) => {
|
|
1433
1555
|
if (options.noLoop || app.destroyed) {
|
|
1434
1556
|
return;
|
|
1435
1557
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1558
|
+
const targets = workloop ? [workloop] : app.workloops;
|
|
1559
|
+
for (const w of targets) {
|
|
1560
|
+
if (!w.hasCapacity()) {
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
if (!w.isStopped()) {
|
|
1564
|
+
w.stop("restarting");
|
|
1565
|
+
}
|
|
1566
|
+
logger.info(`Starting workloop for ${w.id}`);
|
|
1567
|
+
w.start(
|
|
1439
1568
|
app,
|
|
1440
1569
|
logger,
|
|
1441
1570
|
options.backoff?.min || MIN_BACKOFF,
|
|
1442
|
-
options.backoff?.max || MAX_BACKOFF
|
|
1443
|
-
options.maxWorkflows
|
|
1571
|
+
options.backoff?.max || MAX_BACKOFF
|
|
1444
1572
|
);
|
|
1445
1573
|
}
|
|
1446
1574
|
};
|
|
@@ -1485,8 +1613,16 @@ function createServer(engine, options = {}) {
|
|
|
1485
1613
|
);
|
|
1486
1614
|
delete app.workflows[id];
|
|
1487
1615
|
runChannel.leave();
|
|
1488
|
-
app.
|
|
1489
|
-
|
|
1616
|
+
const owningWorkloop = app.runWorkloopMap[id];
|
|
1617
|
+
if (owningWorkloop) {
|
|
1618
|
+
owningWorkloop.activeRuns.delete(id);
|
|
1619
|
+
delete app.runWorkloopMap[id];
|
|
1620
|
+
app.events.emit(INTERNAL_RUN_COMPLETE);
|
|
1621
|
+
app.resumeWorkloop(owningWorkloop);
|
|
1622
|
+
} else {
|
|
1623
|
+
app.events.emit(INTERNAL_RUN_COMPLETE);
|
|
1624
|
+
app.resumeWorkloop();
|
|
1625
|
+
}
|
|
1490
1626
|
};
|
|
1491
1627
|
const context = execute(
|
|
1492
1628
|
runChannel,
|
|
@@ -1500,7 +1636,14 @@ function createServer(engine, options = {}) {
|
|
|
1500
1636
|
app.workflows[id] = context;
|
|
1501
1637
|
} catch (e) {
|
|
1502
1638
|
delete app.workflows[id];
|
|
1503
|
-
app.
|
|
1639
|
+
const owningWorkloop = app.runWorkloopMap[id];
|
|
1640
|
+
if (owningWorkloop) {
|
|
1641
|
+
owningWorkloop.activeRuns.delete(id);
|
|
1642
|
+
delete app.runWorkloopMap[id];
|
|
1643
|
+
app.resumeWorkloop(owningWorkloop);
|
|
1644
|
+
} else {
|
|
1645
|
+
app.resumeWorkloop();
|
|
1646
|
+
}
|
|
1504
1647
|
logger.error(`Unexpected error executing ${id}`);
|
|
1505
1648
|
logger.error(e);
|
|
1506
1649
|
}
|
|
@@ -1510,9 +1653,13 @@ function createServer(engine, options = {}) {
|
|
|
1510
1653
|
};
|
|
1511
1654
|
router.post("/claim", async (ctx) => {
|
|
1512
1655
|
logger.info("triggering claim from POST request");
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1656
|
+
const promises = app.workloops.map((w) => {
|
|
1657
|
+
if (w.hasCapacity()) {
|
|
1658
|
+
return claim_default(app, w, logger);
|
|
1659
|
+
}
|
|
1660
|
+
return Promise.reject(new Error("Workloop at capacity"));
|
|
1661
|
+
});
|
|
1662
|
+
return Promise.any(promises).then(() => {
|
|
1516
1663
|
logger.info("claim complete: 1 run claimed");
|
|
1517
1664
|
ctx.body = "complete";
|
|
1518
1665
|
ctx.status = 200;
|
|
@@ -1523,10 +1670,15 @@ function createServer(engine, options = {}) {
|
|
|
1523
1670
|
});
|
|
1524
1671
|
});
|
|
1525
1672
|
app.claim = () => {
|
|
1526
|
-
|
|
1527
|
-
|
|
1673
|
+
const promises = app.workloops.map((w) => {
|
|
1674
|
+
if (w.hasCapacity()) {
|
|
1675
|
+
return claim_default(app, w, logger);
|
|
1676
|
+
}
|
|
1677
|
+
return Promise.reject(new Error("Workloop at capacity"));
|
|
1528
1678
|
});
|
|
1679
|
+
return Promise.any(promises);
|
|
1529
1680
|
};
|
|
1681
|
+
app.pendingClaims = () => app.workloops.reduce((sum, w) => sum + Object.keys(w.openClaims).length, 0);
|
|
1530
1682
|
app.destroy = () => destroy_default(app, logger);
|
|
1531
1683
|
app.use(router.routes());
|
|
1532
1684
|
if (options.lightning) {
|