@openfn/ws-worker 1.21.4 → 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 +22 -0
- package/README.md +79 -0
- package/dist/index.d.ts +27 -9
- package/dist/index.js +279 -125
- package/dist/start.js +312 -130
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ var GET_CREDENTIAL = "fetch:credential";
|
|
|
19
19
|
var RUN_START = "run:start";
|
|
20
20
|
var RUN_COMPLETE = "run:complete";
|
|
21
21
|
var RUN_LOG = "run:log";
|
|
22
|
+
var RUN_LOG_BATCH = "run:batch_logs";
|
|
22
23
|
var STEP_START = "step:start";
|
|
23
24
|
var STEP_COMPLETE = "step:complete";
|
|
24
25
|
var INTERNAL_RUN_COMPLETE = "server:run-complete";
|
|
@@ -32,7 +33,9 @@ var destroy = async (app, logger) => {
|
|
|
32
33
|
await Promise.all([
|
|
33
34
|
new Promise((resolve) => {
|
|
34
35
|
app.destroyed = true;
|
|
35
|
-
app.
|
|
36
|
+
for (const w of app.workloops) {
|
|
37
|
+
w.stop("server closed");
|
|
38
|
+
}
|
|
36
39
|
app.server.close(async () => {
|
|
37
40
|
resolve();
|
|
38
41
|
});
|
|
@@ -50,11 +53,11 @@ var destroy = async (app, logger) => {
|
|
|
50
53
|
var waitForRunsAndClaims = (app, logger) => new Promise((resolve) => {
|
|
51
54
|
const log = () => {
|
|
52
55
|
logger.debug(
|
|
53
|
-
`Waiting for ${Object.keys(app.workflows).length} runs and ${
|
|
56
|
+
`Waiting for ${Object.keys(app.workflows).length} runs and ${app.pendingClaims()} claims to complete...`
|
|
54
57
|
);
|
|
55
58
|
};
|
|
56
59
|
const checkAllClear = () => {
|
|
57
|
-
if (Object.keys(app.workflows).length +
|
|
60
|
+
if (Object.keys(app.workflows).length + app.pendingClaims() === 0) {
|
|
58
61
|
logger.debug("All runs completed!");
|
|
59
62
|
app.events.off(INTERNAL_RUN_COMPLETE, checkAllClear);
|
|
60
63
|
app.events.off(INTERNAL_CLAIM_COMPLETE, checkAllClear);
|
|
@@ -63,7 +66,7 @@ var waitForRunsAndClaims = (app, logger) => new Promise((resolve) => {
|
|
|
63
66
|
log();
|
|
64
67
|
}
|
|
65
68
|
};
|
|
66
|
-
if (Object.keys(app.workflows).length ||
|
|
69
|
+
if (Object.keys(app.workflows).length || app.pendingClaims()) {
|
|
67
70
|
log();
|
|
68
71
|
app.events.on(INTERNAL_RUN_COMPLETE, checkAllClear);
|
|
69
72
|
app.events.on(INTERNAL_CLAIM_COMPLETE, checkAllClear);
|
|
@@ -74,51 +77,6 @@ var waitForRunsAndClaims = (app, logger) => new Promise((resolve) => {
|
|
|
74
77
|
});
|
|
75
78
|
var destroy_default = destroy;
|
|
76
79
|
|
|
77
|
-
// src/util/try-with-backoff.ts
|
|
78
|
-
var BACKOFF_MULTIPLIER = 1.15;
|
|
79
|
-
var tryWithBackoff = (fn, opts = {}) => {
|
|
80
|
-
const { min = 1e3, max = 1e4, maxRuns, runs = 1 } = opts;
|
|
81
|
-
let cancelled = false;
|
|
82
|
-
if (!opts.isCancelled) {
|
|
83
|
-
opts.isCancelled = () => cancelled;
|
|
84
|
-
}
|
|
85
|
-
const promise = new Promise(async (resolve, reject) => {
|
|
86
|
-
try {
|
|
87
|
-
await fn();
|
|
88
|
-
resolve();
|
|
89
|
-
} catch (e) {
|
|
90
|
-
if (e?.abort) {
|
|
91
|
-
cancelled = true;
|
|
92
|
-
return reject();
|
|
93
|
-
}
|
|
94
|
-
if (opts.isCancelled()) {
|
|
95
|
-
return resolve();
|
|
96
|
-
}
|
|
97
|
-
if (!isNaN(maxRuns) && runs >= maxRuns) {
|
|
98
|
-
return reject(new Error("max runs exceeded"));
|
|
99
|
-
}
|
|
100
|
-
setTimeout(() => {
|
|
101
|
-
if (opts.isCancelled()) {
|
|
102
|
-
return resolve();
|
|
103
|
-
}
|
|
104
|
-
const nextOpts = {
|
|
105
|
-
maxRuns,
|
|
106
|
-
runs: runs + 1,
|
|
107
|
-
min: Math.min(max, min * BACKOFF_MULTIPLIER),
|
|
108
|
-
max,
|
|
109
|
-
isCancelled: opts.isCancelled
|
|
110
|
-
};
|
|
111
|
-
tryWithBackoff(fn, nextOpts).then(resolve).catch(reject);
|
|
112
|
-
}, min);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
promise.cancel = () => {
|
|
116
|
-
cancelled = true;
|
|
117
|
-
};
|
|
118
|
-
return promise;
|
|
119
|
-
};
|
|
120
|
-
var try_with_backoff_default = tryWithBackoff;
|
|
121
|
-
|
|
122
80
|
// src/api/claim.ts
|
|
123
81
|
import v8 from "node:v8";
|
|
124
82
|
import * as Sentry from "@sentry/node";
|
|
@@ -147,24 +105,26 @@ var ClaimError = class extends Error {
|
|
|
147
105
|
}
|
|
148
106
|
};
|
|
149
107
|
var claimIdGen = 0;
|
|
150
|
-
var claim = (app, logger = mockLogger, options = {}) => {
|
|
108
|
+
var claim = (app, workloop, logger = mockLogger, options = {}) => {
|
|
151
109
|
return new Promise((resolve, reject) => {
|
|
152
|
-
|
|
153
|
-
const { maxWorkers = 5, demand = 1 } = options;
|
|
110
|
+
const { demand = 1 } = options;
|
|
154
111
|
const podName = NAME ? `[${NAME}] ` : "";
|
|
155
|
-
const
|
|
156
|
-
const
|
|
112
|
+
const activeInWorkloop = workloop.activeRuns.size;
|
|
113
|
+
const capacity = workloop.capacity;
|
|
114
|
+
const pendingWorkloopClaims = Object.values(workloop.openClaims).reduce(
|
|
157
115
|
(a, b) => a + b,
|
|
158
116
|
0
|
|
159
117
|
);
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
} else if (activeWorkers + pendingClaims >= maxWorkers) {
|
|
164
|
-
app.workloop?.stop(
|
|
165
|
-
`server at capacity (${activeWorkers}/${maxWorkers}, ${pendingClaims} pending)`
|
|
118
|
+
if (activeInWorkloop >= capacity) {
|
|
119
|
+
workloop.stop(
|
|
120
|
+
`workloop ${workloop.id} at capacity (${activeInWorkloop}/${capacity})`
|
|
166
121
|
);
|
|
167
|
-
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"));
|
|
168
128
|
}
|
|
169
129
|
if (!app.queueChannel) {
|
|
170
130
|
logger.warn("skipping claim attempt: websocket unavailable");
|
|
@@ -177,21 +137,22 @@ var claim = (app, logger = mockLogger, options = {}) => {
|
|
|
177
137
|
return reject(e);
|
|
178
138
|
}
|
|
179
139
|
const claimId = ++claimIdGen;
|
|
180
|
-
|
|
140
|
+
workloop.openClaims[claimId] = demand;
|
|
181
141
|
const { used_heap_size, heap_size_limit } = v8.getHeapStatistics();
|
|
182
142
|
const usedHeapMb = Math.round(used_heap_size / 1024 / 1024);
|
|
183
143
|
const totalHeapMb = Math.round(heap_size_limit / 1024 / 1024);
|
|
184
144
|
const memPercent = Math.round(usedHeapMb / totalHeapMb * 100);
|
|
185
145
|
logger.debug(
|
|
186
|
-
`Claiming runs :: demand ${demand} | capacity ${
|
|
146
|
+
`Claiming runs [${workloop.id}] :: demand ${demand} | capacity ${activeInWorkloop}/${capacity} | memory ${memPercent}% (${usedHeapMb}/${totalHeapMb}mb)`
|
|
187
147
|
);
|
|
188
148
|
app.events.emit(INTERNAL_CLAIM_START);
|
|
189
149
|
const start = Date.now();
|
|
190
150
|
app.queueChannel.push(CLAIM, {
|
|
191
151
|
demand,
|
|
192
|
-
worker_name: NAME || null
|
|
152
|
+
worker_name: NAME || null,
|
|
153
|
+
queues: workloop.queues
|
|
193
154
|
}).receive("ok", async ({ runs }) => {
|
|
194
|
-
delete
|
|
155
|
+
delete workloop.openClaims[claimId];
|
|
195
156
|
const duration = Date.now() - start;
|
|
196
157
|
logger.debug(
|
|
197
158
|
`${podName}claimed ${runs.length} runs in ${duration}ms (${runs.length ? runs.map((r) => r.id).join(",") : "-"})`
|
|
@@ -215,17 +176,19 @@ var claim = (app, logger = mockLogger, options = {}) => {
|
|
|
215
176
|
} else {
|
|
216
177
|
logger.debug("skipping run token validation for", run.id);
|
|
217
178
|
}
|
|
179
|
+
workloop.activeRuns.add(run.id);
|
|
180
|
+
app.runWorkloopMap[run.id] = workloop;
|
|
218
181
|
logger.debug(`${podName} starting run ${run.id}`);
|
|
219
182
|
app.execute(run);
|
|
220
183
|
}
|
|
221
184
|
resolve();
|
|
222
185
|
app.events.emit(INTERNAL_CLAIM_COMPLETE, { runs });
|
|
223
186
|
}).receive("error", (e) => {
|
|
224
|
-
delete
|
|
187
|
+
delete workloop.openClaims[claimId];
|
|
225
188
|
logger.error("Error on claim", e);
|
|
226
189
|
reject(new Error("claim error"));
|
|
227
190
|
}).receive("timeout", () => {
|
|
228
|
-
delete
|
|
191
|
+
delete workloop.openClaims[claimId];
|
|
229
192
|
logger.error("TIMEOUT on claim. Runs may be lost.");
|
|
230
193
|
reject(new Error("timeout"));
|
|
231
194
|
});
|
|
@@ -233,43 +196,6 @@ var claim = (app, logger = mockLogger, options = {}) => {
|
|
|
233
196
|
};
|
|
234
197
|
var claim_default = claim;
|
|
235
198
|
|
|
236
|
-
// src/api/workloop.ts
|
|
237
|
-
var startWorkloop = (app, logger, minBackoff, maxBackoff, maxWorkers) => {
|
|
238
|
-
let promise;
|
|
239
|
-
let cancelled = false;
|
|
240
|
-
const workLoop = () => {
|
|
241
|
-
if (!cancelled) {
|
|
242
|
-
promise = try_with_backoff_default(
|
|
243
|
-
() => claim_default(app, logger, {
|
|
244
|
-
maxWorkers
|
|
245
|
-
}),
|
|
246
|
-
{
|
|
247
|
-
min: minBackoff,
|
|
248
|
-
max: maxBackoff
|
|
249
|
-
}
|
|
250
|
-
);
|
|
251
|
-
promise.then(() => {
|
|
252
|
-
if (!cancelled) {
|
|
253
|
-
setTimeout(workLoop, minBackoff);
|
|
254
|
-
}
|
|
255
|
-
}).catch(() => {
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
workLoop();
|
|
260
|
-
return {
|
|
261
|
-
stop: (reason = "reason unknown") => {
|
|
262
|
-
if (!cancelled) {
|
|
263
|
-
logger.info(`cancelling workloop: ${reason}`);
|
|
264
|
-
cancelled = true;
|
|
265
|
-
promise.cancel();
|
|
266
|
-
}
|
|
267
|
-
},
|
|
268
|
-
isStopped: () => cancelled
|
|
269
|
-
};
|
|
270
|
-
};
|
|
271
|
-
var workloop_default = startWorkloop;
|
|
272
|
-
|
|
273
199
|
// src/api/execute.ts
|
|
274
200
|
import * as Sentry4 from "@sentry/node";
|
|
275
201
|
import {
|
|
@@ -574,6 +500,51 @@ var stringify_default = (obj) => stringify(obj, (_key, value) => {
|
|
|
574
500
|
return value;
|
|
575
501
|
});
|
|
576
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
|
+
|
|
577
548
|
// src/util/timestamp.ts
|
|
578
549
|
var timeInMicroseconds = (time) => time && (BigInt(time) / BigInt(1e3)).toString();
|
|
579
550
|
|
|
@@ -612,7 +583,7 @@ async function onRunLog(context, events) {
|
|
|
612
583
|
run_id: `${state.plan.id}`,
|
|
613
584
|
logs
|
|
614
585
|
};
|
|
615
|
-
return sendEvent(context,
|
|
586
|
+
return sendEvent(context, RUN_LOG_BATCH, payload);
|
|
616
587
|
} else {
|
|
617
588
|
return new Promise(async (resolve) => {
|
|
618
589
|
for (const log of logs) {
|
|
@@ -1234,6 +1205,7 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger, options) => {
|
|
|
1234
1205
|
messageTimeout = DEFAULT_MESSAGE_TIMEOUT_SECONDS,
|
|
1235
1206
|
claimTimeout = DEFAULT_CLAIM_TIMEOUT_SECONDS,
|
|
1236
1207
|
capacity,
|
|
1208
|
+
queues,
|
|
1237
1209
|
SocketConstructor = PhxSocket
|
|
1238
1210
|
} = options;
|
|
1239
1211
|
const events = new EventEmitter();
|
|
@@ -1270,6 +1242,9 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger, options) => {
|
|
|
1270
1242
|
didOpen = true;
|
|
1271
1243
|
shouldReportConnectionError = true;
|
|
1272
1244
|
const joinPayload = { capacity };
|
|
1245
|
+
if (queues) {
|
|
1246
|
+
joinPayload.queues = queues;
|
|
1247
|
+
}
|
|
1273
1248
|
const channel = socket.channel("worker:queue", joinPayload);
|
|
1274
1249
|
channel.onMessage = (ev, load) => {
|
|
1275
1250
|
events.emit("message", ev, load);
|
|
@@ -1310,6 +1285,140 @@ var connectToWorkerQueue = (endpoint, serverId, secret, logger, options) => {
|
|
|
1310
1285
|
};
|
|
1311
1286
|
var worker_queue_default = connectToWorkerQueue;
|
|
1312
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
|
+
|
|
1313
1422
|
// src/server.ts
|
|
1314
1423
|
var exec = promisify(_exec);
|
|
1315
1424
|
var DEFAULT_PORT = 2222;
|
|
@@ -1337,8 +1446,10 @@ function connect(app, logger, options = {}) {
|
|
|
1337
1446
|
app.resumeWorkloop();
|
|
1338
1447
|
};
|
|
1339
1448
|
const onDisconnect = () => {
|
|
1340
|
-
|
|
1341
|
-
|
|
1449
|
+
for (const w of app.workloops) {
|
|
1450
|
+
if (!w.isStopped()) {
|
|
1451
|
+
w.stop("Socket disconnected unexpectedly");
|
|
1452
|
+
}
|
|
1342
1453
|
}
|
|
1343
1454
|
if (!app.destroyed) {
|
|
1344
1455
|
logger.info("Connection to lightning lost");
|
|
@@ -1360,17 +1471,26 @@ function connect(app, logger, options = {}) {
|
|
|
1360
1471
|
const onMessage = (event) => {
|
|
1361
1472
|
if (event === WORK_AVAILABLE) {
|
|
1362
1473
|
if (!app.destroyed) {
|
|
1363
|
-
|
|
1364
|
-
|
|
1474
|
+
for (const w of app.workloops) {
|
|
1475
|
+
if (w.hasCapacity()) {
|
|
1476
|
+
claim_default(app, w, logger).catch(() => {
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1365
1480
|
}
|
|
1366
1481
|
}
|
|
1367
1482
|
};
|
|
1483
|
+
const queuesMap = {};
|
|
1484
|
+
for (const w of app.workloops) {
|
|
1485
|
+
queuesMap[w.queues.join(">")] = w.capacity;
|
|
1486
|
+
}
|
|
1368
1487
|
worker_queue_default(options.lightning, app.id, options.secret, logger, {
|
|
1369
1488
|
// TODO: options.socketTimeoutSeconds wins because this is what USED to be used
|
|
1370
1489
|
// But it's deprecated and should be removed soon
|
|
1371
1490
|
messageTimeout: options.socketTimeoutSeconds ?? options.messageTimeoutSeconds,
|
|
1372
1491
|
claimTimeout: options.claimTimeoutSeconds,
|
|
1373
|
-
capacity: options.maxWorkflows
|
|
1492
|
+
capacity: options.maxWorkflows,
|
|
1493
|
+
queues: queuesMap
|
|
1374
1494
|
}).on("connect", onConnect).on("disconnect", onDisconnect).on("error", onError).on("message", onMessage);
|
|
1375
1495
|
}
|
|
1376
1496
|
async function setupCollections(options, logger) {
|
|
@@ -1419,27 +1539,36 @@ function createServer(engine, options = {}) {
|
|
|
1419
1539
|
logger.debug(str);
|
|
1420
1540
|
})
|
|
1421
1541
|
);
|
|
1422
|
-
app.openClaims = {};
|
|
1423
1542
|
app.workflows = {};
|
|
1424
1543
|
app.destroyed = false;
|
|
1544
|
+
app.workloops = parseWorkloops(
|
|
1545
|
+
options.workloopConfigs ?? get_default_workloop_config_default(options.maxWorkflows)
|
|
1546
|
+
);
|
|
1547
|
+
app.runWorkloopMap = {};
|
|
1425
1548
|
app.server = app.listen(port);
|
|
1426
1549
|
logger.success(`Worker ${app.id} listening on ${port}`);
|
|
1427
1550
|
process.send?.("READY");
|
|
1428
1551
|
router.get("/livez", healthcheck_default);
|
|
1429
1552
|
router.get("/", healthcheck_default);
|
|
1430
1553
|
app.options = options;
|
|
1431
|
-
app.resumeWorkloop = () => {
|
|
1554
|
+
app.resumeWorkloop = (workloop) => {
|
|
1432
1555
|
if (options.noLoop || app.destroyed) {
|
|
1433
1556
|
return;
|
|
1434
1557
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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(
|
|
1438
1568
|
app,
|
|
1439
1569
|
logger,
|
|
1440
1570
|
options.backoff?.min || MIN_BACKOFF,
|
|
1441
|
-
options.backoff?.max || MAX_BACKOFF
|
|
1442
|
-
options.maxWorkflows
|
|
1571
|
+
options.backoff?.max || MAX_BACKOFF
|
|
1443
1572
|
);
|
|
1444
1573
|
}
|
|
1445
1574
|
};
|
|
@@ -1484,8 +1613,16 @@ function createServer(engine, options = {}) {
|
|
|
1484
1613
|
);
|
|
1485
1614
|
delete app.workflows[id];
|
|
1486
1615
|
runChannel.leave();
|
|
1487
|
-
app.
|
|
1488
|
-
|
|
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
|
+
}
|
|
1489
1626
|
};
|
|
1490
1627
|
const context = execute(
|
|
1491
1628
|
runChannel,
|
|
@@ -1499,7 +1636,14 @@ function createServer(engine, options = {}) {
|
|
|
1499
1636
|
app.workflows[id] = context;
|
|
1500
1637
|
} catch (e) {
|
|
1501
1638
|
delete app.workflows[id];
|
|
1502
|
-
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
|
+
}
|
|
1503
1647
|
logger.error(`Unexpected error executing ${id}`);
|
|
1504
1648
|
logger.error(e);
|
|
1505
1649
|
}
|
|
@@ -1509,9 +1653,13 @@ function createServer(engine, options = {}) {
|
|
|
1509
1653
|
};
|
|
1510
1654
|
router.post("/claim", async (ctx) => {
|
|
1511
1655
|
logger.info("triggering claim from POST request");
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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(() => {
|
|
1515
1663
|
logger.info("claim complete: 1 run claimed");
|
|
1516
1664
|
ctx.body = "complete";
|
|
1517
1665
|
ctx.status = 200;
|
|
@@ -1522,10 +1670,15 @@ function createServer(engine, options = {}) {
|
|
|
1522
1670
|
});
|
|
1523
1671
|
});
|
|
1524
1672
|
app.claim = () => {
|
|
1525
|
-
|
|
1526
|
-
|
|
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"));
|
|
1527
1678
|
});
|
|
1679
|
+
return Promise.any(promises);
|
|
1528
1680
|
};
|
|
1681
|
+
app.pendingClaims = () => app.workloops.reduce((sum, w) => sum + Object.keys(w.openClaims).length, 0);
|
|
1529
1682
|
app.destroy = () => destroy_default(app, logger);
|
|
1530
1683
|
app.use(router.routes());
|
|
1531
1684
|
if (options.lightning) {
|
|
@@ -1567,6 +1720,7 @@ export {
|
|
|
1567
1720
|
INTERNAL_SOCKET_READY,
|
|
1568
1721
|
RUN_COMPLETE,
|
|
1569
1722
|
RUN_LOG,
|
|
1723
|
+
RUN_LOG_BATCH,
|
|
1570
1724
|
RUN_START,
|
|
1571
1725
|
STEP_COMPLETE,
|
|
1572
1726
|
STEP_START,
|