@decaf-ts/core 0.8.50 → 0.8.52
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/README.md +206 -93
- package/dist/core.cjs +1 -1
- package/dist/core.cjs.map +1 -1
- package/dist/core.js +1 -1
- package/dist/core.js.map +1 -1
- package/lib/esm/fs/FilesystemAdapter.d.ts +67 -0
- package/lib/esm/fs/FilesystemAdapter.js +430 -0
- package/lib/esm/fs/FilesystemAdapter.js.map +1 -0
- package/lib/esm/fs/FsDispatch.d.ts +7 -0
- package/lib/esm/fs/FsDispatch.js +20 -0
- package/lib/esm/fs/FsDispatch.js.map +1 -0
- package/lib/esm/fs/helpers.d.ts +17 -0
- package/lib/esm/fs/helpers.js +64 -0
- package/lib/esm/fs/helpers.js.map +1 -0
- package/lib/esm/fs/index.d.ts +4 -0
- package/lib/esm/fs/index.js +5 -0
- package/lib/esm/fs/index.js.map +1 -0
- package/lib/esm/fs/indexStore.d.ts +28 -0
- package/lib/esm/fs/indexStore.js +173 -0
- package/lib/esm/fs/indexStore.js.map +1 -0
- package/lib/esm/fs/locks/FilesystemLock.d.ts +13 -0
- package/lib/esm/fs/locks/FilesystemLock.js +49 -0
- package/lib/esm/fs/locks/FilesystemLock.js.map +1 -0
- package/lib/esm/fs/locks/FilesystemMultiLock.d.ts +8 -0
- package/lib/esm/fs/locks/FilesystemMultiLock.js +23 -0
- package/lib/esm/fs/locks/FilesystemMultiLock.js.map +1 -0
- package/lib/esm/fs/types.d.ts +34 -0
- package/lib/esm/fs/types.js +2 -0
- package/lib/esm/fs/types.js.map +1 -0
- package/lib/esm/index.d.ts +1 -1
- package/lib/esm/index.js +1 -1
- package/lib/esm/query/Paginator.js +2 -2
- package/lib/esm/query/Paginator.js.map +1 -1
- package/lib/esm/ram/RamAdapter.d.ts +1 -1
- package/lib/esm/ram/types.d.ts +2 -1
- package/lib/esm/tasks/TaskEngine.d.ts +30 -1
- package/lib/esm/tasks/TaskEngine.js +361 -16
- package/lib/esm/tasks/TaskEngine.js.map +1 -1
- package/lib/esm/tasks/TaskService.js +3 -0
- package/lib/esm/tasks/TaskService.js.map +1 -1
- package/lib/esm/tasks/builder.js +1 -1
- package/lib/esm/tasks/builder.js.map +1 -1
- package/lib/esm/tasks/constants.js +1 -0
- package/lib/esm/tasks/constants.js.map +1 -1
- package/lib/esm/tasks/types.d.ts +12 -0
- package/lib/esm/tasks/workers/WorkThreadEnvironment.d.ts +32 -0
- package/lib/esm/tasks/workers/WorkThreadEnvironment.js +28 -0
- package/lib/esm/tasks/workers/WorkThreadEnvironment.js.map +1 -0
- package/lib/esm/tasks/workers/messages.d.ts +69 -0
- package/lib/esm/tasks/workers/messages.js +2 -0
- package/lib/esm/tasks/workers/messages.js.map +1 -0
- package/lib/esm/tasks/workers/workerThread.d.ts +1 -0
- package/lib/esm/tasks/workers/workerThread.js +185 -0
- package/lib/esm/tasks/workers/workerThread.js.map +1 -0
- package/lib/fs/FilesystemAdapter.cjs +437 -0
- package/lib/fs/FilesystemAdapter.d.ts +67 -0
- package/lib/fs/FilesystemAdapter.js.map +1 -0
- package/lib/fs/FsDispatch.cjs +24 -0
- package/lib/fs/FsDispatch.d.ts +7 -0
- package/lib/fs/FsDispatch.js.map +1 -0
- package/lib/fs/helpers.cjs +76 -0
- package/lib/fs/helpers.d.ts +17 -0
- package/lib/fs/helpers.js.map +1 -0
- package/lib/fs/index.cjs +21 -0
- package/lib/fs/index.d.ts +4 -0
- package/lib/fs/index.js.map +1 -0
- package/lib/fs/indexStore.cjs +181 -0
- package/lib/fs/indexStore.d.ts +28 -0
- package/lib/fs/indexStore.js.map +1 -0
- package/lib/fs/locks/FilesystemLock.cjs +56 -0
- package/lib/fs/locks/FilesystemLock.d.ts +13 -0
- package/lib/fs/locks/FilesystemLock.js.map +1 -0
- package/lib/fs/locks/FilesystemMultiLock.cjs +30 -0
- package/lib/fs/locks/FilesystemMultiLock.d.ts +8 -0
- package/lib/fs/locks/FilesystemMultiLock.js.map +1 -0
- package/lib/fs/types.cjs +3 -0
- package/lib/fs/types.d.ts +34 -0
- package/lib/fs/types.js.map +1 -0
- package/lib/index.cjs +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/query/Paginator.cjs +2 -2
- package/lib/query/Paginator.js.map +1 -1
- package/lib/ram/RamAdapter.d.ts +1 -1
- package/lib/ram/types.d.ts +2 -1
- package/lib/tasks/TaskEngine.cjs +360 -15
- package/lib/tasks/TaskEngine.d.ts +30 -1
- package/lib/tasks/TaskEngine.js.map +1 -1
- package/lib/tasks/TaskService.cjs +3 -0
- package/lib/tasks/TaskService.js.map +1 -1
- package/lib/tasks/builder.cjs +1 -1
- package/lib/tasks/builder.js.map +1 -1
- package/lib/tasks/constants.cjs +1 -0
- package/lib/tasks/constants.js.map +1 -1
- package/lib/tasks/types.d.ts +12 -0
- package/lib/tasks/workers/WorkThreadEnvironment.cjs +31 -0
- package/lib/tasks/workers/WorkThreadEnvironment.d.ts +32 -0
- package/lib/tasks/workers/WorkThreadEnvironment.js.map +1 -0
- package/lib/tasks/workers/messages.cjs +3 -0
- package/lib/tasks/workers/messages.d.ts +69 -0
- package/lib/tasks/workers/messages.js.map +1 -0
- package/lib/tasks/workers/workerThread.cjs +220 -0
- package/lib/tasks/workers/workerThread.d.ts +1 -0
- package/lib/tasks/workers/workerThread.js.map +1 -0
- package/package.json +19 -8
package/lib/tasks/TaskEngine.cjs
CHANGED
|
@@ -11,6 +11,7 @@ const TaskStepResultModel_1 = require("./models/TaskStepResultModel.cjs");
|
|
|
11
11
|
const TaskLogEntryModel_1 = require("./models/TaskLogEntryModel.cjs");
|
|
12
12
|
const TaskBackoffModel_1 = require("./models/TaskBackoffModel.cjs");
|
|
13
13
|
const TaskStepSpecModel_1 = require("./models/TaskStepSpecModel.cjs");
|
|
14
|
+
const node_worker_threads_1 = require("node:worker_threads");
|
|
14
15
|
const constants_1 = require("./constants.cjs");
|
|
15
16
|
const logging_1 = require("@decaf-ts/logging");
|
|
16
17
|
const ContextualLoggedClass_1 = require("./../utils/ContextualLoggedClass.cjs");
|
|
@@ -23,6 +24,7 @@ const TaskErrorModel_1 = require("./models/TaskErrorModel.cjs");
|
|
|
23
24
|
const TaskTracker_1 = require("./TaskTracker.cjs");
|
|
24
25
|
const transactional_decorators_1 = require("@decaf-ts/transactional-decorators");
|
|
25
26
|
const index_1 = require("./../persistence/index.cjs");
|
|
27
|
+
const WorkThreadEnvironment_1 = require("./workers/WorkThreadEnvironment.cjs");
|
|
26
28
|
class TaskEngine extends ContextualLoggedClass_1.AbsContextual {
|
|
27
29
|
get Context() {
|
|
28
30
|
return TaskContext_1.TaskContext;
|
|
@@ -52,15 +54,87 @@ class TaskEngine extends ContextualLoggedClass_1.AbsContextual {
|
|
|
52
54
|
this._events = this._events.override(this.config.overrides);
|
|
53
55
|
return this._events;
|
|
54
56
|
}
|
|
57
|
+
static createTaskContext(base, overrides) {
|
|
58
|
+
const ctx = new TaskContext_1.TaskContext(base);
|
|
59
|
+
if (overrides && Object.keys(overrides).length) {
|
|
60
|
+
return ctx.accumulate(overrides);
|
|
61
|
+
}
|
|
62
|
+
return ctx;
|
|
63
|
+
}
|
|
55
64
|
constructor(config) {
|
|
56
65
|
super();
|
|
57
66
|
this.config = config;
|
|
67
|
+
this.workerThreads = [];
|
|
68
|
+
this.workerJobQueue = [];
|
|
69
|
+
this.workerJobs = new Map();
|
|
70
|
+
this.workerCounter = 0;
|
|
71
|
+
this.workerThreadCapacity = 1;
|
|
58
72
|
this.lock = new transactional_decorators_1.Lock();
|
|
59
73
|
this.running = false;
|
|
74
|
+
if (config.workerPool && !config.workerAdapter) {
|
|
75
|
+
throw new db_decorators_1.InternalError("Worker pool requires workerAdapter descriptor in TaskEngineConfig");
|
|
76
|
+
}
|
|
60
77
|
this.config = Object.assign({}, constants_1.DefaultTaskEngineConfig, config, {
|
|
61
78
|
bus: config.bus || new TaskEventBus_1.TaskEventBus(),
|
|
62
79
|
registry: config.registry || new TaskHandlerRegistry_1.TaskHandlerRegistry(),
|
|
63
80
|
});
|
|
81
|
+
this.workerThreadCapacity = Math.max(1, this.config.workerConcurrency ?? 1);
|
|
82
|
+
this.workerPoolConfig = this.normalizeWorkerPoolConfig(this.config.workerPool);
|
|
83
|
+
}
|
|
84
|
+
normalizeWorkerPoolConfig(pool) {
|
|
85
|
+
if (!pool)
|
|
86
|
+
return undefined;
|
|
87
|
+
if (!pool.entry) {
|
|
88
|
+
throw new db_decorators_1.InternalError("Worker pool configuration requires an explicit entry file path");
|
|
89
|
+
}
|
|
90
|
+
if (pool.size != null && pool.size !== this.config.concurrency) {
|
|
91
|
+
throw new db_decorators_1.InternalError("TaskEngine concurrency must match workerPool.size when worker pool is enabled");
|
|
92
|
+
}
|
|
93
|
+
return Object.assign({}, pool, {
|
|
94
|
+
size: this.config.concurrency,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
hasWorkerPool() {
|
|
98
|
+
return !!this.workerPoolConfig && (this.workerPoolConfig.size ?? 0) > 0;
|
|
99
|
+
}
|
|
100
|
+
getWorkerCount() {
|
|
101
|
+
if (!this.workerPoolConfig)
|
|
102
|
+
return 0;
|
|
103
|
+
return this.workerPoolConfig.size ?? 0;
|
|
104
|
+
}
|
|
105
|
+
getWorkerExecutionSlots() {
|
|
106
|
+
return this.getWorkerCount() * this.workerThreadCapacity;
|
|
107
|
+
}
|
|
108
|
+
canDispatchToWorkers() {
|
|
109
|
+
return this.hasWorkerPool();
|
|
110
|
+
}
|
|
111
|
+
getExecutionConcurrency() {
|
|
112
|
+
if (this.hasWorkerPool()) {
|
|
113
|
+
return Math.max(1, this.getWorkerExecutionSlots());
|
|
114
|
+
}
|
|
115
|
+
return this.config.concurrency;
|
|
116
|
+
}
|
|
117
|
+
computeWorkerModules() {
|
|
118
|
+
const adapterDescriptor = this.config.workerAdapter ?? WorkThreadEnvironment_1.DefaultWorkThreadEnvironment.persistence;
|
|
119
|
+
if (!adapterDescriptor?.adapterModule) {
|
|
120
|
+
throw new db_decorators_1.InternalError("Worker adapter descriptor must include adapterModule");
|
|
121
|
+
}
|
|
122
|
+
const configuredImports = this.workerPoolConfig?.modules?.imports ??
|
|
123
|
+
WorkThreadEnvironment_1.DefaultWorkThreadEnvironment.modules.imports;
|
|
124
|
+
const imports = [];
|
|
125
|
+
const append = (specifier) => {
|
|
126
|
+
if (!specifier)
|
|
127
|
+
return;
|
|
128
|
+
if (!imports.includes(specifier))
|
|
129
|
+
imports.push(specifier);
|
|
130
|
+
};
|
|
131
|
+
append(adapterDescriptor.adapterModule);
|
|
132
|
+
for (const specifier of configuredImports) {
|
|
133
|
+
if (specifier === adapterDescriptor.adapterModule)
|
|
134
|
+
continue;
|
|
135
|
+
append(specifier);
|
|
136
|
+
}
|
|
137
|
+
return { imports };
|
|
64
138
|
}
|
|
65
139
|
async push(task, track = false, ...args) {
|
|
66
140
|
const { ctx, log } = (await this.logCtx(args, db_decorators_1.OperationKeys.CREATE, true)).for(this.push);
|
|
@@ -142,10 +216,13 @@ class TaskEngine extends ContextualLoggedClass_1.AbsContextual {
|
|
|
142
216
|
async start(...args) {
|
|
143
217
|
const { ctx } = (await this.logCtx(args, "run", true)).for(this.start);
|
|
144
218
|
await this.lock.acquire();
|
|
145
|
-
if (this.running)
|
|
219
|
+
if (this.running) {
|
|
220
|
+
this.lock.release();
|
|
146
221
|
return;
|
|
222
|
+
}
|
|
147
223
|
this.running = true;
|
|
148
224
|
this.lock.release();
|
|
225
|
+
await this.spawnWorkers();
|
|
149
226
|
void this.loop(ctx);
|
|
150
227
|
}
|
|
151
228
|
async stop(...args) {
|
|
@@ -161,7 +238,7 @@ class TaskEngine extends ContextualLoggedClass_1.AbsContextual {
|
|
|
161
238
|
.execute(ctx);
|
|
162
239
|
const timeout = ctx.getOrUndefined?.("gracefulShutdownMsTimeout") ??
|
|
163
240
|
this.config.gracefulShutdownMsTimeout;
|
|
164
|
-
|
|
241
|
+
await new Promise((resolve, reject) => {
|
|
165
242
|
const timer = setTimeout(() => {
|
|
166
243
|
log.error(`Graceful shutdown interrupted after ${timeout} ms...`);
|
|
167
244
|
resolve();
|
|
@@ -183,6 +260,249 @@ class TaskEngine extends ContextualLoggedClass_1.AbsContextual {
|
|
|
183
260
|
reject(err);
|
|
184
261
|
});
|
|
185
262
|
});
|
|
263
|
+
await this.shutdownWorkers();
|
|
264
|
+
}
|
|
265
|
+
// -------------------------
|
|
266
|
+
// Worker pool orchestration
|
|
267
|
+
// -------------------------
|
|
268
|
+
async spawnWorkers() {
|
|
269
|
+
if (!this.hasWorkerPool())
|
|
270
|
+
return;
|
|
271
|
+
const target = this.getWorkerCount();
|
|
272
|
+
const creations = [];
|
|
273
|
+
while (this.workerThreads.length < target) {
|
|
274
|
+
const ready = this.createWorker();
|
|
275
|
+
if (ready)
|
|
276
|
+
creations.push(ready);
|
|
277
|
+
}
|
|
278
|
+
if (creations.length) {
|
|
279
|
+
await Promise.all(creations);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
createWorker() {
|
|
283
|
+
if (!this.workerPoolConfig)
|
|
284
|
+
return undefined;
|
|
285
|
+
let resolveReady;
|
|
286
|
+
let rejectReady;
|
|
287
|
+
const readyPromise = new Promise((resolve, reject) => {
|
|
288
|
+
resolveReady = resolve;
|
|
289
|
+
rejectReady = reject;
|
|
290
|
+
});
|
|
291
|
+
const entry = this.workerPoolConfig.entry;
|
|
292
|
+
const workerId = `${this.config.workerId}-${this.workerCounter++}`;
|
|
293
|
+
const env = {
|
|
294
|
+
workerId,
|
|
295
|
+
mode: this.workerPoolConfig.mode ?? "node",
|
|
296
|
+
persistence: Object.assign({}, this.config.workerAdapter ?? WorkThreadEnvironment_1.DefaultWorkThreadEnvironment.persistence, {
|
|
297
|
+
alias: this.adapter.alias,
|
|
298
|
+
flavour: this.adapter.flavour,
|
|
299
|
+
}),
|
|
300
|
+
taskEngine: {
|
|
301
|
+
concurrency: this.workerThreadCapacity,
|
|
302
|
+
leaseMs: this.config.leaseMs,
|
|
303
|
+
pollMsBusy: this.config.pollMsBusy,
|
|
304
|
+
pollMsIdle: this.config.pollMsIdle,
|
|
305
|
+
logTailMax: this.config.logTailMax,
|
|
306
|
+
streamBufferSize: this.config.streamBufferSize,
|
|
307
|
+
maxLoggingBuffer: this.config.maxLoggingBuffer,
|
|
308
|
+
loggingBufferTruncation: this.config.loggingBufferTruncation,
|
|
309
|
+
gracefulShutdownMsTimeout: this.config.gracefulShutdownMsTimeout,
|
|
310
|
+
},
|
|
311
|
+
modules: this.computeWorkerModules(),
|
|
312
|
+
};
|
|
313
|
+
const worker = new node_worker_threads_1.Worker(entry, {
|
|
314
|
+
workerData: { environment: env },
|
|
315
|
+
});
|
|
316
|
+
const state = {
|
|
317
|
+
id: workerId,
|
|
318
|
+
worker,
|
|
319
|
+
ready: false,
|
|
320
|
+
activeJobs: 0,
|
|
321
|
+
capacity: this.workerThreadCapacity,
|
|
322
|
+
readyPromise,
|
|
323
|
+
resolveReady,
|
|
324
|
+
rejectReady,
|
|
325
|
+
};
|
|
326
|
+
this.workerThreads.push(state);
|
|
327
|
+
worker.on("message", (msg) => this.handleWorkerMessage(state, msg));
|
|
328
|
+
worker.on("error", (err) => this.handleWorkerError(state, err));
|
|
329
|
+
worker.on("exit", (code) => this.handleWorkerExit(state, code));
|
|
330
|
+
return readyPromise;
|
|
331
|
+
}
|
|
332
|
+
async shutdownWorkers() {
|
|
333
|
+
for (const state of this.workerThreads.splice(0)) {
|
|
334
|
+
const message = {
|
|
335
|
+
type: "control",
|
|
336
|
+
command: "shutdown",
|
|
337
|
+
};
|
|
338
|
+
try {
|
|
339
|
+
state.worker.postMessage(message);
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
// ignore
|
|
343
|
+
}
|
|
344
|
+
await state.worker.terminate();
|
|
345
|
+
}
|
|
346
|
+
for (const job of this.workerJobQueue.splice(0)) {
|
|
347
|
+
job.reject(new db_decorators_1.InternalError(`Worker pool shutting down before job ${job.id} could start`));
|
|
348
|
+
}
|
|
349
|
+
for (const job of this.workerJobs.values()) {
|
|
350
|
+
job.reject(new db_decorators_1.InternalError(`Worker terminated before finishing job ${job.id}`));
|
|
351
|
+
}
|
|
352
|
+
this.workerJobs.clear();
|
|
353
|
+
}
|
|
354
|
+
handleWorkerError(state, err) {
|
|
355
|
+
this.log.error(`worker ${state.id} error: ${err.message}`, err);
|
|
356
|
+
if (state.rejectReady) {
|
|
357
|
+
state.rejectReady(err);
|
|
358
|
+
state.resolveReady = undefined;
|
|
359
|
+
state.rejectReady = undefined;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
handleWorkerExit(state, code) {
|
|
363
|
+
this.log.info(`worker ${state.id} exited with code ${code}`);
|
|
364
|
+
if (state.rejectReady) {
|
|
365
|
+
state.rejectReady(new Error(`worker ${state.id} exited before reporting ready`));
|
|
366
|
+
state.resolveReady = undefined;
|
|
367
|
+
state.rejectReady = undefined;
|
|
368
|
+
}
|
|
369
|
+
const idx = this.workerThreads.indexOf(state);
|
|
370
|
+
if (idx >= 0)
|
|
371
|
+
this.workerThreads.splice(idx, 1);
|
|
372
|
+
for (const [jobId, job] of this.workerJobs.entries()) {
|
|
373
|
+
if (job.worker === state) {
|
|
374
|
+
job.worker = undefined;
|
|
375
|
+
this.workerJobs.delete(jobId);
|
|
376
|
+
this.workerJobQueue.unshift(job);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (this.running) {
|
|
380
|
+
void this.spawnWorkers().catch((err) => this.log.error(`failed to respawn worker`, err));
|
|
381
|
+
this.processWorkerQueue();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
handleWorkerMessage(state, msg) {
|
|
385
|
+
if (msg.type === "ready") {
|
|
386
|
+
state.ready = true;
|
|
387
|
+
if (state.resolveReady) {
|
|
388
|
+
state.resolveReady();
|
|
389
|
+
state.resolveReady = undefined;
|
|
390
|
+
state.rejectReady = undefined;
|
|
391
|
+
}
|
|
392
|
+
this.log.info(`worker ${state.id} ready`);
|
|
393
|
+
this.processWorkerQueue();
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (msg.type === "error") {
|
|
397
|
+
this.log.error(`worker ${state.id} reported error: ${msg.error}`);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (msg.type === "log") {
|
|
401
|
+
const job = this.workerJobs.get(msg.jobId);
|
|
402
|
+
if (!job)
|
|
403
|
+
return;
|
|
404
|
+
void this.appendLog(job.ctx, job.task, msg.entries)
|
|
405
|
+
.then(([updated, entries]) => {
|
|
406
|
+
job.task = updated;
|
|
407
|
+
return this.emitLog(job.ctx, job.task.id, entries);
|
|
408
|
+
})
|
|
409
|
+
.catch((err) => this.log.error(`Failed to append worker log`, err));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (msg.type === "progress") {
|
|
413
|
+
const job = this.workerJobs.get(msg.jobId);
|
|
414
|
+
if (!job)
|
|
415
|
+
return;
|
|
416
|
+
void this.emitProgress(job.ctx, job.task.id, msg.payload);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (msg.type === "heartbeat") {
|
|
420
|
+
const job = this.workerJobs.get(msg.jobId);
|
|
421
|
+
if (!job)
|
|
422
|
+
return;
|
|
423
|
+
job.task.leaseExpiry = new Date(Date.now() + this.config.leaseMs);
|
|
424
|
+
void this.tasks.update(job.task).catch(() => null);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (msg.type === "result") {
|
|
428
|
+
const job = this.workerJobs.get(msg.jobId);
|
|
429
|
+
if (!job)
|
|
430
|
+
return;
|
|
431
|
+
this.workerJobs.delete(job.id);
|
|
432
|
+
state.activeJobs = Math.max(0, state.activeJobs - 1);
|
|
433
|
+
this.applyWorkerCache(job.ctx, msg.cache);
|
|
434
|
+
switch (msg.status) {
|
|
435
|
+
case "success":
|
|
436
|
+
job.resolve(msg.output);
|
|
437
|
+
break;
|
|
438
|
+
case "error": {
|
|
439
|
+
const err = new Error(msg.error.message);
|
|
440
|
+
if (msg.error.name)
|
|
441
|
+
err.name = msg.error.name;
|
|
442
|
+
if (msg.error.stack)
|
|
443
|
+
err.stack = msg.error.stack;
|
|
444
|
+
job.reject(err);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
case "state-change":
|
|
448
|
+
job.reject(new TaskStateChangeError_1.TaskStateChangeError(msg.request));
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
this.processWorkerQueue();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
applyWorkerCache(ctx, cache) {
|
|
456
|
+
if (!cache)
|
|
457
|
+
return;
|
|
458
|
+
Object.entries(cache).forEach(([key, value]) => {
|
|
459
|
+
ctx.cacheResult(key, value);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
processWorkerQueue() {
|
|
463
|
+
if (!this.hasWorkerPool())
|
|
464
|
+
return;
|
|
465
|
+
const available = this.workerThreads
|
|
466
|
+
.filter((state) => state.ready && state.activeJobs < state.capacity)
|
|
467
|
+
.sort((a, b) => a.activeJobs - b.activeJobs);
|
|
468
|
+
for (const state of available) {
|
|
469
|
+
while (state.activeJobs < state.capacity &&
|
|
470
|
+
this.workerJobQueue.length > 0) {
|
|
471
|
+
const job = this.workerJobQueue.shift();
|
|
472
|
+
if (!job)
|
|
473
|
+
break;
|
|
474
|
+
this.assignWorker(state, job);
|
|
475
|
+
}
|
|
476
|
+
if (!this.workerJobQueue.length)
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
assignWorker(state, job) {
|
|
481
|
+
if (!this.workerPoolConfig)
|
|
482
|
+
return;
|
|
483
|
+
const payload = {
|
|
484
|
+
jobId: job.id,
|
|
485
|
+
taskId: job.task.id,
|
|
486
|
+
classification: job.classification,
|
|
487
|
+
input: job.input,
|
|
488
|
+
attempt: job.task.attempt ?? 0,
|
|
489
|
+
resultCache: job.ctx.resultCache ?? {},
|
|
490
|
+
streamBufferSize: this.config.streamBufferSize,
|
|
491
|
+
maxLoggingBuffer: this.config.maxLoggingBuffer,
|
|
492
|
+
loggingBufferTruncation: this.config.loggingBufferTruncation,
|
|
493
|
+
};
|
|
494
|
+
job.worker = state;
|
|
495
|
+
this.workerJobs.set(job.id, job);
|
|
496
|
+
state.activeJobs += 1;
|
|
497
|
+
const message = {
|
|
498
|
+
type: "execute",
|
|
499
|
+
job: payload,
|
|
500
|
+
};
|
|
501
|
+
state.worker.postMessage(message);
|
|
502
|
+
}
|
|
503
|
+
enqueueWorkerJob(job) {
|
|
504
|
+
this.workerJobQueue.push(job);
|
|
505
|
+
this.processWorkerQueue();
|
|
186
506
|
}
|
|
187
507
|
// -------------------------
|
|
188
508
|
// Worker loop
|
|
@@ -217,10 +537,11 @@ class TaskEngine extends ContextualLoggedClass_1.AbsContextual {
|
|
|
217
537
|
.or(condLeaseExpired)
|
|
218
538
|
.or(condScheduled);
|
|
219
539
|
// Fetch more than concurrency because some will fail to claim due to conflicts
|
|
540
|
+
const concurrency = this.getExecutionConcurrency();
|
|
220
541
|
const candidates = await this.tasks
|
|
221
542
|
.select()
|
|
222
543
|
.where(runnable)
|
|
223
|
-
.limit(Math.max(
|
|
544
|
+
.limit(Math.max(concurrency * 4, 20))
|
|
224
545
|
.execute();
|
|
225
546
|
log.verbose(`claimBatch candidates:${candidates.length}`);
|
|
226
547
|
const out = [];
|
|
@@ -228,7 +549,7 @@ class TaskEngine extends ContextualLoggedClass_1.AbsContextual {
|
|
|
228
549
|
const claimed = await this.tryClaim(c, ctx);
|
|
229
550
|
if (claimed)
|
|
230
551
|
out.push(claimed);
|
|
231
|
-
if (out.length >=
|
|
552
|
+
if (out.length >= concurrency)
|
|
232
553
|
break;
|
|
233
554
|
}
|
|
234
555
|
log.verbose(`claimBatch claimed:${out.length}`);
|
|
@@ -264,9 +585,39 @@ class TaskEngine extends ContextualLoggedClass_1.AbsContextual {
|
|
|
264
585
|
// -------------------------
|
|
265
586
|
// Execution
|
|
266
587
|
// -------------------------
|
|
588
|
+
async runHandlerInline(classification, input, ctx) {
|
|
589
|
+
const handler = this.registry.get(classification);
|
|
590
|
+
if (!handler)
|
|
591
|
+
throw new db_decorators_1.InternalError(`No task handler registered for type: ${classification}`);
|
|
592
|
+
return handler.run(input, ctx);
|
|
593
|
+
}
|
|
594
|
+
async dispatchToWorker(classification, input, task, ctx) {
|
|
595
|
+
if (!this.canDispatchToWorkers()) {
|
|
596
|
+
return this.runHandlerInline(classification, input, ctx);
|
|
597
|
+
}
|
|
598
|
+
const uuid = await index_1.UUID.instance.generate();
|
|
599
|
+
return new Promise((resolve, reject) => {
|
|
600
|
+
const job = {
|
|
601
|
+
id: uuid,
|
|
602
|
+
classification,
|
|
603
|
+
input,
|
|
604
|
+
task,
|
|
605
|
+
ctx,
|
|
606
|
+
resolve,
|
|
607
|
+
reject,
|
|
608
|
+
};
|
|
609
|
+
this.enqueueWorkerJob(job);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
async invokeHandler(classification, input, task, ctx) {
|
|
613
|
+
if (!this.hasWorkerPool()) {
|
|
614
|
+
return this.runHandlerInline(classification, input, ctx);
|
|
615
|
+
}
|
|
616
|
+
return this.dispatchToWorker(classification, input, task, ctx);
|
|
617
|
+
}
|
|
267
618
|
async executeClaimed(task) {
|
|
268
619
|
const { ctx, log } = (await this.logCtx([], task.classification, true)).for(this.executeClaimed);
|
|
269
|
-
const taskCtx =
|
|
620
|
+
const taskCtx = TaskEngine.createTaskContext(ctx, {
|
|
270
621
|
taskId: task.id,
|
|
271
622
|
logger: new logging_2.TaskLogger(log, this.config.streamBufferSize, this.config.maxLoggingBuffer),
|
|
272
623
|
attempt: task.attempt,
|
|
@@ -276,7 +627,7 @@ class TaskEngine extends ContextualLoggedClass_1.AbsContextual {
|
|
|
276
627
|
await this.emitLog(taskCtx, task.id, logs);
|
|
277
628
|
},
|
|
278
629
|
flush: async () => {
|
|
279
|
-
|
|
630
|
+
await taskCtx.logger.flush(taskCtx.pipe);
|
|
280
631
|
},
|
|
281
632
|
progress: async (data) => {
|
|
282
633
|
await this.emitProgress(taskCtx, task.id, data);
|
|
@@ -311,11 +662,8 @@ class TaskEngine extends ContextualLoggedClass_1.AbsContextual {
|
|
|
311
662
|
}
|
|
312
663
|
}
|
|
313
664
|
else {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (!handler)
|
|
317
|
-
throw new db_decorators_1.InternalError(`No task handler registered for type: ${task.classification}`);
|
|
318
|
-
output = await handler.run(task.input, taskCtx);
|
|
665
|
+
log.debug(`dispatching handler for ${task.id}`);
|
|
666
|
+
output = await this.invokeHandler(task.classification, task.input, task, taskCtx);
|
|
319
667
|
log.verbose(`handler finished for ${task.id}`);
|
|
320
668
|
}
|
|
321
669
|
task.status = constants_1.TaskStatus.SUCCEEDED;
|
|
@@ -473,15 +821,12 @@ class TaskEngine extends ContextualLoggedClass_1.AbsContextual {
|
|
|
473
821
|
}
|
|
474
822
|
while (idx < steps.length) {
|
|
475
823
|
const step = steps[idx];
|
|
476
|
-
const handler = this.registry.get(step.classification);
|
|
477
|
-
if (!handler)
|
|
478
|
-
throw new Error(`No task handler registered for composite step: ${step.classification}`);
|
|
479
824
|
await context.pipe([
|
|
480
825
|
logging_1.LogLevel.info,
|
|
481
826
|
`Composite step ${idx + 1}/${steps.length}: ${step.classification}`,
|
|
482
827
|
]);
|
|
483
828
|
try {
|
|
484
|
-
const out = await
|
|
829
|
+
const out = await this.invokeHandler(step.classification, step.input, task, context);
|
|
485
830
|
const stepIndex = idx;
|
|
486
831
|
const now = new Date();
|
|
487
832
|
results[stepIndex] = new TaskStepResultModel_1.TaskStepResultModel({
|
|
@@ -4,17 +4,25 @@ import { TaskEventModel } from "./models/TaskEventModel";
|
|
|
4
4
|
import { TaskHandlerRegistry } from "./TaskHandlerRegistry";
|
|
5
5
|
import { TaskEventBus } from "./TaskEventBus";
|
|
6
6
|
import { Adapter } from "../persistence/Adapter";
|
|
7
|
+
import { Context } from "../persistence/Context";
|
|
7
8
|
import { ContextOf, FlagsOf } from "../persistence/types";
|
|
8
9
|
import { AbsContextual, MaybeContextualArg } from "../utils/ContextualLoggedClass";
|
|
9
10
|
import { OperationKeys } from "@decaf-ts/db-decorators";
|
|
11
|
+
import { TaskContext } from "./TaskContext";
|
|
10
12
|
import { DateTarget } from "@decaf-ts/decorator-validation";
|
|
11
13
|
import { Constructor } from "@decaf-ts/decoration";
|
|
12
|
-
import { TaskEngineConfig } from "./types";
|
|
14
|
+
import { TaskEngineConfig, TaskFlags } from "./types";
|
|
13
15
|
import { TaskTracker } from "./TaskTracker";
|
|
14
16
|
export declare class TaskEngine<A extends Adapter<any, any, any, any>> extends AbsContextual<ContextOf<A>> {
|
|
15
17
|
private config;
|
|
16
18
|
private _tasks?;
|
|
17
19
|
private _events?;
|
|
20
|
+
private workerPoolConfig?;
|
|
21
|
+
private workerThreads;
|
|
22
|
+
private workerJobQueue;
|
|
23
|
+
private workerJobs;
|
|
24
|
+
private workerCounter;
|
|
25
|
+
private workerThreadCapacity;
|
|
18
26
|
private lock;
|
|
19
27
|
protected get Context(): Constructor<ContextOf<A>>;
|
|
20
28
|
protected get adapter(): A;
|
|
@@ -23,7 +31,15 @@ export declare class TaskEngine<A extends Adapter<any, any, any, any>> extends A
|
|
|
23
31
|
protected get tasks(): Repo<TaskModel>;
|
|
24
32
|
protected get events(): Repo<TaskEventModel>;
|
|
25
33
|
protected running: boolean;
|
|
34
|
+
static createTaskContext(base?: Context<any>, overrides?: Partial<TaskFlags>): TaskContext;
|
|
26
35
|
constructor(config: TaskEngineConfig<A>);
|
|
36
|
+
private normalizeWorkerPoolConfig;
|
|
37
|
+
private hasWorkerPool;
|
|
38
|
+
private getWorkerCount;
|
|
39
|
+
private getWorkerExecutionSlots;
|
|
40
|
+
private canDispatchToWorkers;
|
|
41
|
+
private getExecutionConcurrency;
|
|
42
|
+
private computeWorkerModules;
|
|
27
43
|
push<I, O>(task: TaskModel<I, O>, ...args: MaybeContextualArg<any>): Promise<TaskModel<I, O>>;
|
|
28
44
|
push<I, O>(task: TaskModel<I, O>, track: false, ...args: MaybeContextualArg<any>): Promise<TaskModel<I, O>>;
|
|
29
45
|
push<I, O>(task: TaskModel<I, O>, track: true, ...args: MaybeContextualArg<any>): Promise<{
|
|
@@ -51,9 +67,22 @@ export declare class TaskEngine<A extends Adapter<any, any, any, any>> extends A
|
|
|
51
67
|
isRunning(): Promise<boolean>;
|
|
52
68
|
start(...args: MaybeContextualArg<any>): Promise<void>;
|
|
53
69
|
stop(...args: MaybeContextualArg<any>): Promise<void>;
|
|
70
|
+
private spawnWorkers;
|
|
71
|
+
private createWorker;
|
|
72
|
+
private shutdownWorkers;
|
|
73
|
+
private handleWorkerError;
|
|
74
|
+
private handleWorkerExit;
|
|
75
|
+
private handleWorkerMessage;
|
|
76
|
+
private applyWorkerCache;
|
|
77
|
+
private processWorkerQueue;
|
|
78
|
+
private assignWorker;
|
|
79
|
+
private enqueueWorkerJob;
|
|
54
80
|
private loop;
|
|
55
81
|
private claimBatch;
|
|
56
82
|
private tryClaim;
|
|
83
|
+
private runHandlerInline;
|
|
84
|
+
private dispatchToWorker;
|
|
85
|
+
private invokeHandler;
|
|
57
86
|
private executeClaimed;
|
|
58
87
|
private handleTaskStateChange;
|
|
59
88
|
private runComposite;
|