@openfn/ws-worker 1.21.5 → 1.22.1
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 +20 -0
- package/README.md +79 -0
- package/dist/index.d.ts +25 -8
- package/dist/index.js +284 -137
- package/dist/start.js +318 -142
- package/package.json +5 -5
package/dist/start.js
CHANGED
|
@@ -182,29 +182,30 @@ var destroy = async (app, logger2) => {
|
|
|
182
182
|
await Promise.all([
|
|
183
183
|
new Promise((resolve5) => {
|
|
184
184
|
app.destroyed = true;
|
|
185
|
-
app.
|
|
185
|
+
for (const w of app.workloops) {
|
|
186
|
+
w.stop("server closed");
|
|
187
|
+
}
|
|
186
188
|
app.server.close(async () => {
|
|
187
189
|
resolve5();
|
|
188
190
|
});
|
|
189
191
|
}),
|
|
190
|
-
|
|
192
|
+
(async () => {
|
|
191
193
|
await waitForRunsAndClaims(app, logger2);
|
|
192
194
|
app.queueChannel?.leave();
|
|
193
195
|
await app.engine.destroy();
|
|
194
196
|
app.socket?.disconnect();
|
|
195
|
-
|
|
196
|
-
})
|
|
197
|
+
})()
|
|
197
198
|
]);
|
|
198
199
|
logger2.success("Server closed");
|
|
199
200
|
};
|
|
200
201
|
var waitForRunsAndClaims = (app, logger2) => new Promise((resolve5) => {
|
|
201
202
|
const log = () => {
|
|
202
203
|
logger2.debug(
|
|
203
|
-
`Waiting for ${Object.keys(app.workflows).length} runs and ${
|
|
204
|
+
`Waiting for ${Object.keys(app.workflows).length} runs and ${app.pendingClaims()} claims to complete...`
|
|
204
205
|
);
|
|
205
206
|
};
|
|
206
207
|
const checkAllClear = () => {
|
|
207
|
-
if (Object.keys(app.workflows).length +
|
|
208
|
+
if (Object.keys(app.workflows).length + app.pendingClaims() === 0) {
|
|
208
209
|
logger2.debug("All runs completed!");
|
|
209
210
|
app.events.off(INTERNAL_RUN_COMPLETE, checkAllClear);
|
|
210
211
|
app.events.off(INTERNAL_CLAIM_COMPLETE, checkAllClear);
|
|
@@ -213,7 +214,7 @@ var waitForRunsAndClaims = (app, logger2) => new Promise((resolve5) => {
|
|
|
213
214
|
log();
|
|
214
215
|
}
|
|
215
216
|
};
|
|
216
|
-
if (Object.keys(app.workflows).length ||
|
|
217
|
+
if (Object.keys(app.workflows).length || app.pendingClaims()) {
|
|
217
218
|
log();
|
|
218
219
|
app.events.on(INTERNAL_RUN_COMPLETE, checkAllClear);
|
|
219
220
|
app.events.on(INTERNAL_CLAIM_COMPLETE, checkAllClear);
|
|
@@ -224,51 +225,6 @@ var waitForRunsAndClaims = (app, logger2) => new Promise((resolve5) => {
|
|
|
224
225
|
});
|
|
225
226
|
var destroy_default = destroy;
|
|
226
227
|
|
|
227
|
-
// src/util/try-with-backoff.ts
|
|
228
|
-
var BACKOFF_MULTIPLIER = 1.15;
|
|
229
|
-
var tryWithBackoff = (fn, opts = {}) => {
|
|
230
|
-
const { min = 1e3, max = 1e4, maxRuns, runs = 1 } = opts;
|
|
231
|
-
let cancelled = false;
|
|
232
|
-
if (!opts.isCancelled) {
|
|
233
|
-
opts.isCancelled = () => cancelled;
|
|
234
|
-
}
|
|
235
|
-
const promise = new Promise(async (resolve5, reject) => {
|
|
236
|
-
try {
|
|
237
|
-
await fn();
|
|
238
|
-
resolve5();
|
|
239
|
-
} catch (e) {
|
|
240
|
-
if (e?.abort) {
|
|
241
|
-
cancelled = true;
|
|
242
|
-
return reject();
|
|
243
|
-
}
|
|
244
|
-
if (opts.isCancelled()) {
|
|
245
|
-
return resolve5();
|
|
246
|
-
}
|
|
247
|
-
if (!isNaN(maxRuns) && runs >= maxRuns) {
|
|
248
|
-
return reject(new Error("max runs exceeded"));
|
|
249
|
-
}
|
|
250
|
-
setTimeout(() => {
|
|
251
|
-
if (opts.isCancelled()) {
|
|
252
|
-
return resolve5();
|
|
253
|
-
}
|
|
254
|
-
const nextOpts = {
|
|
255
|
-
maxRuns,
|
|
256
|
-
runs: runs + 1,
|
|
257
|
-
min: Math.min(max, min * BACKOFF_MULTIPLIER),
|
|
258
|
-
max,
|
|
259
|
-
isCancelled: opts.isCancelled
|
|
260
|
-
};
|
|
261
|
-
tryWithBackoff(fn, nextOpts).then(resolve5).catch(reject);
|
|
262
|
-
}, min);
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
promise.cancel = () => {
|
|
266
|
-
cancelled = true;
|
|
267
|
-
};
|
|
268
|
-
return promise;
|
|
269
|
-
};
|
|
270
|
-
var try_with_backoff_default = tryWithBackoff;
|
|
271
|
-
|
|
272
228
|
// src/api/claim.ts
|
|
273
229
|
import v8 from "node:v8";
|
|
274
230
|
import * as Sentry from "@sentry/node";
|
|
@@ -297,24 +253,26 @@ var ClaimError = class extends Error {
|
|
|
297
253
|
}
|
|
298
254
|
};
|
|
299
255
|
var claimIdGen = 0;
|
|
300
|
-
var claim = (app, logger2 = mockLogger, options = {}) => {
|
|
256
|
+
var claim = (app, workloop, logger2 = mockLogger, options = {}) => {
|
|
301
257
|
return new Promise((resolve5, reject) => {
|
|
302
|
-
|
|
303
|
-
const { maxWorkers = 5, demand = 1 } = options;
|
|
258
|
+
const { demand = 1 } = options;
|
|
304
259
|
const podName = NAME ? `[${NAME}] ` : "";
|
|
305
|
-
const
|
|
306
|
-
const
|
|
260
|
+
const activeInWorkloop = workloop.activeRuns.size;
|
|
261
|
+
const capacity = workloop.capacity;
|
|
262
|
+
const pendingWorkloopClaims = Object.values(workloop.openClaims).reduce(
|
|
307
263
|
(a, b) => a + b,
|
|
308
264
|
0
|
|
309
265
|
);
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
} else if (activeWorkers + pendingClaims >= maxWorkers) {
|
|
314
|
-
app.workloop?.stop(
|
|
315
|
-
`server at capacity (${activeWorkers}/${maxWorkers}, ${pendingClaims} pending)`
|
|
266
|
+
if (activeInWorkloop >= capacity) {
|
|
267
|
+
workloop.stop(
|
|
268
|
+
`workloop ${workloop.id} at capacity (${activeInWorkloop}/${capacity})`
|
|
316
269
|
);
|
|
317
|
-
return reject(new ClaimError("
|
|
270
|
+
return reject(new ClaimError("Workloop at capacity"));
|
|
271
|
+
} else if (activeInWorkloop + pendingWorkloopClaims >= capacity) {
|
|
272
|
+
workloop.stop(
|
|
273
|
+
`workloop ${workloop.id} at capacity (${activeInWorkloop}/${capacity}, ${pendingWorkloopClaims} pending)`
|
|
274
|
+
);
|
|
275
|
+
return reject(new ClaimError("Workloop at capacity"));
|
|
318
276
|
}
|
|
319
277
|
if (!app.queueChannel) {
|
|
320
278
|
logger2.warn("skipping claim attempt: websocket unavailable");
|
|
@@ -327,21 +285,22 @@ var claim = (app, logger2 = mockLogger, options = {}) => {
|
|
|
327
285
|
return reject(e);
|
|
328
286
|
}
|
|
329
287
|
const claimId = ++claimIdGen;
|
|
330
|
-
|
|
288
|
+
workloop.openClaims[claimId] = demand;
|
|
331
289
|
const { used_heap_size, heap_size_limit } = v8.getHeapStatistics();
|
|
332
290
|
const usedHeapMb = Math.round(used_heap_size / 1024 / 1024);
|
|
333
291
|
const totalHeapMb = Math.round(heap_size_limit / 1024 / 1024);
|
|
334
292
|
const memPercent = Math.round(usedHeapMb / totalHeapMb * 100);
|
|
335
293
|
logger2.debug(
|
|
336
|
-
`Claiming runs :: demand ${demand} | capacity ${
|
|
294
|
+
`Claiming runs [${workloop.id}] :: demand ${demand} | capacity ${activeInWorkloop}/${capacity} | memory ${memPercent}% (${usedHeapMb}/${totalHeapMb}mb)`
|
|
337
295
|
);
|
|
338
296
|
app.events.emit(INTERNAL_CLAIM_START);
|
|
339
297
|
const start = Date.now();
|
|
340
298
|
app.queueChannel.push(CLAIM, {
|
|
341
299
|
demand,
|
|
342
|
-
worker_name: NAME || null
|
|
300
|
+
worker_name: NAME || null,
|
|
301
|
+
queues: workloop.queues
|
|
343
302
|
}).receive("ok", async ({ runs }) => {
|
|
344
|
-
delete
|
|
303
|
+
delete workloop.openClaims[claimId];
|
|
345
304
|
const duration = Date.now() - start;
|
|
346
305
|
logger2.debug(
|
|
347
306
|
`${podName}claimed ${runs.length} runs in ${duration}ms (${runs.length ? runs.map((r) => r.id).join(",") : "-"})`
|
|
@@ -365,17 +324,19 @@ var claim = (app, logger2 = mockLogger, options = {}) => {
|
|
|
365
324
|
} else {
|
|
366
325
|
logger2.debug("skipping run token validation for", run2.id);
|
|
367
326
|
}
|
|
327
|
+
workloop.activeRuns.add(run2.id);
|
|
328
|
+
app.runWorkloopMap[run2.id] = workloop;
|
|
368
329
|
logger2.debug(`${podName} starting run ${run2.id}`);
|
|
369
330
|
app.execute(run2);
|
|
370
331
|
}
|
|
371
332
|
resolve5();
|
|
372
333
|
app.events.emit(INTERNAL_CLAIM_COMPLETE, { runs });
|
|
373
334
|
}).receive("error", (e) => {
|
|
374
|
-
delete
|
|
335
|
+
delete workloop.openClaims[claimId];
|
|
375
336
|
logger2.error("Error on claim", e);
|
|
376
337
|
reject(new Error("claim error"));
|
|
377
338
|
}).receive("timeout", () => {
|
|
378
|
-
delete
|
|
339
|
+
delete workloop.openClaims[claimId];
|
|
379
340
|
logger2.error("TIMEOUT on claim. Runs may be lost.");
|
|
380
341
|
reject(new Error("timeout"));
|
|
381
342
|
});
|
|
@@ -383,43 +344,6 @@ var claim = (app, logger2 = mockLogger, options = {}) => {
|
|
|
383
344
|
};
|
|
384
345
|
var claim_default = claim;
|
|
385
346
|
|
|
386
|
-
// src/api/workloop.ts
|
|
387
|
-
var startWorkloop = (app, logger2, minBackoff2, maxBackoff2, maxWorkers) => {
|
|
388
|
-
let promise;
|
|
389
|
-
let cancelled = false;
|
|
390
|
-
const workLoop = () => {
|
|
391
|
-
if (!cancelled) {
|
|
392
|
-
promise = try_with_backoff_default(
|
|
393
|
-
() => claim_default(app, logger2, {
|
|
394
|
-
maxWorkers
|
|
395
|
-
}),
|
|
396
|
-
{
|
|
397
|
-
min: minBackoff2,
|
|
398
|
-
max: maxBackoff2
|
|
399
|
-
}
|
|
400
|
-
);
|
|
401
|
-
promise.then(() => {
|
|
402
|
-
if (!cancelled) {
|
|
403
|
-
setTimeout(workLoop, minBackoff2);
|
|
404
|
-
}
|
|
405
|
-
}).catch(() => {
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
workLoop();
|
|
410
|
-
return {
|
|
411
|
-
stop: (reason = "reason unknown") => {
|
|
412
|
-
if (!cancelled) {
|
|
413
|
-
logger2.info(`cancelling workloop: ${reason}`);
|
|
414
|
-
cancelled = true;
|
|
415
|
-
promise.cancel();
|
|
416
|
-
}
|
|
417
|
-
},
|
|
418
|
-
isStopped: () => cancelled
|
|
419
|
-
};
|
|
420
|
-
};
|
|
421
|
-
var workloop_default = startWorkloop;
|
|
422
|
-
|
|
423
347
|
// src/api/execute.ts
|
|
424
348
|
import * as Sentry4 from "@sentry/node";
|
|
425
349
|
import {
|
|
@@ -724,6 +648,50 @@ var stringify_default = (obj) => stringify(obj, (_key, value) => {
|
|
|
724
648
|
return value;
|
|
725
649
|
});
|
|
726
650
|
|
|
651
|
+
// src/util/try-with-backoff.ts
|
|
652
|
+
var BACKOFF_MULTIPLIER = 1.15;
|
|
653
|
+
var tryWithBackoff = (fn, opts = {}) => {
|
|
654
|
+
const { min = 1e3, max = 1e4, maxRuns, runs = 1 } = opts;
|
|
655
|
+
let cancelled = false;
|
|
656
|
+
if (!opts.isCancelled) {
|
|
657
|
+
opts.isCancelled = () => cancelled;
|
|
658
|
+
}
|
|
659
|
+
const run2 = async () => {
|
|
660
|
+
try {
|
|
661
|
+
await fn();
|
|
662
|
+
} catch (e) {
|
|
663
|
+
if (e?.abort) {
|
|
664
|
+
cancelled = true;
|
|
665
|
+
throw e;
|
|
666
|
+
}
|
|
667
|
+
if (opts.isCancelled()) {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (!isNaN(maxRuns) && runs >= maxRuns) {
|
|
671
|
+
throw new Error("max runs exceeded");
|
|
672
|
+
}
|
|
673
|
+
await new Promise((resolve5) => setTimeout(resolve5, min));
|
|
674
|
+
if (opts.isCancelled()) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
const nextOpts = {
|
|
678
|
+
maxRuns,
|
|
679
|
+
runs: runs + 1,
|
|
680
|
+
min: Math.min(max, min * BACKOFF_MULTIPLIER),
|
|
681
|
+
max,
|
|
682
|
+
isCancelled: opts.isCancelled
|
|
683
|
+
};
|
|
684
|
+
return tryWithBackoff(fn, nextOpts);
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
const promise = run2();
|
|
688
|
+
promise.cancel = () => {
|
|
689
|
+
cancelled = true;
|
|
690
|
+
};
|
|
691
|
+
return promise;
|
|
692
|
+
};
|
|
693
|
+
var try_with_backoff_default = tryWithBackoff;
|
|
694
|
+
|
|
727
695
|
// src/util/timestamp.ts
|
|
728
696
|
var timeInMicroseconds = (time) => time && (BigInt(time) / BigInt(1e3)).toString();
|
|
729
697
|
|
|
@@ -764,16 +732,13 @@ async function onRunLog(context, events) {
|
|
|
764
732
|
};
|
|
765
733
|
return sendEvent(context, RUN_LOG_BATCH, payload);
|
|
766
734
|
} else {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
}
|
|
775
|
-
resolve5();
|
|
776
|
-
});
|
|
735
|
+
for (const log of logs) {
|
|
736
|
+
const payload = {
|
|
737
|
+
run_id: `${state.plan.id}`,
|
|
738
|
+
...log
|
|
739
|
+
};
|
|
740
|
+
await sendEvent(context, RUN_LOG, payload);
|
|
741
|
+
}
|
|
777
742
|
}
|
|
778
743
|
}
|
|
779
744
|
|
|
@@ -1384,6 +1349,7 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger2, options) => {
|
|
|
1384
1349
|
messageTimeout = DEFAULT_MESSAGE_TIMEOUT_SECONDS,
|
|
1385
1350
|
claimTimeout = DEFAULT_CLAIM_TIMEOUT_SECONDS,
|
|
1386
1351
|
capacity,
|
|
1352
|
+
queues,
|
|
1387
1353
|
SocketConstructor = PhxSocket
|
|
1388
1354
|
} = options;
|
|
1389
1355
|
const events = new EventEmitter2();
|
|
@@ -1420,6 +1386,9 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger2, options) => {
|
|
|
1420
1386
|
didOpen = true;
|
|
1421
1387
|
shouldReportConnectionError = true;
|
|
1422
1388
|
const joinPayload = { capacity };
|
|
1389
|
+
if (queues) {
|
|
1390
|
+
joinPayload.queues = queues;
|
|
1391
|
+
}
|
|
1423
1392
|
const channel = socket.channel("worker:queue", joinPayload);
|
|
1424
1393
|
channel.onMessage = (ev, load) => {
|
|
1425
1394
|
events.emit("message", ev, load);
|
|
@@ -1460,6 +1429,140 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger2, options) => {
|
|
|
1460
1429
|
};
|
|
1461
1430
|
var worker_queue_default = connectToWorkerQueue;
|
|
1462
1431
|
|
|
1432
|
+
// src/api/workloop.ts
|
|
1433
|
+
var Workloop = class {
|
|
1434
|
+
constructor({
|
|
1435
|
+
id,
|
|
1436
|
+
queues,
|
|
1437
|
+
capacity
|
|
1438
|
+
}) {
|
|
1439
|
+
this.activeRuns = /* @__PURE__ */ new Set();
|
|
1440
|
+
this.openClaims = {};
|
|
1441
|
+
this.cancelled = true;
|
|
1442
|
+
this.id = id;
|
|
1443
|
+
this.queues = queues;
|
|
1444
|
+
this.capacity = capacity;
|
|
1445
|
+
}
|
|
1446
|
+
hasCapacity() {
|
|
1447
|
+
const pendingClaims = Object.values(this.openClaims).reduce(
|
|
1448
|
+
(a, b) => a + b,
|
|
1449
|
+
0
|
|
1450
|
+
);
|
|
1451
|
+
return this.activeRuns.size + pendingClaims < this.capacity;
|
|
1452
|
+
}
|
|
1453
|
+
start(app, logger2, minBackoff2, maxBackoff2) {
|
|
1454
|
+
this.logger = logger2;
|
|
1455
|
+
this.cancelled = false;
|
|
1456
|
+
const loop = () => {
|
|
1457
|
+
if (!this.cancelled) {
|
|
1458
|
+
this.promise = try_with_backoff_default(() => claim_default(app, this, logger2), {
|
|
1459
|
+
min: minBackoff2,
|
|
1460
|
+
max: maxBackoff2
|
|
1461
|
+
});
|
|
1462
|
+
this.promise.then(() => {
|
|
1463
|
+
if (!this.cancelled) {
|
|
1464
|
+
setTimeout(loop, minBackoff2);
|
|
1465
|
+
}
|
|
1466
|
+
}).catch(() => {
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
loop();
|
|
1471
|
+
}
|
|
1472
|
+
stop(reason = "reason unknown") {
|
|
1473
|
+
if (!this.cancelled) {
|
|
1474
|
+
this.logger?.info(`cancelling workloop: ${reason}`);
|
|
1475
|
+
this.cancelled = true;
|
|
1476
|
+
this.promise?.cancel();
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
isStopped() {
|
|
1480
|
+
return this.cancelled;
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
|
|
1484
|
+
// src/util/parse-workloops.ts
|
|
1485
|
+
var WorkloopValidationError = class extends Error {
|
|
1486
|
+
constructor(message) {
|
|
1487
|
+
super(message);
|
|
1488
|
+
this.name = "WorkloopValidationError";
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
var VALID_NAME = /^[a-zA-Z0-9_]+$/;
|
|
1492
|
+
function parseWorkloops(input) {
|
|
1493
|
+
const trimmed = input.trim();
|
|
1494
|
+
if (!trimmed) {
|
|
1495
|
+
throw new WorkloopValidationError("Workloop configuration cannot be empty");
|
|
1496
|
+
}
|
|
1497
|
+
const tokens = trimmed.split(/\s+/);
|
|
1498
|
+
const configs = tokens.map(parseToken);
|
|
1499
|
+
const seenConfigs = /* @__PURE__ */ new Map();
|
|
1500
|
+
for (let i = 0; i < configs.length; i++) {
|
|
1501
|
+
const key = JSON.stringify(configs[i].queues);
|
|
1502
|
+
if (seenConfigs.has(key)) {
|
|
1503
|
+
const prevIndex = seenConfigs.get(key);
|
|
1504
|
+
console.warn(
|
|
1505
|
+
`Warning: workloops at positions ${prevIndex} and ${i} have identical queue configurations: ${tokens[prevIndex]} and ${tokens[i]}`
|
|
1506
|
+
);
|
|
1507
|
+
} else {
|
|
1508
|
+
seenConfigs.set(key, i);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return configs;
|
|
1512
|
+
}
|
|
1513
|
+
function parseToken(token) {
|
|
1514
|
+
const lastColon = token.lastIndexOf(":");
|
|
1515
|
+
if (lastColon === -1) {
|
|
1516
|
+
throw new WorkloopValidationError(
|
|
1517
|
+
`Invalid token "${token}": missing :<count> suffix`
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
const prefStr = token.slice(0, lastColon);
|
|
1521
|
+
const countStr = token.slice(lastColon + 1);
|
|
1522
|
+
const count = Number(countStr);
|
|
1523
|
+
if (!Number.isInteger(count) || countStr !== String(Math.floor(count))) {
|
|
1524
|
+
throw new WorkloopValidationError(
|
|
1525
|
+
`Invalid count "${countStr}" in token "${token}": must be a positive integer`
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
if (count < 1) {
|
|
1529
|
+
throw new WorkloopValidationError(
|
|
1530
|
+
`Invalid count "${countStr}" in token "${token}": must be >= 1`
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
const names = prefStr.split(">");
|
|
1534
|
+
for (const name of names) {
|
|
1535
|
+
if (name === "") {
|
|
1536
|
+
throw new WorkloopValidationError(`Empty queue name in token "${token}"`);
|
|
1537
|
+
}
|
|
1538
|
+
if (name !== "*" && !VALID_NAME.test(name)) {
|
|
1539
|
+
throw new WorkloopValidationError(
|
|
1540
|
+
`Invalid queue name "${name}" in token "${token}": must match /^[a-zA-Z0-9_]+$/ or be "*"`
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
const nonWildcardNames = names.filter((n) => n !== "*");
|
|
1545
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1546
|
+
for (const name of nonWildcardNames) {
|
|
1547
|
+
if (seen.has(name)) {
|
|
1548
|
+
console.warn(
|
|
1549
|
+
`Warning: duplicate queue name "${name}" in token "${token}"`
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
seen.add(name);
|
|
1553
|
+
}
|
|
1554
|
+
const wildcardIndex = names.indexOf("*");
|
|
1555
|
+
if (wildcardIndex !== -1 && wildcardIndex !== names.length - 1) {
|
|
1556
|
+
throw new WorkloopValidationError(
|
|
1557
|
+
`Wildcard "*" must be the last element in token "${token}"`
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
return new Workloop({ id: token, queues: names, capacity: count });
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
// src/util/get-default-workloop-config.ts
|
|
1564
|
+
var get_default_workloop_config_default = (capacity = 5) => `manual>*:${capacity}`;
|
|
1565
|
+
|
|
1463
1566
|
// src/server.ts
|
|
1464
1567
|
var exec = promisify(_exec);
|
|
1465
1568
|
var DEFAULT_PORT = 2222;
|
|
@@ -1487,8 +1590,10 @@ function connect(app, logger2, options = {}) {
|
|
|
1487
1590
|
app.resumeWorkloop();
|
|
1488
1591
|
};
|
|
1489
1592
|
const onDisconnect = () => {
|
|
1490
|
-
|
|
1491
|
-
|
|
1593
|
+
for (const w of app.workloops) {
|
|
1594
|
+
if (!w.isStopped()) {
|
|
1595
|
+
w.stop("Socket disconnected unexpectedly");
|
|
1596
|
+
}
|
|
1492
1597
|
}
|
|
1493
1598
|
if (!app.destroyed) {
|
|
1494
1599
|
logger2.info("Connection to lightning lost");
|
|
@@ -1510,17 +1615,26 @@ function connect(app, logger2, options = {}) {
|
|
|
1510
1615
|
const onMessage = (event) => {
|
|
1511
1616
|
if (event === WORK_AVAILABLE) {
|
|
1512
1617
|
if (!app.destroyed) {
|
|
1513
|
-
|
|
1514
|
-
|
|
1618
|
+
for (const w of app.workloops) {
|
|
1619
|
+
if (w.hasCapacity()) {
|
|
1620
|
+
claim_default(app, w, logger2).catch(() => {
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1515
1624
|
}
|
|
1516
1625
|
}
|
|
1517
1626
|
};
|
|
1627
|
+
const queuesMap = {};
|
|
1628
|
+
for (const w of app.workloops) {
|
|
1629
|
+
queuesMap[w.queues.join(">")] = w.capacity;
|
|
1630
|
+
}
|
|
1518
1631
|
worker_queue_default(options.lightning, app.id, options.secret, logger2, {
|
|
1519
1632
|
// TODO: options.socketTimeoutSeconds wins because this is what USED to be used
|
|
1520
1633
|
// But it's deprecated and should be removed soon
|
|
1521
1634
|
messageTimeout: options.socketTimeoutSeconds ?? options.messageTimeoutSeconds,
|
|
1522
1635
|
claimTimeout: options.claimTimeoutSeconds,
|
|
1523
|
-
capacity: options.maxWorkflows
|
|
1636
|
+
capacity: options.maxWorkflows,
|
|
1637
|
+
queues: queuesMap
|
|
1524
1638
|
}).on("connect", onConnect).on("disconnect", onDisconnect).on("error", onError).on("message", onMessage);
|
|
1525
1639
|
}
|
|
1526
1640
|
async function setupCollections(options, logger2) {
|
|
@@ -1569,27 +1683,36 @@ function createServer(engine, options = {}) {
|
|
|
1569
1683
|
logger2.debug(str);
|
|
1570
1684
|
})
|
|
1571
1685
|
);
|
|
1572
|
-
app.openClaims = {};
|
|
1573
1686
|
app.workflows = {};
|
|
1574
1687
|
app.destroyed = false;
|
|
1688
|
+
app.workloops = parseWorkloops(
|
|
1689
|
+
options.workloopConfigs ?? get_default_workloop_config_default(options.maxWorkflows)
|
|
1690
|
+
);
|
|
1691
|
+
app.runWorkloopMap = {};
|
|
1575
1692
|
app.server = app.listen(port);
|
|
1576
1693
|
logger2.success(`Worker ${app.id} listening on ${port}`);
|
|
1577
1694
|
process.send?.("READY");
|
|
1578
1695
|
router.get("/livez", healthcheck_default);
|
|
1579
1696
|
router.get("/", healthcheck_default);
|
|
1580
1697
|
app.options = options;
|
|
1581
|
-
app.resumeWorkloop = () => {
|
|
1698
|
+
app.resumeWorkloop = (workloop) => {
|
|
1582
1699
|
if (options.noLoop || app.destroyed) {
|
|
1583
1700
|
return;
|
|
1584
1701
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1702
|
+
const targets = workloop ? [workloop] : app.workloops;
|
|
1703
|
+
for (const w of targets) {
|
|
1704
|
+
if (!w.hasCapacity()) {
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
if (!w.isStopped()) {
|
|
1708
|
+
w.stop("restarting");
|
|
1709
|
+
}
|
|
1710
|
+
logger2.info(`Starting workloop for ${w.id}`);
|
|
1711
|
+
w.start(
|
|
1588
1712
|
app,
|
|
1589
1713
|
logger2,
|
|
1590
1714
|
options.backoff?.min || MIN_BACKOFF,
|
|
1591
|
-
options.backoff?.max || MAX_BACKOFF
|
|
1592
|
-
options.maxWorkflows
|
|
1715
|
+
options.backoff?.max || MAX_BACKOFF
|
|
1593
1716
|
);
|
|
1594
1717
|
}
|
|
1595
1718
|
};
|
|
@@ -1634,8 +1757,16 @@ function createServer(engine, options = {}) {
|
|
|
1634
1757
|
);
|
|
1635
1758
|
delete app.workflows[id];
|
|
1636
1759
|
runChannel.leave();
|
|
1637
|
-
app.
|
|
1638
|
-
|
|
1760
|
+
const owningWorkloop = app.runWorkloopMap[id];
|
|
1761
|
+
if (owningWorkloop) {
|
|
1762
|
+
owningWorkloop.activeRuns.delete(id);
|
|
1763
|
+
delete app.runWorkloopMap[id];
|
|
1764
|
+
app.events.emit(INTERNAL_RUN_COMPLETE);
|
|
1765
|
+
app.resumeWorkloop(owningWorkloop);
|
|
1766
|
+
} else {
|
|
1767
|
+
app.events.emit(INTERNAL_RUN_COMPLETE);
|
|
1768
|
+
app.resumeWorkloop();
|
|
1769
|
+
}
|
|
1639
1770
|
};
|
|
1640
1771
|
const context = execute(
|
|
1641
1772
|
runChannel,
|
|
@@ -1649,7 +1780,14 @@ function createServer(engine, options = {}) {
|
|
|
1649
1780
|
app.workflows[id] = context;
|
|
1650
1781
|
} catch (e) {
|
|
1651
1782
|
delete app.workflows[id];
|
|
1652
|
-
app.
|
|
1783
|
+
const owningWorkloop = app.runWorkloopMap[id];
|
|
1784
|
+
if (owningWorkloop) {
|
|
1785
|
+
owningWorkloop.activeRuns.delete(id);
|
|
1786
|
+
delete app.runWorkloopMap[id];
|
|
1787
|
+
app.resumeWorkloop(owningWorkloop);
|
|
1788
|
+
} else {
|
|
1789
|
+
app.resumeWorkloop();
|
|
1790
|
+
}
|
|
1653
1791
|
logger2.error(`Unexpected error executing ${id}`);
|
|
1654
1792
|
logger2.error(e);
|
|
1655
1793
|
}
|
|
@@ -1659,9 +1797,13 @@ function createServer(engine, options = {}) {
|
|
|
1659
1797
|
};
|
|
1660
1798
|
router.post("/claim", async (ctx) => {
|
|
1661
1799
|
logger2.info("triggering claim from POST request");
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1800
|
+
const promises = app.workloops.map((w) => {
|
|
1801
|
+
if (w.hasCapacity()) {
|
|
1802
|
+
return claim_default(app, w, logger2);
|
|
1803
|
+
}
|
|
1804
|
+
return Promise.reject(new Error("Workloop at capacity"));
|
|
1805
|
+
});
|
|
1806
|
+
return Promise.any(promises).then(() => {
|
|
1665
1807
|
logger2.info("claim complete: 1 run claimed");
|
|
1666
1808
|
ctx.body = "complete";
|
|
1667
1809
|
ctx.status = 200;
|
|
@@ -1672,10 +1814,15 @@ function createServer(engine, options = {}) {
|
|
|
1672
1814
|
});
|
|
1673
1815
|
});
|
|
1674
1816
|
app.claim = () => {
|
|
1675
|
-
|
|
1676
|
-
|
|
1817
|
+
const promises = app.workloops.map((w) => {
|
|
1818
|
+
if (w.hasCapacity()) {
|
|
1819
|
+
return claim_default(app, w, logger2);
|
|
1820
|
+
}
|
|
1821
|
+
return Promise.reject(new Error("Workloop at capacity"));
|
|
1677
1822
|
});
|
|
1823
|
+
return Promise.any(promises);
|
|
1678
1824
|
};
|
|
1825
|
+
app.pendingClaims = () => app.workloops.reduce((sum, w) => sum + Object.keys(w.openClaims).length, 0);
|
|
1679
1826
|
app.destroy = () => destroy_default(app, logger2);
|
|
1680
1827
|
app.use(router.routes());
|
|
1681
1828
|
if (options.lightning) {
|
|
@@ -6605,6 +6752,7 @@ function parseArgs(argv) {
|
|
|
6605
6752
|
WORKER_STATE_PROPS_TO_REMOVE,
|
|
6606
6753
|
WORKER_TIMEOUT_RETRY_COUNT,
|
|
6607
6754
|
WORKER_TIMEOUT_RETRY_DELAY_MS,
|
|
6755
|
+
WORKER_WORKLOOPS,
|
|
6608
6756
|
WORKER_VALIDATION_RETRIES,
|
|
6609
6757
|
WORKER_VALIDATION_TIMEOUT_MS
|
|
6610
6758
|
} = process.env;
|
|
@@ -6666,8 +6814,12 @@ function parseArgs(argv) {
|
|
|
6666
6814
|
}).option("backoff", {
|
|
6667
6815
|
description: "Claim backoff rules: min/max (in seconds). Env: WORKER_BACKOFF"
|
|
6668
6816
|
}).option("capacity", {
|
|
6669
|
-
description: `
|
|
6817
|
+
description: `Sets the maximum concurrent workers - but only if workloops is not set. Default ${DEFAULT_WORKER_CAPACITY}. Env: WORKER_CAPACITY`,
|
|
6670
6818
|
type: "number"
|
|
6819
|
+
}).option("workloops", {
|
|
6820
|
+
description: 'Configure workloops with a priorised queue list and a max capacity. Syntax: "<queues>:<capacity> ...". Mutually exclusive with --capacity. Env: WORKER_WORKLOOPS',
|
|
6821
|
+
type: "string",
|
|
6822
|
+
example: "fast_lane:1 manual>*:4"
|
|
6671
6823
|
}).option("state-props-to-remove", {
|
|
6672
6824
|
description: "A list of properties to remove from the final state returned by a job. Env: WORKER_STATE_PROPS_TO_REMOVE",
|
|
6673
6825
|
type: "array"
|
|
@@ -6705,10 +6857,25 @@ function parseArgs(argv) {
|
|
|
6705
6857
|
}).option("timeout-retry-delay", {
|
|
6706
6858
|
description: "When a websocket event receives a timeout, this option sets how log to wait before retrying Default 30000. Env: WORKER_TIMEOUT_RETRY_DELAY_MS",
|
|
6707
6859
|
type: "number"
|
|
6708
|
-
})
|
|
6860
|
+
}).example(
|
|
6861
|
+
"start --queues *:5",
|
|
6862
|
+
"Default start configuration: a single workloop with capacity 5, claiming from all queues"
|
|
6863
|
+
).example(
|
|
6864
|
+
"start --queues manual>*:5",
|
|
6865
|
+
"A single workloop, capacity 5, which claims across two queues. Runs in the manual queue will be picked first, else any other queue will be picked."
|
|
6866
|
+
).example(
|
|
6867
|
+
"start --queues fast_lane:1 manual>*:4",
|
|
6868
|
+
"production start configuration with 1 fast lane workloop (capacity 1) and a second workloop with capacity 4"
|
|
6869
|
+
);
|
|
6709
6870
|
const args2 = parser2.parse();
|
|
6871
|
+
const resolvedWorkloops = setArg(args2.workloops, WORKER_WORKLOOPS);
|
|
6872
|
+
const capacityExplicit = args2.capacity !== void 0 || WORKER_CAPACITY !== void 0;
|
|
6873
|
+
if (resolvedWorkloops !== void 0 && capacityExplicit) {
|
|
6874
|
+
throw new Error("--workloops and --capacity are mutually exclusive");
|
|
6875
|
+
}
|
|
6710
6876
|
return {
|
|
6711
6877
|
...args2,
|
|
6878
|
+
workloops: resolvedWorkloops,
|
|
6712
6879
|
port: setArg(args2.port, WORKER_PORT, DEFAULT_PORT2),
|
|
6713
6880
|
lightning: setArg(
|
|
6714
6881
|
args2.lightning,
|
|
@@ -6797,8 +6964,16 @@ function parseArgs(argv) {
|
|
|
6797
6964
|
|
|
6798
6965
|
// src/start.ts
|
|
6799
6966
|
var args = parseArgs(process.argv);
|
|
6967
|
+
var workloopConfigs = args.workloops ?? get_default_workloop_config_default(args.capacity);
|
|
6968
|
+
var effectiveCapacity = workloopConfigs.trim().split(/\s+/).reduce((sum, token) => sum + (parseInt(token.split(":").pop()) || 0), 0);
|
|
6800
6969
|
var logger = createLogger("SRV", { level: args.log });
|
|
6801
|
-
logger.info("Starting worker
|
|
6970
|
+
logger.info("Starting worker...");
|
|
6971
|
+
logger.info(
|
|
6972
|
+
"Workloops:",
|
|
6973
|
+
workloopConfigs,
|
|
6974
|
+
"effective capacity:",
|
|
6975
|
+
effectiveCapacity
|
|
6976
|
+
);
|
|
6802
6977
|
if (args.lightning === "mock") {
|
|
6803
6978
|
args.lightning = "ws://localhost:8888/worker";
|
|
6804
6979
|
if (!args.secret) {
|
|
@@ -6823,7 +6998,8 @@ function engineReady(engine) {
|
|
|
6823
6998
|
min: minBackoff,
|
|
6824
6999
|
max: maxBackoff
|
|
6825
7000
|
},
|
|
6826
|
-
maxWorkflows:
|
|
7001
|
+
maxWorkflows: effectiveCapacity,
|
|
7002
|
+
workloopConfigs,
|
|
6827
7003
|
payloadLimitMb: args.payloadMemory,
|
|
6828
7004
|
logPayloadLimitMb: args.logPayloadMemory ?? 1,
|
|
6829
7005
|
// Default to 1MB
|
|
@@ -6873,7 +7049,7 @@ if (args.mock) {
|
|
|
6873
7049
|
const engineOptions = {
|
|
6874
7050
|
repoDir: args.repoDir,
|
|
6875
7051
|
memoryLimitMb: args.runMemory,
|
|
6876
|
-
maxWorkers:
|
|
7052
|
+
maxWorkers: effectiveCapacity,
|
|
6877
7053
|
statePropsToRemove: args.statePropsToRemove,
|
|
6878
7054
|
runTimeoutMs: args.maxRunDurationSeconds * 1e3,
|
|
6879
7055
|
workerValidationTimeout: args.engineValidationTimeoutMs,
|