@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.js
CHANGED
|
@@ -79,6 +79,12 @@ function toJsonColumn(value) {
|
|
|
79
79
|
return JSON.stringify(value);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// src/tasks/servicesRegistry.ts
|
|
83
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
84
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
85
|
+
import os from "os";
|
|
86
|
+
import path3 from "path";
|
|
87
|
+
|
|
82
88
|
// src/tasks/taskUtils.ts
|
|
83
89
|
import { randomUUID } from "crypto";
|
|
84
90
|
function getDb(context) {
|
|
@@ -97,6 +103,12 @@ function queueToTableNames(queue) {
|
|
|
97
103
|
historyTable: `${queue}_history`
|
|
98
104
|
};
|
|
99
105
|
}
|
|
106
|
+
function servicesRegistryTable(queue) {
|
|
107
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
|
|
108
|
+
throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
|
|
109
|
+
}
|
|
110
|
+
return `${queue}_services_registry`;
|
|
111
|
+
}
|
|
100
112
|
async function ensureTaskTables(context, options = {}) {
|
|
101
113
|
const queue = options.queue ?? "tasks";
|
|
102
114
|
const recreate = options.recreate ?? false;
|
|
@@ -133,18 +145,6 @@ async function ensureTaskTables(context, options = {}) {
|
|
|
133
145
|
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
134
146
|
});
|
|
135
147
|
}
|
|
136
|
-
const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
|
|
137
|
-
if (!tasksHasOpid) {
|
|
138
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
139
|
-
t.text("opid");
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
|
|
143
|
-
if (!tasksHasPausedAt) {
|
|
144
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
145
|
-
t.timestamp("paused_at").defaultTo(null);
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
148
|
if (needsHistory) {
|
|
149
149
|
await db.schema.createTable(historyTable, (t) => {
|
|
150
150
|
t.uuid("id").notNullable();
|
|
@@ -167,10 +167,24 @@ async function ensureTaskTables(context, options = {}) {
|
|
|
167
167
|
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
const registryTable = servicesRegistryTable(queue);
|
|
171
|
+
const needsRegistry = !await db.tableExists(registryTable);
|
|
172
|
+
if (needsRegistry) {
|
|
173
|
+
await db.schema.createTable(registryTable, (t) => {
|
|
174
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
175
|
+
t.uuid("instance_id").notNullable().unique();
|
|
176
|
+
t.text("queue").notNullable();
|
|
177
|
+
t.text("service_group").notNullable();
|
|
178
|
+
t.text("service_name").notNullable();
|
|
179
|
+
t.text("target").notNullable();
|
|
180
|
+
t.text("hostname");
|
|
181
|
+
t.integer("pid");
|
|
182
|
+
t.json("metadata");
|
|
183
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
184
|
+
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
185
|
+
t.unique(["queue", "service_name"], `${registryTable}_queue_service_name_uniq`);
|
|
186
|
+
t.index(["queue", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
187
|
+
t.index(["queue", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
174
188
|
});
|
|
175
189
|
}
|
|
176
190
|
}
|
|
@@ -197,9 +211,263 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
|
197
211
|
});
|
|
198
212
|
}
|
|
199
213
|
|
|
214
|
+
// src/tasks/servicesRegistry.ts
|
|
215
|
+
function getDb2(context) {
|
|
216
|
+
const db = context.db;
|
|
217
|
+
if (!db) {
|
|
218
|
+
throw new Error("Services registry requires context.db");
|
|
219
|
+
}
|
|
220
|
+
return db;
|
|
221
|
+
}
|
|
222
|
+
function parseMetadataColumn(value) {
|
|
223
|
+
if (!value) return {};
|
|
224
|
+
if (typeof value === "object" && !Array.isArray(value)) return value;
|
|
225
|
+
if (typeof value === "string") {
|
|
226
|
+
try {
|
|
227
|
+
const p = JSON.parse(value);
|
|
228
|
+
return p && typeof p === "object" && !Array.isArray(p) ? p : {};
|
|
229
|
+
} catch {
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return {};
|
|
234
|
+
}
|
|
235
|
+
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
236
|
+
intake: 1,
|
|
237
|
+
harvest: 1,
|
|
238
|
+
loader: 0,
|
|
239
|
+
photos: 0,
|
|
240
|
+
photosprocessor: 0,
|
|
241
|
+
ingest: 0
|
|
242
|
+
};
|
|
243
|
+
function sanitizeNamePart(raw) {
|
|
244
|
+
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
245
|
+
return s.slice(0, 80) || "runner";
|
|
246
|
+
}
|
|
247
|
+
function identityFilePath(identityDir, queue, serviceGroup) {
|
|
248
|
+
const safeQ = sanitizeNamePart(queue);
|
|
249
|
+
const safeG = sanitizeNamePart(serviceGroup);
|
|
250
|
+
return path3.join(identityDir, `${safeQ}_${safeG}.json`);
|
|
251
|
+
}
|
|
252
|
+
async function readIdentityFile(filePath) {
|
|
253
|
+
try {
|
|
254
|
+
const text = await readFile(filePath, "utf8");
|
|
255
|
+
const parsed = JSON.parse(text);
|
|
256
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
257
|
+
} catch {
|
|
258
|
+
return {};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async function writeIdentityFile(filePath, data) {
|
|
262
|
+
await mkdir(path3.dirname(filePath), { recursive: true });
|
|
263
|
+
await writeFile(filePath, `${JSON.stringify(data, null, 2)}
|
|
264
|
+
`, "utf8");
|
|
265
|
+
}
|
|
266
|
+
function resolveMaxInstances(serviceGroup, override) {
|
|
267
|
+
if (override !== void 0 && Number.isFinite(override)) {
|
|
268
|
+
return Math.max(0, Math.floor(Number(override)));
|
|
269
|
+
}
|
|
270
|
+
const g = serviceGroup.trim().toLowerCase();
|
|
271
|
+
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
272
|
+
}
|
|
273
|
+
async function countAliveInGroup(db, registryTable, queue, serviceGroup, staleMs, excludeInstanceId) {
|
|
274
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
275
|
+
let q = db(registryTable).where({ queue, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
|
|
276
|
+
if (excludeInstanceId) {
|
|
277
|
+
q = q.whereNot("instance_id", excludeInstanceId);
|
|
278
|
+
}
|
|
279
|
+
const row = await q.count("id as count").first();
|
|
280
|
+
return Number(row?.count ?? 0);
|
|
281
|
+
}
|
|
282
|
+
function isUniqueViolation(error) {
|
|
283
|
+
const code = error?.code ?? error?.errno;
|
|
284
|
+
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
285
|
+
}
|
|
286
|
+
async function registerInServicesRegistry(context, options) {
|
|
287
|
+
const db = getDb2(context);
|
|
288
|
+
const registryTable = servicesRegistryTable(options.queue);
|
|
289
|
+
const serviceGroup = options.serviceGroup.trim();
|
|
290
|
+
if (!serviceGroup) {
|
|
291
|
+
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
292
|
+
}
|
|
293
|
+
const identityPath = identityFilePath(options.identityDir, options.queue, serviceGroup);
|
|
294
|
+
let identity = await readIdentityFile(identityPath);
|
|
295
|
+
let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : randomUUID2();
|
|
296
|
+
identity.instanceId = instanceId;
|
|
297
|
+
await writeIdentityFile(identityPath, identity);
|
|
298
|
+
const hostname = os.hostname();
|
|
299
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
300
|
+
const meta = toJsonColumn(options.metadata ?? null);
|
|
301
|
+
const existing = await db(registryTable).where({ instance_id: instanceId }).first();
|
|
302
|
+
if (existing) {
|
|
303
|
+
await db(registryTable).where({ instance_id: instanceId }).update({
|
|
304
|
+
target: options.target,
|
|
305
|
+
hostname,
|
|
306
|
+
pid,
|
|
307
|
+
metadata: meta,
|
|
308
|
+
last_seen_at: db.fn.now()
|
|
309
|
+
});
|
|
310
|
+
const serviceName = String(existing.service_name);
|
|
311
|
+
identity.serviceName = serviceName;
|
|
312
|
+
await writeIdentityFile(identityPath, identity);
|
|
313
|
+
const reg = {
|
|
314
|
+
instanceId,
|
|
315
|
+
serviceName,
|
|
316
|
+
serviceGroup,
|
|
317
|
+
queue: options.queue,
|
|
318
|
+
target: options.target,
|
|
319
|
+
rowId: String(existing.id)
|
|
320
|
+
};
|
|
321
|
+
context.servicesRegistry = reg;
|
|
322
|
+
context.runnerHeartbeat = reg;
|
|
323
|
+
context.logger.info?.(
|
|
324
|
+
`[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
|
|
325
|
+
);
|
|
326
|
+
return {
|
|
327
|
+
instanceId,
|
|
328
|
+
serviceName,
|
|
329
|
+
serviceGroup,
|
|
330
|
+
queue: options.queue,
|
|
331
|
+
target: options.target,
|
|
332
|
+
rowId: String(existing.id),
|
|
333
|
+
registryTable
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
|
|
337
|
+
const aliveOthers = await countAliveInGroup(
|
|
338
|
+
db,
|
|
339
|
+
registryTable,
|
|
340
|
+
options.queue,
|
|
341
|
+
serviceGroup,
|
|
342
|
+
options.staleMs,
|
|
343
|
+
instanceId
|
|
344
|
+
);
|
|
345
|
+
if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
|
|
346
|
+
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
|
|
347
|
+
if (options.enforceMaxInstances) {
|
|
348
|
+
throw new Error(msg);
|
|
349
|
+
}
|
|
350
|
+
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
351
|
+
}
|
|
352
|
+
const explicitName = options.serviceName?.trim();
|
|
353
|
+
const fromFile = typeof identity.serviceName === "string" ? identity.serviceName.trim() : "";
|
|
354
|
+
const hostBase = sanitizeNamePart(hostname);
|
|
355
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
356
|
+
const baseCandidates = [];
|
|
357
|
+
if (explicitName) baseCandidates.push(sanitizeNamePart(explicitName));
|
|
358
|
+
if (fromFile) baseCandidates.push(sanitizeNamePart(fromFile));
|
|
359
|
+
baseCandidates.push(`${groupBase}-${hostBase}`);
|
|
360
|
+
baseCandidates.push(groupBase);
|
|
361
|
+
function* eachServiceNameCandidate(bases) {
|
|
362
|
+
const seen = /* @__PURE__ */ new Set();
|
|
363
|
+
for (const rawBase of bases) {
|
|
364
|
+
const base = sanitizeNamePart(rawBase);
|
|
365
|
+
if (!base) continue;
|
|
366
|
+
const seq = [base];
|
|
367
|
+
for (let n = 2; n <= 500; n++) seq.push(`${base}-${n}`);
|
|
368
|
+
for (const c of seq) {
|
|
369
|
+
if (seen.has(c)) continue;
|
|
370
|
+
seen.add(c);
|
|
371
|
+
yield c;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
let inserted;
|
|
376
|
+
for (const candidate of eachServiceNameCandidate(baseCandidates)) {
|
|
377
|
+
try {
|
|
378
|
+
const rows = await db(registryTable).insert({
|
|
379
|
+
instance_id: instanceId,
|
|
380
|
+
queue: options.queue,
|
|
381
|
+
service_group: serviceGroup,
|
|
382
|
+
service_name: candidate,
|
|
383
|
+
target: options.target,
|
|
384
|
+
hostname,
|
|
385
|
+
pid,
|
|
386
|
+
metadata: meta,
|
|
387
|
+
last_seen_at: db.fn.now()
|
|
388
|
+
}).returning(["id", "service_name"]);
|
|
389
|
+
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
390
|
+
if (row) {
|
|
391
|
+
inserted = { id: String(row.id), service_name: String(row.service_name) };
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (!isUniqueViolation(error)) {
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (!inserted) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
`[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
identity.serviceName = inserted.service_name;
|
|
406
|
+
await writeIdentityFile(identityPath, identity);
|
|
407
|
+
const regNew = {
|
|
408
|
+
instanceId,
|
|
409
|
+
serviceName: inserted.service_name,
|
|
410
|
+
serviceGroup,
|
|
411
|
+
queue: options.queue,
|
|
412
|
+
target: options.target,
|
|
413
|
+
rowId: inserted.id
|
|
414
|
+
};
|
|
415
|
+
context.servicesRegistry = regNew;
|
|
416
|
+
context.runnerHeartbeat = regNew;
|
|
417
|
+
context.logger.info?.(
|
|
418
|
+
`[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
|
|
419
|
+
);
|
|
420
|
+
return {
|
|
421
|
+
instanceId,
|
|
422
|
+
serviceName: inserted.service_name,
|
|
423
|
+
serviceGroup,
|
|
424
|
+
queue: options.queue,
|
|
425
|
+
target: options.target,
|
|
426
|
+
rowId: inserted.id,
|
|
427
|
+
registryTable
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async function touchServicesRegistry(context, registration) {
|
|
431
|
+
const db = getDb2(context);
|
|
432
|
+
const hostname = os.hostname();
|
|
433
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
434
|
+
await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
|
|
435
|
+
last_seen_at: db.fn.now(),
|
|
436
|
+
hostname,
|
|
437
|
+
pid
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
async function updateServicesRegistryMetadata(context, registration, patch) {
|
|
441
|
+
const db = getDb2(context);
|
|
442
|
+
const row = await db(registration.registryTable).where({ instance_id: registration.instanceId }).first();
|
|
443
|
+
const prev = parseMetadataColumn(row?.metadata);
|
|
444
|
+
const merged = { ...prev, ...patch };
|
|
445
|
+
await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
|
|
446
|
+
metadata: toJsonColumn(merged),
|
|
447
|
+
last_seen_at: db.fn.now()
|
|
448
|
+
});
|
|
449
|
+
context.logger.info?.(`[services-registry] metadata updated for ${registration.serviceName}`);
|
|
450
|
+
}
|
|
451
|
+
async function unregisterServicesRegistry(context, registration) {
|
|
452
|
+
const db = getDb2(context);
|
|
453
|
+
await db(registration.registryTable).where({ instance_id: registration.instanceId }).delete();
|
|
454
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} instance_id=${registration.instanceId}`);
|
|
455
|
+
}
|
|
456
|
+
async function listServicesRegistry(context, options = { queue: "tasks" }) {
|
|
457
|
+
const db = getDb2(context);
|
|
458
|
+
const staleMs = options.staleMs ?? 6e4;
|
|
459
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
460
|
+
const table = servicesRegistryTable(options.queue);
|
|
461
|
+
let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
|
|
462
|
+
if (options.serviceGroup?.trim()) {
|
|
463
|
+
q = q.where({ service_group: options.serviceGroup.trim() });
|
|
464
|
+
}
|
|
465
|
+
return await q;
|
|
466
|
+
}
|
|
467
|
+
|
|
200
468
|
// src/filedatabase/index.ts
|
|
201
469
|
import fs3 from "fs";
|
|
202
|
-
import
|
|
470
|
+
import path4 from "path";
|
|
203
471
|
|
|
204
472
|
// src/filedatabase/serializers.ts
|
|
205
473
|
function detectDataType(data) {
|
|
@@ -346,7 +614,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
346
614
|
if (this.versioned && version) {
|
|
347
615
|
parts.push(version);
|
|
348
616
|
}
|
|
349
|
-
return
|
|
617
|
+
return path4.resolve(...parts);
|
|
350
618
|
}
|
|
351
619
|
/**
|
|
352
620
|
* Set current version and version folder
|
|
@@ -383,7 +651,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
383
651
|
this.currentFileNumber = 0;
|
|
384
652
|
const versions = await this.getVersions();
|
|
385
653
|
while (versions.length > this.maxVersions) {
|
|
386
|
-
const versionToDelete =
|
|
654
|
+
const versionToDelete = path4.resolve(this.getDestinationPath(), versions.shift());
|
|
387
655
|
this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
|
|
388
656
|
await fs3.promises.rm(versionToDelete, { recursive: true, force: true });
|
|
389
657
|
}
|
|
@@ -402,7 +670,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
402
670
|
await ensurePath(destPath);
|
|
403
671
|
const items = await fs3.promises.readdir(destPath);
|
|
404
672
|
const versions = items.filter((item) => {
|
|
405
|
-
const itemPath =
|
|
673
|
+
const itemPath = path4.join(destPath, item);
|
|
406
674
|
const stat = fs3.statSync(itemPath);
|
|
407
675
|
return stat.isDirectory() && isTimestampFolder(item);
|
|
408
676
|
});
|
|
@@ -459,7 +727,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
459
727
|
const items = await fs3.promises.readdir(tablePath);
|
|
460
728
|
if (items.includes("metadata.json")) {
|
|
461
729
|
const metadata = JSON.parse(
|
|
462
|
-
await fs3.promises.readFile(
|
|
730
|
+
await fs3.promises.readFile(path4.join(tablePath, "metadata.json"), "utf8")
|
|
463
731
|
);
|
|
464
732
|
return {
|
|
465
733
|
versioned: false,
|
|
@@ -468,13 +736,13 @@ var FileDatabase = class _FileDatabase {
|
|
|
468
736
|
};
|
|
469
737
|
}
|
|
470
738
|
const versionFolders = items.filter((item) => {
|
|
471
|
-
const itemPath =
|
|
739
|
+
const itemPath = path4.join(tablePath, item);
|
|
472
740
|
const stat = fs3.statSync(itemPath);
|
|
473
741
|
return stat.isDirectory() && isTimestampFolder(item);
|
|
474
742
|
});
|
|
475
743
|
if (versionFolders.length > 0) {
|
|
476
744
|
const latestVersion = versionFolders.sort().pop();
|
|
477
|
-
const versionMetadataPath =
|
|
745
|
+
const versionMetadataPath = path4.join(tablePath, latestVersion, "metadata.json");
|
|
478
746
|
return {
|
|
479
747
|
versioned: true,
|
|
480
748
|
hasMetadata: fs3.existsSync(versionMetadataPath),
|
|
@@ -495,7 +763,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
495
763
|
* Load metadata from JSON file
|
|
496
764
|
*/
|
|
497
765
|
async loadMetadataJson(version) {
|
|
498
|
-
const metadataFile =
|
|
766
|
+
const metadataFile = path4.join(this.getDestinationPath(), version, "metadata.json");
|
|
499
767
|
if (fs3.existsSync(metadataFile)) {
|
|
500
768
|
try {
|
|
501
769
|
const rawData = await fs3.promises.readFile(metadataFile, "utf8");
|
|
@@ -511,7 +779,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
511
779
|
* Reads all files to get accurate counts - used when synopsis calculation is needed
|
|
512
780
|
*/
|
|
513
781
|
async figureMetadataFromVersionFiles(version) {
|
|
514
|
-
const versionPath =
|
|
782
|
+
const versionPath = path4.join(this.getDestinationPath(), version);
|
|
515
783
|
if (!fs3.existsSync(versionPath)) {
|
|
516
784
|
return this.getDefaultMetadata();
|
|
517
785
|
}
|
|
@@ -523,10 +791,10 @@ var FileDatabase = class _FileDatabase {
|
|
|
523
791
|
let detectedDataType = null;
|
|
524
792
|
for (let i = 0; i < files.length; i++) {
|
|
525
793
|
const fileName = files[i];
|
|
526
|
-
const filePath =
|
|
794
|
+
const filePath = path4.join(versionPath, fileName);
|
|
527
795
|
try {
|
|
528
796
|
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
529
|
-
const extension =
|
|
797
|
+
const extension = path4.extname(fileName).toLowerCase();
|
|
530
798
|
let dataType = "text";
|
|
531
799
|
if (extension === ".json") {
|
|
532
800
|
dataType = "json-array";
|
|
@@ -559,7 +827,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
559
827
|
* Much faster for large datasets with many files
|
|
560
828
|
*/
|
|
561
829
|
async buildMetadataOptimized(version) {
|
|
562
|
-
const versionPath =
|
|
830
|
+
const versionPath = path4.join(this.getDestinationPath(), version);
|
|
563
831
|
if (!fs3.existsSync(versionPath)) {
|
|
564
832
|
return this.getDefaultMetadata();
|
|
565
833
|
}
|
|
@@ -575,7 +843,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
575
843
|
fileName
|
|
576
844
|
}));
|
|
577
845
|
const firstFile = metadata.files[0];
|
|
578
|
-
const firstFilePath =
|
|
846
|
+
const firstFilePath = path4.join(versionPath, firstFile.fileName);
|
|
579
847
|
const firstFileRaw = await fs3.promises.readFile(firstFilePath, "utf8");
|
|
580
848
|
let firstFileData;
|
|
581
849
|
try {
|
|
@@ -592,7 +860,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
592
860
|
}
|
|
593
861
|
if (files.length > 1) {
|
|
594
862
|
const lastFile = metadata.files[metadata.files.length - 1];
|
|
595
|
-
const lastFilePath =
|
|
863
|
+
const lastFilePath = path4.join(versionPath, lastFile.fileName);
|
|
596
864
|
const lastFileRaw = await fs3.promises.readFile(lastFilePath, "utf8");
|
|
597
865
|
const lastFileData = deserializeData(lastFileRaw, metadata.dataType);
|
|
598
866
|
lastFile.recordsCount = Array.isArray(lastFileData) ? lastFileData.length : 1;
|
|
@@ -643,9 +911,9 @@ var FileDatabase = class _FileDatabase {
|
|
|
643
911
|
if (!this.currentVersion) {
|
|
644
912
|
return;
|
|
645
913
|
}
|
|
646
|
-
metadataFile =
|
|
914
|
+
metadataFile = path4.join(this.getDestinationPath(), this.currentVersion, "metadata.json");
|
|
647
915
|
} else {
|
|
648
|
-
metadataFile =
|
|
916
|
+
metadataFile = path4.join(this.getDestinationPath(), "metadata.json");
|
|
649
917
|
}
|
|
650
918
|
await fs3.promises.writeFile(metadataFile, JSON.stringify(metadataToSave, null, 4), "utf8");
|
|
651
919
|
}
|
|
@@ -708,7 +976,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
708
976
|
}
|
|
709
977
|
}
|
|
710
978
|
if (!Array.isArray(data) && !forceNewFile) {
|
|
711
|
-
const lastFileExtension =
|
|
979
|
+
const lastFileExtension = path4.extname(lastFile.fileName);
|
|
712
980
|
const expectedExtension = `.${getFileExtension(incomingDataType)}`;
|
|
713
981
|
if (lastFileExtension !== expectedExtension) {
|
|
714
982
|
if (lastFileRecordsCount > 0) {
|
|
@@ -718,7 +986,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
718
986
|
}
|
|
719
987
|
}
|
|
720
988
|
} else if (!Array.isArray(data) && forceNewFile) {
|
|
721
|
-
const lastFileExtension =
|
|
989
|
+
const lastFileExtension = path4.extname(lastFile.fileName);
|
|
722
990
|
const expectedExtension = `.${getFileExtension(incomingDataType)}`;
|
|
723
991
|
if (lastFileExtension !== expectedExtension) {
|
|
724
992
|
lastFile.fileName = `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(incomingDataType)}`;
|
|
@@ -808,7 +1076,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
808
1076
|
*/
|
|
809
1077
|
async safeWrite(filePath, data) {
|
|
810
1078
|
const serializedData = serializeData(data);
|
|
811
|
-
const dir =
|
|
1079
|
+
const dir = path4.dirname(filePath);
|
|
812
1080
|
const requiredBytes = Buffer.byteLength(serializedData, "utf8");
|
|
813
1081
|
const freeBytes = getFreeDiskSpace(dir);
|
|
814
1082
|
if (freeBytes !== null) {
|
|
@@ -853,7 +1121,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
853
1121
|
} else {
|
|
854
1122
|
await ensurePath(this.getDestinationPath());
|
|
855
1123
|
if (this.useMetadata === true) {
|
|
856
|
-
const metadataPath =
|
|
1124
|
+
const metadataPath = path4.join(this.getDestinationPath(), "metadata.json");
|
|
857
1125
|
if (fs3.existsSync(metadataPath)) {
|
|
858
1126
|
try {
|
|
859
1127
|
const rawData = await fs3.promises.readFile(metadataPath, "utf8");
|
|
@@ -906,7 +1174,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
906
1174
|
}
|
|
907
1175
|
if (this.useMetadata) {
|
|
908
1176
|
const destPath = this.getDestinationPath();
|
|
909
|
-
const metadataPath =
|
|
1177
|
+
const metadataPath = path4.join(destPath, "metadata.json");
|
|
910
1178
|
if (fs3.existsSync(metadataPath)) {
|
|
911
1179
|
try {
|
|
912
1180
|
const rawData = await fs3.promises.readFile(metadataPath, "utf8");
|
|
@@ -942,7 +1210,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
942
1210
|
if (options.filename) {
|
|
943
1211
|
const destPath2 = this.getDestinationPath();
|
|
944
1212
|
await ensurePath(destPath2);
|
|
945
|
-
const filePath =
|
|
1213
|
+
const filePath = path4.join(destPath2, options.filename);
|
|
946
1214
|
await this.safeWrite(filePath, data);
|
|
947
1215
|
return;
|
|
948
1216
|
}
|
|
@@ -991,11 +1259,11 @@ var FileDatabase = class _FileDatabase {
|
|
|
991
1259
|
const forceNewFile = hasCustomMetadata && targetFileIndex === null;
|
|
992
1260
|
let { dataToWrite, dataLeftOver, fileName } = this.figureOutDataAndFileToWrite(data, targetFileIndex, forceNewFile);
|
|
993
1261
|
const destPath = this.getDestinationPath(this.currentVersion || void 0);
|
|
994
|
-
await this.safeWrite(
|
|
1262
|
+
await this.safeWrite(path4.join(destPath, fileName), dataToWrite);
|
|
995
1263
|
this.updateMetadata(dataToWrite, fileName, options.customMetadata);
|
|
996
1264
|
while (dataLeftOver && dataLeftOver.length > 0 && targetFileIndex === null) {
|
|
997
1265
|
const writeContext = this.figureOutDataAndFileToWrite(dataLeftOver);
|
|
998
|
-
await this.safeWrite(
|
|
1266
|
+
await this.safeWrite(path4.join(destPath, writeContext.fileName), writeContext.dataToWrite);
|
|
999
1267
|
this.updateMetadata(writeContext.dataToWrite, writeContext.fileName, options.customMetadata);
|
|
1000
1268
|
dataLeftOver = writeContext.dataLeftOver;
|
|
1001
1269
|
}
|
|
@@ -1011,7 +1279,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
1011
1279
|
const { version, nextPage = false, pageSize, filename } = options;
|
|
1012
1280
|
if (filename) {
|
|
1013
1281
|
const destPath = this.getDestinationPath(version);
|
|
1014
|
-
const filePath =
|
|
1282
|
+
const filePath = path4.join(destPath, filename);
|
|
1015
1283
|
try {
|
|
1016
1284
|
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
1017
1285
|
return JSON.parse(rawData);
|
|
@@ -1023,7 +1291,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
1023
1291
|
const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
|
|
1024
1292
|
if (isNonPaginatedData) {
|
|
1025
1293
|
const file = this.metadata.files[0];
|
|
1026
|
-
const filePath =
|
|
1294
|
+
const filePath = path4.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
|
|
1027
1295
|
try {
|
|
1028
1296
|
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
1029
1297
|
return deserializeData(rawData, this.metadata.dataType);
|
|
@@ -1061,7 +1329,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
1061
1329
|
let cumulativeRecords = currentFileOffset;
|
|
1062
1330
|
for (let i = currentFileIndex; i < this.metadata.files.length && recordsRead < effectivePageSize; i++) {
|
|
1063
1331
|
const file = this.metadata.files[i];
|
|
1064
|
-
const filePath =
|
|
1332
|
+
const filePath = path4.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
|
|
1065
1333
|
try {
|
|
1066
1334
|
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
1067
1335
|
const fileData = deserializeData(rawData, this.metadata.dataType);
|
|
@@ -1105,7 +1373,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
1105
1373
|
* Returns data file names (.json, .txt, .xml) excluding metadata.json.
|
|
1106
1374
|
*/
|
|
1107
1375
|
async listFilenames() {
|
|
1108
|
-
const destPath = this.versioned && this.currentVersion ?
|
|
1376
|
+
const destPath = this.versioned && this.currentVersion ? path4.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
1109
1377
|
try {
|
|
1110
1378
|
const entries = await fs3.promises.readdir(destPath, { withFileTypes: true });
|
|
1111
1379
|
return entries.filter((e) => e.isFile() && e.name !== "metadata.json" && /\.(json|txt|xml)$/i.test(e.name)).map((e) => e.name);
|
|
@@ -1119,8 +1387,8 @@ var FileDatabase = class _FileDatabase {
|
|
|
1119
1387
|
* Use with listFilenames() to manage individual files.
|
|
1120
1388
|
*/
|
|
1121
1389
|
async removeFile(filename) {
|
|
1122
|
-
const destPath = this.versioned && this.currentVersion ?
|
|
1123
|
-
const filePath =
|
|
1390
|
+
const destPath = this.versioned && this.currentVersion ? path4.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
1391
|
+
const filePath = path4.join(destPath, filename);
|
|
1124
1392
|
try {
|
|
1125
1393
|
await fs3.promises.unlink(filePath);
|
|
1126
1394
|
} catch (err) {
|
|
@@ -1146,7 +1414,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
1146
1414
|
this.metadata.files.splice(idx, 1);
|
|
1147
1415
|
this.metadata.totalRecords = Math.max(0, (this.metadata.totalRecords || 0) - recordsCount);
|
|
1148
1416
|
const destPath = this.getDestinationPath();
|
|
1149
|
-
const filePath =
|
|
1417
|
+
const filePath = path4.join(destPath, filename);
|
|
1150
1418
|
try {
|
|
1151
1419
|
await fs3.promises.unlink(filePath);
|
|
1152
1420
|
} catch (err) {
|
|
@@ -1202,7 +1470,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
1202
1470
|
});
|
|
1203
1471
|
if (matches) {
|
|
1204
1472
|
const destPath = this.getDestinationPath();
|
|
1205
|
-
const filePath =
|
|
1473
|
+
const filePath = path4.join(destPath, fileEntry.fileName);
|
|
1206
1474
|
const fileData = await fs3.promises.readFile(filePath, "utf8");
|
|
1207
1475
|
const data = deserializeData(fileData, metadata.dataType || "json-object");
|
|
1208
1476
|
results.push({
|
|
@@ -1225,7 +1493,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
1225
1493
|
});
|
|
1226
1494
|
if (matches) {
|
|
1227
1495
|
const destPath = this.getDestinationPath(version);
|
|
1228
|
-
const filePath =
|
|
1496
|
+
const filePath = path4.join(destPath, fileEntry.fileName);
|
|
1229
1497
|
const fileData = await fs3.promises.readFile(filePath, "utf8");
|
|
1230
1498
|
const data = deserializeData(fileData, metadata.dataType || "json-object");
|
|
1231
1499
|
results.push({
|
|
@@ -1585,7 +1853,7 @@ var TaskShellCommand = class extends TaskMaster {
|
|
|
1585
1853
|
};
|
|
1586
1854
|
|
|
1587
1855
|
// src/tasks/coreTasks/TaskSystemInfo.ts
|
|
1588
|
-
import
|
|
1856
|
+
import os2 from "os";
|
|
1589
1857
|
import fs4 from "fs/promises";
|
|
1590
1858
|
function toGb(valueBytes) {
|
|
1591
1859
|
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
@@ -1607,10 +1875,10 @@ async function getDiskStats() {
|
|
|
1607
1875
|
var TaskSystemInfo = class extends TaskMaster {
|
|
1608
1876
|
async run() {
|
|
1609
1877
|
try {
|
|
1610
|
-
const totalMemory =
|
|
1611
|
-
const freeMemory =
|
|
1878
|
+
const totalMemory = os2.totalmem();
|
|
1879
|
+
const freeMemory = os2.freemem();
|
|
1612
1880
|
const usedMemory = totalMemory - freeMemory;
|
|
1613
|
-
const cpus =
|
|
1881
|
+
const cpus = os2.cpus();
|
|
1614
1882
|
const cpuUtilization = cpus.map((cpu) => {
|
|
1615
1883
|
const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
|
|
1616
1884
|
const usage = (total - cpu.times.idle) / total * 100;
|
|
@@ -1636,10 +1904,10 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
1636
1904
|
utilization: cpuUtilization
|
|
1637
1905
|
},
|
|
1638
1906
|
runtime: {
|
|
1639
|
-
platform:
|
|
1640
|
-
arch:
|
|
1641
|
-
uptimeSec:
|
|
1642
|
-
hostname:
|
|
1907
|
+
platform: os2.platform(),
|
|
1908
|
+
arch: os2.arch(),
|
|
1909
|
+
uptimeSec: os2.uptime(),
|
|
1910
|
+
hostname: os2.hostname()
|
|
1643
1911
|
}
|
|
1644
1912
|
};
|
|
1645
1913
|
this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
|
|
@@ -1872,7 +2140,7 @@ async function runNodeTaskScript(context, options) {
|
|
|
1872
2140
|
// src/tasks/index.ts
|
|
1873
2141
|
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
1874
2142
|
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
1875
|
-
function
|
|
2143
|
+
function getDb3(context) {
|
|
1876
2144
|
const db = context.db;
|
|
1877
2145
|
if (!db) {
|
|
1878
2146
|
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
@@ -1916,7 +2184,7 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
|
|
|
1916
2184
|
context.emitter.emit("stop", allowanceMs);
|
|
1917
2185
|
}
|
|
1918
2186
|
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
1919
|
-
const db =
|
|
2187
|
+
const db = getDb3(context);
|
|
1920
2188
|
const taskName = row.task;
|
|
1921
2189
|
const TaskClass = registry.get(taskName);
|
|
1922
2190
|
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
@@ -2017,7 +2285,7 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
2017
2285
|
return { stopRunnerRequested, stopAllowanceMs };
|
|
2018
2286
|
}
|
|
2019
2287
|
async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
|
|
2020
|
-
const db =
|
|
2288
|
+
const db = getDb3(context);
|
|
2021
2289
|
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);
|
|
2022
2290
|
if (taskNames && taskNames.length > 0) {
|
|
2023
2291
|
query = query.whereIn("task", taskNames);
|
|
@@ -2063,25 +2331,76 @@ async function runTasksLoop(context, options) {
|
|
|
2063
2331
|
let stopRequested = false;
|
|
2064
2332
|
let stopAllowanceMs = 5e3;
|
|
2065
2333
|
context.__tasksRunnerStop = false;
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
)
|
|
2076
|
-
|
|
2077
|
-
|
|
2334
|
+
let registryReg = null;
|
|
2335
|
+
let registryInterval = null;
|
|
2336
|
+
const hbGroup = options.runnerServiceGroup?.trim();
|
|
2337
|
+
if (hbGroup) {
|
|
2338
|
+
const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
|
|
2339
|
+
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
2340
|
+
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
2341
|
+
const defaultMeta = {
|
|
2342
|
+
component: "tasks-runner",
|
|
2343
|
+
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
2344
|
+
};
|
|
2345
|
+
registryReg = await registerInServicesRegistry(context, {
|
|
2346
|
+
queue,
|
|
2347
|
+
target,
|
|
2348
|
+
serviceGroup: hbGroup,
|
|
2349
|
+
serviceName: options.runnerServiceName,
|
|
2350
|
+
identityDir,
|
|
2351
|
+
staleMs,
|
|
2352
|
+
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
2353
|
+
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
2354
|
+
metadata: options.runnerMetadata ?? defaultMeta
|
|
2355
|
+
});
|
|
2356
|
+
registryInterval = setInterval(() => {
|
|
2357
|
+
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
2358
|
+
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
2359
|
+
});
|
|
2360
|
+
}, hbIntervalMs);
|
|
2361
|
+
}
|
|
2362
|
+
try {
|
|
2363
|
+
while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
|
|
2364
|
+
if (!runningStopControlPromise) {
|
|
2365
|
+
const claimedStopTask = await claimNextRunnableTask(
|
|
2366
|
+
context,
|
|
2367
|
+
tasksTable,
|
|
2368
|
+
target,
|
|
2369
|
+
registry,
|
|
2370
|
+
10,
|
|
2371
|
+
["stopRunner", "stop"]
|
|
2372
|
+
);
|
|
2373
|
+
if (claimedStopTask) {
|
|
2374
|
+
runningStopControlPromise = executeClaimedTask(
|
|
2375
|
+
context,
|
|
2376
|
+
tasksTable,
|
|
2377
|
+
historyTable,
|
|
2378
|
+
claimedStopTask,
|
|
2379
|
+
registry,
|
|
2380
|
+
runningTaskInstances
|
|
2381
|
+
).then(async (outcome) => {
|
|
2382
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
2383
|
+
stopRequested = true;
|
|
2384
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
2385
|
+
context.__tasksRunnerStop = true;
|
|
2386
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
2387
|
+
}
|
|
2388
|
+
}).finally(() => {
|
|
2389
|
+
runningStopControlPromise = null;
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
while (runningPromises.size < maxParallel) {
|
|
2394
|
+
const claimed = await claimNextRunnableTask(
|
|
2078
2395
|
context,
|
|
2079
2396
|
tasksTable,
|
|
2080
|
-
|
|
2081
|
-
claimedStopTask,
|
|
2397
|
+
target,
|
|
2082
2398
|
registry,
|
|
2083
|
-
|
|
2084
|
-
|
|
2399
|
+
scanLimit,
|
|
2400
|
+
allowedTasks
|
|
2401
|
+
);
|
|
2402
|
+
if (!claimed) break;
|
|
2403
|
+
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
2085
2404
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
2086
2405
|
stopRequested = true;
|
|
2087
2406
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
@@ -2089,54 +2408,46 @@ async function runTasksLoop(context, options) {
|
|
|
2089
2408
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
2090
2409
|
}
|
|
2091
2410
|
}).finally(() => {
|
|
2092
|
-
|
|
2411
|
+
runningPromises.delete(p);
|
|
2093
2412
|
});
|
|
2413
|
+
runningPromises.add(p);
|
|
2414
|
+
}
|
|
2415
|
+
await sleepMs(pollMs);
|
|
2416
|
+
}
|
|
2417
|
+
if (context.isStop() && !stopRequested) {
|
|
2418
|
+
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
2419
|
+
}
|
|
2420
|
+
if (runningPromises.size > 0) {
|
|
2421
|
+
if (stopRequested) {
|
|
2422
|
+
await Promise.race([
|
|
2423
|
+
Promise.allSettled(Array.from(runningPromises)),
|
|
2424
|
+
sleepMs(stopAllowanceMs).then(() => {
|
|
2425
|
+
context.logger.warn?.(
|
|
2426
|
+
`[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
|
|
2427
|
+
);
|
|
2428
|
+
})
|
|
2429
|
+
]);
|
|
2430
|
+
} else {
|
|
2431
|
+
await Promise.allSettled(Array.from(runningPromises));
|
|
2094
2432
|
}
|
|
2095
2433
|
}
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
target,
|
|
2101
|
-
registry,
|
|
2102
|
-
scanLimit,
|
|
2103
|
-
allowedTasks
|
|
2104
|
-
);
|
|
2105
|
-
if (!claimed) break;
|
|
2106
|
-
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
2107
|
-
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
2108
|
-
stopRequested = true;
|
|
2109
|
-
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
2110
|
-
context.__tasksRunnerStop = true;
|
|
2111
|
-
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
2112
|
-
}
|
|
2113
|
-
}).finally(() => {
|
|
2114
|
-
runningPromises.delete(p);
|
|
2115
|
-
});
|
|
2116
|
-
runningPromises.add(p);
|
|
2434
|
+
} finally {
|
|
2435
|
+
if (registryInterval) {
|
|
2436
|
+
clearInterval(registryInterval);
|
|
2437
|
+
registryInterval = null;
|
|
2117
2438
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
await Promise.race([
|
|
2126
|
-
Promise.allSettled(Array.from(runningPromises)),
|
|
2127
|
-
sleepMs(stopAllowanceMs).then(() => {
|
|
2128
|
-
context.logger.warn?.(
|
|
2129
|
-
`[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
|
|
2130
|
-
);
|
|
2131
|
-
})
|
|
2132
|
-
]);
|
|
2133
|
-
} else {
|
|
2134
|
-
await Promise.allSettled(Array.from(runningPromises));
|
|
2439
|
+
if (registryReg) {
|
|
2440
|
+
await unregisterServicesRegistry(context, registryReg).catch((err) => {
|
|
2441
|
+
context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
|
|
2442
|
+
});
|
|
2443
|
+
registryReg = null;
|
|
2444
|
+
delete context.servicesRegistry;
|
|
2445
|
+
delete context.runnerHeartbeat;
|
|
2135
2446
|
}
|
|
2136
2447
|
}
|
|
2137
2448
|
}
|
|
2138
2449
|
async function waitForTaskResult(context, taskId, options = {}) {
|
|
2139
|
-
const db =
|
|
2450
|
+
const db = getDb3(context);
|
|
2140
2451
|
const queue = options.queue ?? "tasks";
|
|
2141
2452
|
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
2142
2453
|
const pollMs = options.pollMs ?? 500;
|
|
@@ -2164,6 +2475,14 @@ var TasksManager = class _TasksManager {
|
|
|
2164
2475
|
scanLimit;
|
|
2165
2476
|
allowedTasks;
|
|
2166
2477
|
registry;
|
|
2478
|
+
runnerServiceGroup;
|
|
2479
|
+
runnerServiceName;
|
|
2480
|
+
runnerIdentityDir;
|
|
2481
|
+
runnerHeartbeatIntervalMs;
|
|
2482
|
+
runnerHeartbeatStaleMs;
|
|
2483
|
+
runnerGroupMaxInstances;
|
|
2484
|
+
runnerEnforceMaxInstances;
|
|
2485
|
+
runnerMetadata;
|
|
2167
2486
|
constructor(context, options = {}) {
|
|
2168
2487
|
this.context = context;
|
|
2169
2488
|
this.queue = options.queue ?? "tasks";
|
|
@@ -2174,6 +2493,14 @@ var TasksManager = class _TasksManager {
|
|
|
2174
2493
|
this.scanLimit = options.scanLimit ?? 100;
|
|
2175
2494
|
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
2176
2495
|
this.registry = normalizeRegistry(options.registry);
|
|
2496
|
+
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
2497
|
+
this.runnerServiceName = options.runnerServiceName;
|
|
2498
|
+
this.runnerIdentityDir = options.runnerIdentityDir;
|
|
2499
|
+
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
2500
|
+
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
2501
|
+
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
2502
|
+
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
2503
|
+
this.runnerMetadata = options.runnerMetadata;
|
|
2177
2504
|
}
|
|
2178
2505
|
static init(context, options = {}) {
|
|
2179
2506
|
const defs = {
|
|
@@ -2183,7 +2510,14 @@ var TasksManager = class _TasksManager {
|
|
|
2183
2510
|
pollMs: "number default 1000",
|
|
2184
2511
|
maxParallel: "number default 1",
|
|
2185
2512
|
scanLimit: "number default 100",
|
|
2186
|
-
allowedTasks: "string"
|
|
2513
|
+
allowedTasks: "string",
|
|
2514
|
+
runnerServiceGroup: "string",
|
|
2515
|
+
runnerServiceName: "string",
|
|
2516
|
+
runnerIdentityDir: "string default ./data/runner-identities",
|
|
2517
|
+
runnerHeartbeatIntervalMs: "number default 10000",
|
|
2518
|
+
runnerHeartbeatStaleMs: "number default 45000",
|
|
2519
|
+
runnerGroupMaxInstances: "number",
|
|
2520
|
+
runnerEnforceMaxInstances: "boolean default true"
|
|
2187
2521
|
};
|
|
2188
2522
|
const discovered = context.params.getAllForModule(defs);
|
|
2189
2523
|
const resolved = {
|
|
@@ -2194,6 +2528,13 @@ var TasksManager = class _TasksManager {
|
|
|
2194
2528
|
maxParallel: discovered.maxParallel,
|
|
2195
2529
|
scanLimit: discovered.scanLimit,
|
|
2196
2530
|
allowedTasks: discovered.allowedTasks,
|
|
2531
|
+
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
2532
|
+
runnerServiceName: discovered.runnerServiceName,
|
|
2533
|
+
runnerIdentityDir: discovered.runnerIdentityDir,
|
|
2534
|
+
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
2535
|
+
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
2536
|
+
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
2537
|
+
runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
|
|
2197
2538
|
...options
|
|
2198
2539
|
};
|
|
2199
2540
|
return new _TasksManager(context, resolved);
|
|
@@ -2212,7 +2553,15 @@ var TasksManager = class _TasksManager {
|
|
|
2212
2553
|
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
2213
2554
|
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
2214
2555
|
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
2215
|
-
registry: options.registry ?? this.registry
|
|
2556
|
+
registry: options.registry ?? this.registry,
|
|
2557
|
+
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
2558
|
+
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
2559
|
+
runnerIdentityDir: options.runnerIdentityDir ?? this.runnerIdentityDir,
|
|
2560
|
+
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
2561
|
+
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
2562
|
+
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
2563
|
+
runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
|
|
2564
|
+
runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
|
|
2216
2565
|
});
|
|
2217
2566
|
}
|
|
2218
2567
|
};
|
|
@@ -2231,9 +2580,20 @@ export {
|
|
|
2231
2580
|
enqueueStopTask,
|
|
2232
2581
|
enqueueTask,
|
|
2233
2582
|
ensureTaskTables,
|
|
2583
|
+
listServicesRegistry as listAliveRunnerHeartbeats,
|
|
2584
|
+
listServicesRegistry,
|
|
2234
2585
|
queueToTableNames,
|
|
2586
|
+
registerInServicesRegistry,
|
|
2587
|
+
registerInServicesRegistry as registerRunnerHeartbeat,
|
|
2235
2588
|
runNodeTaskScript,
|
|
2236
2589
|
runTasksLoop,
|
|
2590
|
+
servicesRegistryTable as runnerHeartbeatsTable,
|
|
2591
|
+
servicesRegistryTable,
|
|
2592
|
+
touchServicesRegistry as touchRunnerHeartbeat,
|
|
2593
|
+
touchServicesRegistry,
|
|
2594
|
+
unregisterServicesRegistry as unregisterRunnerHeartbeat,
|
|
2595
|
+
unregisterServicesRegistry,
|
|
2596
|
+
updateServicesRegistryMetadata,
|
|
2237
2597
|
updateTaskProgress,
|
|
2238
2598
|
waitForTaskResult
|
|
2239
2599
|
};
|