@nmakarov/cli-toolkit 0.18.0 → 0.21.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/README.md +9 -0
- package/dist/cli-runner.cjs +426 -97
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +457 -128
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +1 -1
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +1 -1
- package/dist/db.js.map +1 -1
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client2.cjs +6 -2
- package/dist/http-client2.cjs.map +1 -1
- package/dist/http-client2.js +6 -2
- package/dist/http-client2.js.map +1 -1
- package/dist/index.cjs +482 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +469 -94
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +16 -1
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +16 -1
- package/dist/init.js.map +1 -1
- package/dist/mock-server.cjs.map +1 -1
- package/dist/mock-server.js.map +1 -1
- package/dist/params.cjs +16 -1
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +16 -1
- package/dist/params.js.map +1 -1
- package/dist/tasks.cjs +458 -87
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +478 -118
- package/dist/tasks.js.map +1 -1
- package/package.json +10 -3
- package/scripts/ssm/parse-cli.ts +35 -0
- package/scripts/ssm/ssm-admin.ts +151 -0
- package/scripts/ssm/ssm-pull.ts +150 -0
package/dist/tasks.cjs
CHANGED
|
@@ -44,9 +44,20 @@ __export(tasks_exports, {
|
|
|
44
44
|
enqueueStopTask: () => enqueueStopTask,
|
|
45
45
|
enqueueTask: () => enqueueTask,
|
|
46
46
|
ensureTaskTables: () => ensureTaskTables,
|
|
47
|
+
listAliveRunnerHeartbeats: () => listServicesRegistry,
|
|
48
|
+
listServicesRegistry: () => listServicesRegistry,
|
|
47
49
|
queueToTableNames: () => queueToTableNames,
|
|
50
|
+
registerInServicesRegistry: () => registerInServicesRegistry,
|
|
51
|
+
registerRunnerHeartbeat: () => registerInServicesRegistry,
|
|
48
52
|
runNodeTaskScript: () => runNodeTaskScript,
|
|
49
53
|
runTasksLoop: () => runTasksLoop,
|
|
54
|
+
runnerHeartbeatsTable: () => servicesRegistryTable,
|
|
55
|
+
servicesRegistryTable: () => servicesRegistryTable,
|
|
56
|
+
touchRunnerHeartbeat: () => touchServicesRegistry,
|
|
57
|
+
touchServicesRegistry: () => touchServicesRegistry,
|
|
58
|
+
unregisterRunnerHeartbeat: () => unregisterServicesRegistry,
|
|
59
|
+
unregisterServicesRegistry: () => unregisterServicesRegistry,
|
|
60
|
+
updateServicesRegistryMetadata: () => updateServicesRegistryMetadata,
|
|
50
61
|
updateTaskProgress: () => updateTaskProgress,
|
|
51
62
|
waitForTaskResult: () => waitForTaskResult
|
|
52
63
|
});
|
|
@@ -133,6 +144,12 @@ function toJsonColumn(value) {
|
|
|
133
144
|
return JSON.stringify(value);
|
|
134
145
|
}
|
|
135
146
|
|
|
147
|
+
// src/tasks/servicesRegistry.ts
|
|
148
|
+
var import_node_crypto2 = require("crypto");
|
|
149
|
+
var import_promises = require("fs/promises");
|
|
150
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
151
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
152
|
+
|
|
136
153
|
// src/tasks/taskUtils.ts
|
|
137
154
|
var import_node_crypto = require("crypto");
|
|
138
155
|
function getDb(context) {
|
|
@@ -151,6 +168,12 @@ function queueToTableNames(queue) {
|
|
|
151
168
|
historyTable: `${queue}_history`
|
|
152
169
|
};
|
|
153
170
|
}
|
|
171
|
+
function servicesRegistryTable(queue) {
|
|
172
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
|
|
173
|
+
throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
|
|
174
|
+
}
|
|
175
|
+
return `${queue}_services_registry`;
|
|
176
|
+
}
|
|
154
177
|
async function ensureTaskTables(context, options = {}) {
|
|
155
178
|
const queue = options.queue ?? "tasks";
|
|
156
179
|
const recreate = options.recreate ?? false;
|
|
@@ -187,18 +210,6 @@ async function ensureTaskTables(context, options = {}) {
|
|
|
187
210
|
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
188
211
|
});
|
|
189
212
|
}
|
|
190
|
-
const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
|
|
191
|
-
if (!tasksHasOpid) {
|
|
192
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
193
|
-
t.text("opid");
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
|
|
197
|
-
if (!tasksHasPausedAt) {
|
|
198
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
199
|
-
t.timestamp("paused_at").defaultTo(null);
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
213
|
if (needsHistory) {
|
|
203
214
|
await db.schema.createTable(historyTable, (t) => {
|
|
204
215
|
t.uuid("id").notNullable();
|
|
@@ -221,10 +232,24 @@ async function ensureTaskTables(context, options = {}) {
|
|
|
221
232
|
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
222
233
|
});
|
|
223
234
|
}
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
235
|
+
const registryTable = servicesRegistryTable(queue);
|
|
236
|
+
const needsRegistry = !await db.tableExists(registryTable);
|
|
237
|
+
if (needsRegistry) {
|
|
238
|
+
await db.schema.createTable(registryTable, (t) => {
|
|
239
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
240
|
+
t.uuid("instance_id").notNullable().unique();
|
|
241
|
+
t.text("queue").notNullable();
|
|
242
|
+
t.text("service_group").notNullable();
|
|
243
|
+
t.text("service_name").notNullable();
|
|
244
|
+
t.text("target").notNullable();
|
|
245
|
+
t.text("hostname");
|
|
246
|
+
t.integer("pid");
|
|
247
|
+
t.json("metadata");
|
|
248
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
249
|
+
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
250
|
+
t.unique(["queue", "service_name"], `${registryTable}_queue_service_name_uniq`);
|
|
251
|
+
t.index(["queue", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
252
|
+
t.index(["queue", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
228
253
|
});
|
|
229
254
|
}
|
|
230
255
|
}
|
|
@@ -251,6 +276,260 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
|
251
276
|
});
|
|
252
277
|
}
|
|
253
278
|
|
|
279
|
+
// src/tasks/servicesRegistry.ts
|
|
280
|
+
function getDb2(context) {
|
|
281
|
+
const db = context.db;
|
|
282
|
+
if (!db) {
|
|
283
|
+
throw new Error("Services registry requires context.db");
|
|
284
|
+
}
|
|
285
|
+
return db;
|
|
286
|
+
}
|
|
287
|
+
function parseMetadataColumn(value) {
|
|
288
|
+
if (!value) return {};
|
|
289
|
+
if (typeof value === "object" && !Array.isArray(value)) return value;
|
|
290
|
+
if (typeof value === "string") {
|
|
291
|
+
try {
|
|
292
|
+
const p = JSON.parse(value);
|
|
293
|
+
return p && typeof p === "object" && !Array.isArray(p) ? p : {};
|
|
294
|
+
} catch {
|
|
295
|
+
return {};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return {};
|
|
299
|
+
}
|
|
300
|
+
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
301
|
+
intake: 1,
|
|
302
|
+
harvest: 1,
|
|
303
|
+
loader: 0,
|
|
304
|
+
photos: 0,
|
|
305
|
+
photosprocessor: 0,
|
|
306
|
+
ingest: 0
|
|
307
|
+
};
|
|
308
|
+
function sanitizeNamePart(raw) {
|
|
309
|
+
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
310
|
+
return s.slice(0, 80) || "runner";
|
|
311
|
+
}
|
|
312
|
+
function identityFilePath(identityDir, queue, serviceGroup) {
|
|
313
|
+
const safeQ = sanitizeNamePart(queue);
|
|
314
|
+
const safeG = sanitizeNamePart(serviceGroup);
|
|
315
|
+
return import_node_path.default.join(identityDir, `${safeQ}_${safeG}.json`);
|
|
316
|
+
}
|
|
317
|
+
async function readIdentityFile(filePath) {
|
|
318
|
+
try {
|
|
319
|
+
const text = await (0, import_promises.readFile)(filePath, "utf8");
|
|
320
|
+
const parsed = JSON.parse(text);
|
|
321
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
322
|
+
} catch {
|
|
323
|
+
return {};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function writeIdentityFile(filePath, data) {
|
|
327
|
+
await (0, import_promises.mkdir)(import_node_path.default.dirname(filePath), { recursive: true });
|
|
328
|
+
await (0, import_promises.writeFile)(filePath, `${JSON.stringify(data, null, 2)}
|
|
329
|
+
`, "utf8");
|
|
330
|
+
}
|
|
331
|
+
function resolveMaxInstances(serviceGroup, override) {
|
|
332
|
+
if (override !== void 0 && Number.isFinite(override)) {
|
|
333
|
+
return Math.max(0, Math.floor(Number(override)));
|
|
334
|
+
}
|
|
335
|
+
const g = serviceGroup.trim().toLowerCase();
|
|
336
|
+
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
337
|
+
}
|
|
338
|
+
async function countAliveInGroup(db, registryTable, queue, serviceGroup, staleMs, excludeInstanceId) {
|
|
339
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
340
|
+
let q = db(registryTable).where({ queue, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
|
|
341
|
+
if (excludeInstanceId) {
|
|
342
|
+
q = q.whereNot("instance_id", excludeInstanceId);
|
|
343
|
+
}
|
|
344
|
+
const row = await q.count("id as count").first();
|
|
345
|
+
return Number(row?.count ?? 0);
|
|
346
|
+
}
|
|
347
|
+
function isUniqueViolation(error) {
|
|
348
|
+
const code = error?.code ?? error?.errno;
|
|
349
|
+
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
350
|
+
}
|
|
351
|
+
async function registerInServicesRegistry(context, options) {
|
|
352
|
+
const db = getDb2(context);
|
|
353
|
+
const registryTable = servicesRegistryTable(options.queue);
|
|
354
|
+
const serviceGroup = options.serviceGroup.trim();
|
|
355
|
+
if (!serviceGroup) {
|
|
356
|
+
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
357
|
+
}
|
|
358
|
+
const identityPath = identityFilePath(options.identityDir, options.queue, serviceGroup);
|
|
359
|
+
let identity = await readIdentityFile(identityPath);
|
|
360
|
+
let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : (0, import_node_crypto2.randomUUID)();
|
|
361
|
+
identity.instanceId = instanceId;
|
|
362
|
+
await writeIdentityFile(identityPath, identity);
|
|
363
|
+
const hostname = import_node_os.default.hostname();
|
|
364
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
365
|
+
const meta = toJsonColumn(options.metadata ?? null);
|
|
366
|
+
const existing = await db(registryTable).where({ instance_id: instanceId }).first();
|
|
367
|
+
if (existing) {
|
|
368
|
+
await db(registryTable).where({ instance_id: instanceId }).update({
|
|
369
|
+
target: options.target,
|
|
370
|
+
hostname,
|
|
371
|
+
pid,
|
|
372
|
+
metadata: meta,
|
|
373
|
+
last_seen_at: db.fn.now()
|
|
374
|
+
});
|
|
375
|
+
const serviceName = String(existing.service_name);
|
|
376
|
+
identity.serviceName = serviceName;
|
|
377
|
+
await writeIdentityFile(identityPath, identity);
|
|
378
|
+
const reg = {
|
|
379
|
+
instanceId,
|
|
380
|
+
serviceName,
|
|
381
|
+
serviceGroup,
|
|
382
|
+
queue: options.queue,
|
|
383
|
+
target: options.target,
|
|
384
|
+
rowId: String(existing.id)
|
|
385
|
+
};
|
|
386
|
+
context.servicesRegistry = reg;
|
|
387
|
+
context.runnerHeartbeat = reg;
|
|
388
|
+
context.logger.info?.(
|
|
389
|
+
`[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
|
|
390
|
+
);
|
|
391
|
+
return {
|
|
392
|
+
instanceId,
|
|
393
|
+
serviceName,
|
|
394
|
+
serviceGroup,
|
|
395
|
+
queue: options.queue,
|
|
396
|
+
target: options.target,
|
|
397
|
+
rowId: String(existing.id),
|
|
398
|
+
registryTable
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
|
|
402
|
+
const aliveOthers = await countAliveInGroup(
|
|
403
|
+
db,
|
|
404
|
+
registryTable,
|
|
405
|
+
options.queue,
|
|
406
|
+
serviceGroup,
|
|
407
|
+
options.staleMs,
|
|
408
|
+
instanceId
|
|
409
|
+
);
|
|
410
|
+
if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
|
|
411
|
+
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
|
|
412
|
+
if (options.enforceMaxInstances) {
|
|
413
|
+
throw new Error(msg);
|
|
414
|
+
}
|
|
415
|
+
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
416
|
+
}
|
|
417
|
+
const explicitName = options.serviceName?.trim();
|
|
418
|
+
const fromFile = typeof identity.serviceName === "string" ? identity.serviceName.trim() : "";
|
|
419
|
+
const hostBase = sanitizeNamePart(hostname);
|
|
420
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
421
|
+
const baseCandidates = [];
|
|
422
|
+
if (explicitName) baseCandidates.push(sanitizeNamePart(explicitName));
|
|
423
|
+
if (fromFile) baseCandidates.push(sanitizeNamePart(fromFile));
|
|
424
|
+
baseCandidates.push(`${groupBase}-${hostBase}`);
|
|
425
|
+
baseCandidates.push(groupBase);
|
|
426
|
+
function* eachServiceNameCandidate(bases) {
|
|
427
|
+
const seen = /* @__PURE__ */ new Set();
|
|
428
|
+
for (const rawBase of bases) {
|
|
429
|
+
const base = sanitizeNamePart(rawBase);
|
|
430
|
+
if (!base) continue;
|
|
431
|
+
const seq = [base];
|
|
432
|
+
for (let n = 2; n <= 500; n++) seq.push(`${base}-${n}`);
|
|
433
|
+
for (const c of seq) {
|
|
434
|
+
if (seen.has(c)) continue;
|
|
435
|
+
seen.add(c);
|
|
436
|
+
yield c;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
let inserted;
|
|
441
|
+
for (const candidate of eachServiceNameCandidate(baseCandidates)) {
|
|
442
|
+
try {
|
|
443
|
+
const rows = await db(registryTable).insert({
|
|
444
|
+
instance_id: instanceId,
|
|
445
|
+
queue: options.queue,
|
|
446
|
+
service_group: serviceGroup,
|
|
447
|
+
service_name: candidate,
|
|
448
|
+
target: options.target,
|
|
449
|
+
hostname,
|
|
450
|
+
pid,
|
|
451
|
+
metadata: meta,
|
|
452
|
+
last_seen_at: db.fn.now()
|
|
453
|
+
}).returning(["id", "service_name"]);
|
|
454
|
+
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
455
|
+
if (row) {
|
|
456
|
+
inserted = { id: String(row.id), service_name: String(row.service_name) };
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (!isUniqueViolation(error)) {
|
|
461
|
+
throw error;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (!inserted) {
|
|
466
|
+
throw new Error(
|
|
467
|
+
`[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
identity.serviceName = inserted.service_name;
|
|
471
|
+
await writeIdentityFile(identityPath, identity);
|
|
472
|
+
const regNew = {
|
|
473
|
+
instanceId,
|
|
474
|
+
serviceName: inserted.service_name,
|
|
475
|
+
serviceGroup,
|
|
476
|
+
queue: options.queue,
|
|
477
|
+
target: options.target,
|
|
478
|
+
rowId: inserted.id
|
|
479
|
+
};
|
|
480
|
+
context.servicesRegistry = regNew;
|
|
481
|
+
context.runnerHeartbeat = regNew;
|
|
482
|
+
context.logger.info?.(
|
|
483
|
+
`[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
|
|
484
|
+
);
|
|
485
|
+
return {
|
|
486
|
+
instanceId,
|
|
487
|
+
serviceName: inserted.service_name,
|
|
488
|
+
serviceGroup,
|
|
489
|
+
queue: options.queue,
|
|
490
|
+
target: options.target,
|
|
491
|
+
rowId: inserted.id,
|
|
492
|
+
registryTable
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
async function touchServicesRegistry(context, registration) {
|
|
496
|
+
const db = getDb2(context);
|
|
497
|
+
const hostname = import_node_os.default.hostname();
|
|
498
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
499
|
+
await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
|
|
500
|
+
last_seen_at: db.fn.now(),
|
|
501
|
+
hostname,
|
|
502
|
+
pid
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
async function updateServicesRegistryMetadata(context, registration, patch) {
|
|
506
|
+
const db = getDb2(context);
|
|
507
|
+
const row = await db(registration.registryTable).where({ instance_id: registration.instanceId }).first();
|
|
508
|
+
const prev = parseMetadataColumn(row?.metadata);
|
|
509
|
+
const merged = { ...prev, ...patch };
|
|
510
|
+
await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
|
|
511
|
+
metadata: toJsonColumn(merged),
|
|
512
|
+
last_seen_at: db.fn.now()
|
|
513
|
+
});
|
|
514
|
+
context.logger.info?.(`[services-registry] metadata updated for ${registration.serviceName}`);
|
|
515
|
+
}
|
|
516
|
+
async function unregisterServicesRegistry(context, registration) {
|
|
517
|
+
const db = getDb2(context);
|
|
518
|
+
await db(registration.registryTable).where({ instance_id: registration.instanceId }).delete();
|
|
519
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} instance_id=${registration.instanceId}`);
|
|
520
|
+
}
|
|
521
|
+
async function listServicesRegistry(context, options = { queue: "tasks" }) {
|
|
522
|
+
const db = getDb2(context);
|
|
523
|
+
const staleMs = options.staleMs ?? 6e4;
|
|
524
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
525
|
+
const table = servicesRegistryTable(options.queue);
|
|
526
|
+
let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
|
|
527
|
+
if (options.serviceGroup?.trim()) {
|
|
528
|
+
q = q.where({ service_group: options.serviceGroup.trim() });
|
|
529
|
+
}
|
|
530
|
+
return await q;
|
|
531
|
+
}
|
|
532
|
+
|
|
254
533
|
// src/filedatabase/index.ts
|
|
255
534
|
var import_fs3 = __toESM(require("fs"), 1);
|
|
256
535
|
var import_path3 = __toESM(require("path"), 1);
|
|
@@ -1639,8 +1918,8 @@ var TaskShellCommand = class extends TaskMaster {
|
|
|
1639
1918
|
};
|
|
1640
1919
|
|
|
1641
1920
|
// src/tasks/coreTasks/TaskSystemInfo.ts
|
|
1642
|
-
var
|
|
1643
|
-
var
|
|
1921
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
1922
|
+
var import_promises2 = __toESM(require("fs/promises"), 1);
|
|
1644
1923
|
function toGb(valueBytes) {
|
|
1645
1924
|
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
1646
1925
|
}
|
|
@@ -1648,7 +1927,7 @@ function toMb(valueBytes) {
|
|
|
1648
1927
|
return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
|
|
1649
1928
|
}
|
|
1650
1929
|
async function getDiskStats() {
|
|
1651
|
-
const stats = await
|
|
1930
|
+
const stats = await import_promises2.default.statfs("/");
|
|
1652
1931
|
const total = Number(stats.bsize) * Number(stats.blocks);
|
|
1653
1932
|
const free = Number(stats.bsize) * Number(stats.bavail);
|
|
1654
1933
|
const used = total - free;
|
|
@@ -1661,10 +1940,10 @@ async function getDiskStats() {
|
|
|
1661
1940
|
var TaskSystemInfo = class extends TaskMaster {
|
|
1662
1941
|
async run() {
|
|
1663
1942
|
try {
|
|
1664
|
-
const totalMemory =
|
|
1665
|
-
const freeMemory =
|
|
1943
|
+
const totalMemory = import_node_os2.default.totalmem();
|
|
1944
|
+
const freeMemory = import_node_os2.default.freemem();
|
|
1666
1945
|
const usedMemory = totalMemory - freeMemory;
|
|
1667
|
-
const cpus =
|
|
1946
|
+
const cpus = import_node_os2.default.cpus();
|
|
1668
1947
|
const cpuUtilization = cpus.map((cpu) => {
|
|
1669
1948
|
const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
|
|
1670
1949
|
const usage = (total - cpu.times.idle) / total * 100;
|
|
@@ -1690,10 +1969,10 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
1690
1969
|
utilization: cpuUtilization
|
|
1691
1970
|
},
|
|
1692
1971
|
runtime: {
|
|
1693
|
-
platform:
|
|
1694
|
-
arch:
|
|
1695
|
-
uptimeSec:
|
|
1696
|
-
hostname:
|
|
1972
|
+
platform: import_node_os2.default.platform(),
|
|
1973
|
+
arch: import_node_os2.default.arch(),
|
|
1974
|
+
uptimeSec: import_node_os2.default.uptime(),
|
|
1975
|
+
hostname: import_node_os2.default.hostname()
|
|
1697
1976
|
}
|
|
1698
1977
|
};
|
|
1699
1978
|
this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
|
|
@@ -1926,7 +2205,7 @@ async function runNodeTaskScript(context, options) {
|
|
|
1926
2205
|
// src/tasks/index.ts
|
|
1927
2206
|
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
1928
2207
|
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
1929
|
-
function
|
|
2208
|
+
function getDb3(context) {
|
|
1930
2209
|
const db = context.db;
|
|
1931
2210
|
if (!db) {
|
|
1932
2211
|
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
@@ -1970,7 +2249,7 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
|
|
|
1970
2249
|
context.emitter.emit("stop", allowanceMs);
|
|
1971
2250
|
}
|
|
1972
2251
|
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
1973
|
-
const db =
|
|
2252
|
+
const db = getDb3(context);
|
|
1974
2253
|
const taskName = row.task;
|
|
1975
2254
|
const TaskClass = registry.get(taskName);
|
|
1976
2255
|
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
@@ -2071,7 +2350,7 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
2071
2350
|
return { stopRunnerRequested, stopAllowanceMs };
|
|
2072
2351
|
}
|
|
2073
2352
|
async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
|
|
2074
|
-
const db =
|
|
2353
|
+
const db = getDb3(context);
|
|
2075
2354
|
let query = db(tasksTable).whereNull("started_at").whereNull("paused_at").where({ target }).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "desc" }]).orderByRaw("CASE WHEN completed_at IS NULL THEN 0 ELSE 1 END ASC").orderBy([{ column: "completed_at", order: "asc" }, { column: "created_at", order: "asc" }]).limit(scanLimit);
|
|
2076
2355
|
if (taskNames && taskNames.length > 0) {
|
|
2077
2356
|
query = query.whereIn("task", taskNames);
|
|
@@ -2117,25 +2396,76 @@ async function runTasksLoop(context, options) {
|
|
|
2117
2396
|
let stopRequested = false;
|
|
2118
2397
|
let stopAllowanceMs = 5e3;
|
|
2119
2398
|
context.__tasksRunnerStop = false;
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
)
|
|
2130
|
-
|
|
2131
|
-
|
|
2399
|
+
let registryReg = null;
|
|
2400
|
+
let registryInterval = null;
|
|
2401
|
+
const hbGroup = options.runnerServiceGroup?.trim();
|
|
2402
|
+
if (hbGroup) {
|
|
2403
|
+
const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
|
|
2404
|
+
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
2405
|
+
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
2406
|
+
const defaultMeta = {
|
|
2407
|
+
component: "tasks-runner",
|
|
2408
|
+
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
2409
|
+
};
|
|
2410
|
+
registryReg = await registerInServicesRegistry(context, {
|
|
2411
|
+
queue,
|
|
2412
|
+
target,
|
|
2413
|
+
serviceGroup: hbGroup,
|
|
2414
|
+
serviceName: options.runnerServiceName,
|
|
2415
|
+
identityDir,
|
|
2416
|
+
staleMs,
|
|
2417
|
+
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
2418
|
+
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
2419
|
+
metadata: options.runnerMetadata ?? defaultMeta
|
|
2420
|
+
});
|
|
2421
|
+
registryInterval = setInterval(() => {
|
|
2422
|
+
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
2423
|
+
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
2424
|
+
});
|
|
2425
|
+
}, hbIntervalMs);
|
|
2426
|
+
}
|
|
2427
|
+
try {
|
|
2428
|
+
while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
|
|
2429
|
+
if (!runningStopControlPromise) {
|
|
2430
|
+
const claimedStopTask = await claimNextRunnableTask(
|
|
2431
|
+
context,
|
|
2432
|
+
tasksTable,
|
|
2433
|
+
target,
|
|
2434
|
+
registry,
|
|
2435
|
+
10,
|
|
2436
|
+
["stopRunner", "stop"]
|
|
2437
|
+
);
|
|
2438
|
+
if (claimedStopTask) {
|
|
2439
|
+
runningStopControlPromise = executeClaimedTask(
|
|
2440
|
+
context,
|
|
2441
|
+
tasksTable,
|
|
2442
|
+
historyTable,
|
|
2443
|
+
claimedStopTask,
|
|
2444
|
+
registry,
|
|
2445
|
+
runningTaskInstances
|
|
2446
|
+
).then(async (outcome) => {
|
|
2447
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
2448
|
+
stopRequested = true;
|
|
2449
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
2450
|
+
context.__tasksRunnerStop = true;
|
|
2451
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
2452
|
+
}
|
|
2453
|
+
}).finally(() => {
|
|
2454
|
+
runningStopControlPromise = null;
|
|
2455
|
+
});
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
while (runningPromises.size < maxParallel) {
|
|
2459
|
+
const claimed = await claimNextRunnableTask(
|
|
2132
2460
|
context,
|
|
2133
2461
|
tasksTable,
|
|
2134
|
-
|
|
2135
|
-
claimedStopTask,
|
|
2462
|
+
target,
|
|
2136
2463
|
registry,
|
|
2137
|
-
|
|
2138
|
-
|
|
2464
|
+
scanLimit,
|
|
2465
|
+
allowedTasks
|
|
2466
|
+
);
|
|
2467
|
+
if (!claimed) break;
|
|
2468
|
+
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
2139
2469
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
2140
2470
|
stopRequested = true;
|
|
2141
2471
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
@@ -2143,54 +2473,46 @@ async function runTasksLoop(context, options) {
|
|
|
2143
2473
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
2144
2474
|
}
|
|
2145
2475
|
}).finally(() => {
|
|
2146
|
-
|
|
2476
|
+
runningPromises.delete(p);
|
|
2147
2477
|
});
|
|
2478
|
+
runningPromises.add(p);
|
|
2479
|
+
}
|
|
2480
|
+
await sleepMs(pollMs);
|
|
2481
|
+
}
|
|
2482
|
+
if (context.isStop() && !stopRequested) {
|
|
2483
|
+
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
2484
|
+
}
|
|
2485
|
+
if (runningPromises.size > 0) {
|
|
2486
|
+
if (stopRequested) {
|
|
2487
|
+
await Promise.race([
|
|
2488
|
+
Promise.allSettled(Array.from(runningPromises)),
|
|
2489
|
+
sleepMs(stopAllowanceMs).then(() => {
|
|
2490
|
+
context.logger.warn?.(
|
|
2491
|
+
`[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
|
|
2492
|
+
);
|
|
2493
|
+
})
|
|
2494
|
+
]);
|
|
2495
|
+
} else {
|
|
2496
|
+
await Promise.allSettled(Array.from(runningPromises));
|
|
2148
2497
|
}
|
|
2149
2498
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
target,
|
|
2155
|
-
registry,
|
|
2156
|
-
scanLimit,
|
|
2157
|
-
allowedTasks
|
|
2158
|
-
);
|
|
2159
|
-
if (!claimed) break;
|
|
2160
|
-
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
2161
|
-
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
2162
|
-
stopRequested = true;
|
|
2163
|
-
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
2164
|
-
context.__tasksRunnerStop = true;
|
|
2165
|
-
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
2166
|
-
}
|
|
2167
|
-
}).finally(() => {
|
|
2168
|
-
runningPromises.delete(p);
|
|
2169
|
-
});
|
|
2170
|
-
runningPromises.add(p);
|
|
2499
|
+
} finally {
|
|
2500
|
+
if (registryInterval) {
|
|
2501
|
+
clearInterval(registryInterval);
|
|
2502
|
+
registryInterval = null;
|
|
2171
2503
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
await Promise.race([
|
|
2180
|
-
Promise.allSettled(Array.from(runningPromises)),
|
|
2181
|
-
sleepMs(stopAllowanceMs).then(() => {
|
|
2182
|
-
context.logger.warn?.(
|
|
2183
|
-
`[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
|
|
2184
|
-
);
|
|
2185
|
-
})
|
|
2186
|
-
]);
|
|
2187
|
-
} else {
|
|
2188
|
-
await Promise.allSettled(Array.from(runningPromises));
|
|
2504
|
+
if (registryReg) {
|
|
2505
|
+
await unregisterServicesRegistry(context, registryReg).catch((err) => {
|
|
2506
|
+
context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
|
|
2507
|
+
});
|
|
2508
|
+
registryReg = null;
|
|
2509
|
+
delete context.servicesRegistry;
|
|
2510
|
+
delete context.runnerHeartbeat;
|
|
2189
2511
|
}
|
|
2190
2512
|
}
|
|
2191
2513
|
}
|
|
2192
2514
|
async function waitForTaskResult(context, taskId, options = {}) {
|
|
2193
|
-
const db =
|
|
2515
|
+
const db = getDb3(context);
|
|
2194
2516
|
const queue = options.queue ?? "tasks";
|
|
2195
2517
|
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
2196
2518
|
const pollMs = options.pollMs ?? 500;
|
|
@@ -2218,6 +2540,14 @@ var TasksManager = class _TasksManager {
|
|
|
2218
2540
|
scanLimit;
|
|
2219
2541
|
allowedTasks;
|
|
2220
2542
|
registry;
|
|
2543
|
+
runnerServiceGroup;
|
|
2544
|
+
runnerServiceName;
|
|
2545
|
+
runnerIdentityDir;
|
|
2546
|
+
runnerHeartbeatIntervalMs;
|
|
2547
|
+
runnerHeartbeatStaleMs;
|
|
2548
|
+
runnerGroupMaxInstances;
|
|
2549
|
+
runnerEnforceMaxInstances;
|
|
2550
|
+
runnerMetadata;
|
|
2221
2551
|
constructor(context, options = {}) {
|
|
2222
2552
|
this.context = context;
|
|
2223
2553
|
this.queue = options.queue ?? "tasks";
|
|
@@ -2228,6 +2558,14 @@ var TasksManager = class _TasksManager {
|
|
|
2228
2558
|
this.scanLimit = options.scanLimit ?? 100;
|
|
2229
2559
|
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
2230
2560
|
this.registry = normalizeRegistry(options.registry);
|
|
2561
|
+
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
2562
|
+
this.runnerServiceName = options.runnerServiceName;
|
|
2563
|
+
this.runnerIdentityDir = options.runnerIdentityDir;
|
|
2564
|
+
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
2565
|
+
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
2566
|
+
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
2567
|
+
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
2568
|
+
this.runnerMetadata = options.runnerMetadata;
|
|
2231
2569
|
}
|
|
2232
2570
|
static init(context, options = {}) {
|
|
2233
2571
|
const defs = {
|
|
@@ -2237,7 +2575,14 @@ var TasksManager = class _TasksManager {
|
|
|
2237
2575
|
pollMs: "number default 1000",
|
|
2238
2576
|
maxParallel: "number default 1",
|
|
2239
2577
|
scanLimit: "number default 100",
|
|
2240
|
-
allowedTasks: "string"
|
|
2578
|
+
allowedTasks: "string",
|
|
2579
|
+
runnerServiceGroup: "string",
|
|
2580
|
+
runnerServiceName: "string",
|
|
2581
|
+
runnerIdentityDir: "string default ./data/runner-identities",
|
|
2582
|
+
runnerHeartbeatIntervalMs: "number default 10000",
|
|
2583
|
+
runnerHeartbeatStaleMs: "number default 45000",
|
|
2584
|
+
runnerGroupMaxInstances: "number",
|
|
2585
|
+
runnerEnforceMaxInstances: "boolean default true"
|
|
2241
2586
|
};
|
|
2242
2587
|
const discovered = context.params.getAllForModule(defs);
|
|
2243
2588
|
const resolved = {
|
|
@@ -2248,6 +2593,13 @@ var TasksManager = class _TasksManager {
|
|
|
2248
2593
|
maxParallel: discovered.maxParallel,
|
|
2249
2594
|
scanLimit: discovered.scanLimit,
|
|
2250
2595
|
allowedTasks: discovered.allowedTasks,
|
|
2596
|
+
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
2597
|
+
runnerServiceName: discovered.runnerServiceName,
|
|
2598
|
+
runnerIdentityDir: discovered.runnerIdentityDir,
|
|
2599
|
+
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
2600
|
+
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
2601
|
+
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
2602
|
+
runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
|
|
2251
2603
|
...options
|
|
2252
2604
|
};
|
|
2253
2605
|
return new _TasksManager(context, resolved);
|
|
@@ -2266,7 +2618,15 @@ var TasksManager = class _TasksManager {
|
|
|
2266
2618
|
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
2267
2619
|
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
2268
2620
|
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
2269
|
-
registry: options.registry ?? this.registry
|
|
2621
|
+
registry: options.registry ?? this.registry,
|
|
2622
|
+
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
2623
|
+
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
2624
|
+
runnerIdentityDir: options.runnerIdentityDir ?? this.runnerIdentityDir,
|
|
2625
|
+
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
2626
|
+
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
2627
|
+
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
2628
|
+
runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
|
|
2629
|
+
runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
|
|
2270
2630
|
});
|
|
2271
2631
|
}
|
|
2272
2632
|
};
|
|
@@ -2286,9 +2646,20 @@ var TasksManager = class _TasksManager {
|
|
|
2286
2646
|
enqueueStopTask,
|
|
2287
2647
|
enqueueTask,
|
|
2288
2648
|
ensureTaskTables,
|
|
2649
|
+
listAliveRunnerHeartbeats,
|
|
2650
|
+
listServicesRegistry,
|
|
2289
2651
|
queueToTableNames,
|
|
2652
|
+
registerInServicesRegistry,
|
|
2653
|
+
registerRunnerHeartbeat,
|
|
2290
2654
|
runNodeTaskScript,
|
|
2291
2655
|
runTasksLoop,
|
|
2656
|
+
runnerHeartbeatsTable,
|
|
2657
|
+
servicesRegistryTable,
|
|
2658
|
+
touchRunnerHeartbeat,
|
|
2659
|
+
touchServicesRegistry,
|
|
2660
|
+
unregisterRunnerHeartbeat,
|
|
2661
|
+
unregisterServicesRegistry,
|
|
2662
|
+
updateServicesRegistryMetadata,
|
|
2292
2663
|
updateTaskProgress,
|
|
2293
2664
|
waitForTaskResult
|
|
2294
2665
|
});
|