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