@nmakarov/cli-toolkit 0.18.0 → 0.23.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/args.cjs +1 -4
- package/dist/args.cjs.map +1 -1
- package/dist/args.js +1 -1
- package/dist/args.js.map +1 -1
- package/dist/cli-runner.cjs +1493 -516
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +1509 -531
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +85 -157
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +84 -150
- package/dist/db.js.map +1 -1
- package/dist/errors.cjs +2 -2
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/filedatabase.cjs +19 -19
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +19 -16
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client.cjs +9 -11
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.js +10 -9
- package/dist/http-client.js.map +1 -1
- package/dist/http-client2.cjs +34 -33
- package/dist/http-client2.cjs.map +1 -1
- package/dist/http-client2.js +34 -30
- package/dist/http-client2.js.map +1 -1
- package/dist/index.cjs +2063 -658
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2063 -663
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +97 -69
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +112 -83
- package/dist/init.js.map +1 -1
- package/dist/logger.cjs +5 -5
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js +5 -4
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.cjs +21 -33
- package/dist/mock-server.cjs.map +1 -1
- package/dist/mock-server.js +21 -28
- package/dist/mock-server.js.map +1 -1
- package/dist/params.cjs +22 -10
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +22 -7
- package/dist/params.js.map +1 -1
- package/dist/s3.cjs +286 -0
- package/dist/s3.cjs.map +1 -0
- package/dist/s3.js +273 -0
- package/dist/s3.js.map +1 -0
- package/dist/screen.cjs +34 -39
- package/dist/screen.cjs.map +1 -1
- package/dist/screen.js +48 -46
- package/dist/screen.js.map +1 -1
- package/dist/tasks.cjs +1640 -416
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +1614 -412
- package/dist/tasks.js.map +1 -1
- package/dist/utils.cjs +7 -8
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.js +6 -6
- package/dist/utils.js.map +1 -1
- package/package.json +36 -44
- package/scripts/ssm/parse-cli.js +35 -0
- package/scripts/ssm/ssm-admin.js +151 -0
- package/scripts/ssm/ssm-pull.js +147 -0
package/dist/tasks.cjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -27,10 +26,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
26
|
));
|
|
28
27
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
28
|
|
|
30
|
-
// src/tasks.
|
|
29
|
+
// src/tasks/index.js
|
|
31
30
|
var tasks_exports = {};
|
|
32
31
|
__export(tasks_exports, {
|
|
33
|
-
|
|
32
|
+
AbstractTask: () => AbstractTask,
|
|
33
|
+
SERVICE_TASK_NAMES: () => SERVICE_TASK_NAMES,
|
|
34
|
+
TaskGetLogs: () => TaskGetLogs,
|
|
34
35
|
TaskPing: () => TaskPing,
|
|
35
36
|
TaskSampleProcess: () => TaskSampleProcess,
|
|
36
37
|
TaskShellCommand: () => TaskShellCommand,
|
|
@@ -40,19 +41,43 @@ __export(tasks_exports, {
|
|
|
40
41
|
TasksManager: () => TasksManager,
|
|
41
42
|
TasksRegistry: () => TasksRegistry,
|
|
42
43
|
appendTaskIpcLog: () => appendTaskIpcLog,
|
|
44
|
+
convertPattern: () => convertPattern,
|
|
43
45
|
defaultTasksRegistry: () => defaultTasksRegistry,
|
|
44
46
|
enqueueStopTask: () => enqueueStopTask,
|
|
45
47
|
enqueueTask: () => enqueueTask,
|
|
46
48
|
ensureTaskTables: () => ensureTaskTables,
|
|
49
|
+
flushTaskIpcLogs: () => flushTaskIpcLogs,
|
|
50
|
+
ipcFileLogsTableNameForSourceResource: () => ipcFileLogsTableNameForSourceResource,
|
|
51
|
+
listAliveRunnerHeartbeats: () => listServicesRegistry,
|
|
52
|
+
listServicesRegistry: () => listServicesRegistry,
|
|
53
|
+
matchesParsedPattern: () => matchesParsedPattern,
|
|
54
|
+
mergeAllowedTasksWithServiceTasks: () => mergeAllowedTasksWithServiceTasks,
|
|
55
|
+
nextTimeMatch: () => nextTimeMatch,
|
|
56
|
+
normalizeAllowedTasks: () => normalizeAllowedTasks,
|
|
47
57
|
queueToTableNames: () => queueToTableNames,
|
|
58
|
+
readTaskIpcLogsSnapshot: () => readTaskIpcLogsSnapshot,
|
|
59
|
+
registerInServicesRegistry: () => registerInServicesRegistry,
|
|
60
|
+
registerRunnerHeartbeat: () => registerInServicesRegistry,
|
|
61
|
+
resolveAsterisks: () => resolveAsterisks,
|
|
62
|
+
resolveIpcFileLogsDir: () => resolveIpcFileLogsDir,
|
|
63
|
+
resolveRanges: () => resolveRanges,
|
|
64
|
+
resolveSteps: () => resolveSteps,
|
|
48
65
|
runNodeTaskScript: () => runNodeTaskScript,
|
|
49
66
|
runTasksLoop: () => runTasksLoop,
|
|
67
|
+
taskHistoryInsertFromQueueRow: () => taskHistoryInsertFromQueueRow,
|
|
68
|
+
timeMatcher: () => timeMatcher,
|
|
69
|
+
touchRunnerHeartbeat: () => touchServicesRegistry,
|
|
70
|
+
touchServicesRegistry: () => touchServicesRegistry,
|
|
71
|
+
unregisterRunnerHeartbeat: () => unregisterServicesRegistry,
|
|
72
|
+
unregisterServicesRegistry: () => unregisterServicesRegistry,
|
|
73
|
+
updateServicesRegistryMetadata: () => updateServicesRegistryMetadata,
|
|
50
74
|
updateTaskProgress: () => updateTaskProgress,
|
|
51
75
|
waitForTaskResult: () => waitForTaskResult
|
|
52
76
|
});
|
|
53
77
|
module.exports = __toCommonJS(tasks_exports);
|
|
78
|
+
var import_node_os3 = __toESM(require("os"), 1);
|
|
54
79
|
|
|
55
|
-
// src/utils/date-utils.
|
|
80
|
+
// src/utils/date-utils.js
|
|
56
81
|
function isTimestampFolder(folderName) {
|
|
57
82
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
|
|
58
83
|
if (!isoRegex.test(folderName)) {
|
|
@@ -62,7 +87,7 @@ function isTimestampFolder(folderName) {
|
|
|
62
87
|
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
63
88
|
}
|
|
64
89
|
|
|
65
|
-
// src/utils/fs-utils.
|
|
90
|
+
// src/utils/fs-utils.js
|
|
66
91
|
var import_fs = __toESM(require("fs"), 1);
|
|
67
92
|
var import_path = __toESM(require("path"), 1);
|
|
68
93
|
async function ensurePath(...pathParts) {
|
|
@@ -86,7 +111,7 @@ function getFileExtension(dataType) {
|
|
|
86
111
|
}
|
|
87
112
|
}
|
|
88
113
|
|
|
89
|
-
// src/utils/os-utils.
|
|
114
|
+
// src/utils/os-utils.js
|
|
90
115
|
var import_fs2 = __toESM(require("fs"), 1);
|
|
91
116
|
var import_path2 = __toESM(require("path"), 1);
|
|
92
117
|
var import_child_process = require("child_process");
|
|
@@ -115,7 +140,7 @@ function getFreeDiskSpace(targetPath) {
|
|
|
115
140
|
}
|
|
116
141
|
}
|
|
117
142
|
|
|
118
|
-
// src/utils/format-utils.
|
|
143
|
+
// src/utils/format-utils.js
|
|
119
144
|
function bytesToHumanReadable(bytes) {
|
|
120
145
|
if (bytes === 0) return "0 B";
|
|
121
146
|
const k = 1024;
|
|
@@ -124,7 +149,7 @@ function bytesToHumanReadable(bytes) {
|
|
|
124
149
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
125
150
|
}
|
|
126
151
|
|
|
127
|
-
// src/utils/core-utils.
|
|
152
|
+
// src/utils/core-utils.js
|
|
128
153
|
function sleepMs(ms) {
|
|
129
154
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
130
155
|
}
|
|
@@ -133,8 +158,82 @@ function toJsonColumn(value) {
|
|
|
133
158
|
return JSON.stringify(value);
|
|
134
159
|
}
|
|
135
160
|
|
|
136
|
-
// src/tasks/
|
|
161
|
+
// src/tasks/servicesRegistry.js
|
|
162
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
163
|
+
|
|
164
|
+
// src/tasks/taskUtils.js
|
|
137
165
|
var import_node_crypto = require("crypto");
|
|
166
|
+
|
|
167
|
+
// src/tasks/time-matcher.js
|
|
168
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
169
|
+
function resolveAsterisks(field, range) {
|
|
170
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
171
|
+
}
|
|
172
|
+
function resolveRanges(field) {
|
|
173
|
+
const regex = /(\d+)-(\d+)/;
|
|
174
|
+
let current = field;
|
|
175
|
+
while (true) {
|
|
176
|
+
const match = regex.exec(current);
|
|
177
|
+
if (!match) break;
|
|
178
|
+
const raw = match[0];
|
|
179
|
+
let first = Number(match[1]);
|
|
180
|
+
let last = Number(match[2]);
|
|
181
|
+
if (last < first) {
|
|
182
|
+
[first, last] = [last, first];
|
|
183
|
+
}
|
|
184
|
+
const values = [];
|
|
185
|
+
for (let i = first; i <= last; i += 1) {
|
|
186
|
+
values.push(i);
|
|
187
|
+
}
|
|
188
|
+
current = current.replace(raw, values.join(","));
|
|
189
|
+
}
|
|
190
|
+
return current;
|
|
191
|
+
}
|
|
192
|
+
function resolveSteps(field) {
|
|
193
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
194
|
+
if (!match) return field;
|
|
195
|
+
const base = match[1];
|
|
196
|
+
const step = Number(match[2]);
|
|
197
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
198
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
199
|
+
}
|
|
200
|
+
function convertPattern(pattern) {
|
|
201
|
+
const parts = pattern.trim().split(/\s+/);
|
|
202
|
+
if (parts.length !== 6) {
|
|
203
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
204
|
+
}
|
|
205
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
206
|
+
}
|
|
207
|
+
function fieldMatches(field, value) {
|
|
208
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
209
|
+
return allowed.includes(value);
|
|
210
|
+
}
|
|
211
|
+
function matchesParsedPattern(parsed, date) {
|
|
212
|
+
return fieldMatches(parsed[0], date.getSeconds()) && fieldMatches(parsed[1], date.getMinutes()) && fieldMatches(parsed[2], date.getHours()) && fieldMatches(parsed[3], date.getDate()) && fieldMatches(parsed[4], date.getMonth() + 1) && fieldMatches(parsed[5], date.getDay());
|
|
213
|
+
}
|
|
214
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
215
|
+
const parsed = convertPattern(pattern);
|
|
216
|
+
return matchesParsedPattern(parsed, date);
|
|
217
|
+
}
|
|
218
|
+
var MS_PER_SECOND = 1e3;
|
|
219
|
+
var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
|
|
220
|
+
function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
|
|
221
|
+
const parsed = convertPattern(pattern);
|
|
222
|
+
let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
|
|
223
|
+
const end = t + maxSearchMs;
|
|
224
|
+
while (t <= end) {
|
|
225
|
+
const date = new Date(t);
|
|
226
|
+
if (matchesParsedPattern(parsed, date)) {
|
|
227
|
+
return date;
|
|
228
|
+
}
|
|
229
|
+
t += MS_PER_SECOND;
|
|
230
|
+
}
|
|
231
|
+
throw new Error(
|
|
232
|
+
`nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/tasks/taskUtils.js
|
|
138
237
|
function getDb(context) {
|
|
139
238
|
const db = context.db;
|
|
140
239
|
if (!db) {
|
|
@@ -142,105 +241,117 @@ function getDb(context) {
|
|
|
142
241
|
}
|
|
143
242
|
return db;
|
|
144
243
|
}
|
|
145
|
-
function queueToTableNames(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
244
|
+
function queueToTableNames(queueName) {
|
|
245
|
+
return {
|
|
246
|
+
tasksTable: queueName,
|
|
247
|
+
historyTable: `${queueName}_history`,
|
|
248
|
+
registryTable: `${queueName}_services_registry`
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function defineTasksTable(t, db, tableNameForIndex) {
|
|
252
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
253
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
254
|
+
t.timestamp("started_at");
|
|
255
|
+
t.timestamp("completed_at");
|
|
256
|
+
t.integer("priority").notNullable().defaultTo(50);
|
|
257
|
+
t.text("schedule");
|
|
258
|
+
t.timestamp("next_run_at").defaultTo(null);
|
|
259
|
+
t.timestamp("past_due").defaultTo(null);
|
|
260
|
+
t.text("name").notNullable();
|
|
261
|
+
t.text("opid");
|
|
262
|
+
t.json("params");
|
|
263
|
+
t.text("service_group");
|
|
264
|
+
t.integer("instance_number");
|
|
265
|
+
t.text("service_name");
|
|
266
|
+
t.text("server_name");
|
|
267
|
+
t.text("status").notNullable().defaultTo("idle");
|
|
268
|
+
t.timestamp("status_changed_at").defaultTo(null);
|
|
269
|
+
t.text("progress");
|
|
270
|
+
t.boolean("success");
|
|
271
|
+
t.json("results");
|
|
272
|
+
t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
|
|
273
|
+
t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
|
|
274
|
+
}
|
|
275
|
+
function taskHistoryInsertFromQueueRow(row, overrides) {
|
|
276
|
+
const { id, ...snapshot } = row;
|
|
277
|
+
void id;
|
|
149
278
|
return {
|
|
150
|
-
|
|
151
|
-
|
|
279
|
+
...snapshot,
|
|
280
|
+
...overrides
|
|
152
281
|
};
|
|
153
282
|
}
|
|
154
283
|
async function ensureTaskTables(context, options = {}) {
|
|
155
|
-
const
|
|
284
|
+
const queueName = options.queueName ?? "tasks";
|
|
156
285
|
const recreate = options.recreate ?? false;
|
|
157
286
|
const db = getDb(context);
|
|
158
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
287
|
+
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
159
288
|
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
160
289
|
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
290
|
+
const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
|
|
161
291
|
if (recreate) {
|
|
162
292
|
await db.schema.dropTableIfExists(historyTable);
|
|
163
293
|
await db.schema.dropTableIfExists(tasksTable);
|
|
294
|
+
await db.schema.dropTableIfExists(registryTable);
|
|
164
295
|
}
|
|
165
296
|
if (needsTasks) {
|
|
166
297
|
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
167
298
|
await db.schema.createTable(tasksTable, (t) => {
|
|
168
|
-
t
|
|
169
|
-
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
170
|
-
t.timestamp("started_at");
|
|
171
|
-
t.timestamp("completed_at");
|
|
172
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
173
|
-
t.text("schedule");
|
|
174
|
-
t.timestamp("past_due").defaultTo(null);
|
|
175
|
-
t.text("target").notNullable();
|
|
176
|
-
t.text("task").notNullable();
|
|
177
|
-
t.json("params");
|
|
178
|
-
t.text("opid");
|
|
179
|
-
t.timestamp("paused_at").defaultTo(null);
|
|
180
|
-
t.text("progress");
|
|
181
|
-
t.boolean("success");
|
|
182
|
-
t.json("results");
|
|
183
|
-
});
|
|
184
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
185
|
-
t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
|
|
186
|
-
t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
|
|
187
|
-
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
188
|
-
});
|
|
189
|
-
}
|
|
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);
|
|
299
|
+
defineTasksTable(t, db, tasksTable);
|
|
200
300
|
});
|
|
201
301
|
}
|
|
202
302
|
if (needsHistory) {
|
|
203
303
|
await db.schema.createTable(historyTable, (t) => {
|
|
204
|
-
t
|
|
205
|
-
t.timestamp("created_at").notNullable();
|
|
206
|
-
t.timestamp("started_at");
|
|
207
|
-
t.timestamp("completed_at");
|
|
208
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
209
|
-
t.text("schedule");
|
|
210
|
-
t.timestamp("past_due").defaultTo(null);
|
|
211
|
-
t.text("target").notNullable();
|
|
212
|
-
t.text("task").notNullable();
|
|
213
|
-
t.json("params");
|
|
214
|
-
t.text("opid");
|
|
215
|
-
t.text("progress");
|
|
216
|
-
t.boolean("success");
|
|
217
|
-
t.json("results");
|
|
218
|
-
});
|
|
219
|
-
await db.schema.alterTable(historyTable, (t) => {
|
|
220
|
-
t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
|
|
221
|
-
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
304
|
+
defineTasksTable(t, db, historyTable);
|
|
222
305
|
});
|
|
223
306
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
t.text("
|
|
307
|
+
if (needsRegistry) {
|
|
308
|
+
await db.schema.createTable(registryTable, (t) => {
|
|
309
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
310
|
+
t.text("queue_name").notNullable();
|
|
311
|
+
t.text("service_group").notNullable();
|
|
312
|
+
t.integer("instance_number").notNullable().defaultTo(1);
|
|
313
|
+
t.text("service_name").notNullable();
|
|
314
|
+
t.text("server_name").notNullable();
|
|
315
|
+
t.integer("pid");
|
|
316
|
+
t.json("metadata");
|
|
317
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
318
|
+
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
319
|
+
t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
|
|
320
|
+
t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
321
|
+
t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
228
322
|
});
|
|
229
323
|
}
|
|
230
324
|
}
|
|
231
325
|
async function enqueueTask(context, options) {
|
|
232
326
|
const db = getDb(context);
|
|
233
|
-
const
|
|
234
|
-
const { tasksTable } = queueToTableNames(
|
|
327
|
+
const queueName = options.queueName ?? "tasks";
|
|
328
|
+
const { tasksTable } = queueToTableNames(queueName);
|
|
235
329
|
const id = (0, import_node_crypto.randomUUID)();
|
|
330
|
+
const name = options.name ?? options.task;
|
|
331
|
+
if (!name) {
|
|
332
|
+
throw new Error("enqueueTask: name (or task) is required");
|
|
333
|
+
}
|
|
334
|
+
const schedule = options.schedule?.trim() ? options.schedule : null;
|
|
335
|
+
let nextRunAt = null;
|
|
336
|
+
if (options.nextRunAt !== void 0) {
|
|
337
|
+
nextRunAt = options.nextRunAt == null ? null : new Date(options.nextRunAt);
|
|
338
|
+
} else if (schedule) {
|
|
339
|
+
nextRunAt = nextTimeMatch(schedule, /* @__PURE__ */ new Date());
|
|
340
|
+
}
|
|
236
341
|
await db(tasksTable).insert({
|
|
237
342
|
id,
|
|
238
|
-
|
|
239
|
-
task: options.task,
|
|
343
|
+
name,
|
|
240
344
|
params: toJsonColumn(options.params ?? null),
|
|
241
345
|
opid: options.opid ?? null,
|
|
242
|
-
priority: options.priority ??
|
|
243
|
-
schedule
|
|
346
|
+
priority: options.priority ?? 50,
|
|
347
|
+
schedule,
|
|
348
|
+
next_run_at: nextRunAt,
|
|
349
|
+
service_group: options.serviceGroup ?? null,
|
|
350
|
+
instance_number: options.instanceNumber ?? null,
|
|
351
|
+
service_name: options.serviceName ?? null,
|
|
352
|
+
server_name: options.serverName ?? null,
|
|
353
|
+
status: "idle",
|
|
354
|
+
status_changed_at: db.fn.now()
|
|
244
355
|
});
|
|
245
356
|
return id;
|
|
246
357
|
}
|
|
@@ -251,11 +362,261 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
|
251
362
|
});
|
|
252
363
|
}
|
|
253
364
|
|
|
254
|
-
// src/
|
|
365
|
+
// src/tasks/servicesRegistry.js
|
|
366
|
+
function getDb2(context) {
|
|
367
|
+
const db = context.db;
|
|
368
|
+
if (!db) {
|
|
369
|
+
throw new Error("Services registry requires context.db");
|
|
370
|
+
}
|
|
371
|
+
return db;
|
|
372
|
+
}
|
|
373
|
+
function parseMetadataColumn(value) {
|
|
374
|
+
if (!value) return {};
|
|
375
|
+
if (typeof value === "object" && !Array.isArray(value)) return value;
|
|
376
|
+
if (typeof value === "string") {
|
|
377
|
+
try {
|
|
378
|
+
const p = JSON.parse(value);
|
|
379
|
+
return p && typeof p === "object" && !Array.isArray(value) ? p : {};
|
|
380
|
+
} catch {
|
|
381
|
+
return {};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return {};
|
|
385
|
+
}
|
|
386
|
+
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
387
|
+
intake: 1,
|
|
388
|
+
harvest: 1,
|
|
389
|
+
harvester: 0,
|
|
390
|
+
loader: 0,
|
|
391
|
+
photos: 0,
|
|
392
|
+
photosprocessor: 0,
|
|
393
|
+
ingest: 0
|
|
394
|
+
};
|
|
395
|
+
function sanitizeNamePart(raw) {
|
|
396
|
+
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
397
|
+
return s.slice(0, 80) || "runner";
|
|
398
|
+
}
|
|
399
|
+
function resolveMaxInstances(serviceGroup, override) {
|
|
400
|
+
if (override !== void 0 && Number.isFinite(override)) {
|
|
401
|
+
return Math.max(0, Math.floor(Number(override)));
|
|
402
|
+
}
|
|
403
|
+
const g = serviceGroup.trim().toLowerCase();
|
|
404
|
+
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
405
|
+
}
|
|
406
|
+
async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
|
|
407
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
408
|
+
let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
|
|
409
|
+
if (excludeRowId) {
|
|
410
|
+
q = q.whereNot("id", excludeRowId);
|
|
411
|
+
}
|
|
412
|
+
const row = await q.count("id as count").first();
|
|
413
|
+
return Number(row?.count ?? 0);
|
|
414
|
+
}
|
|
415
|
+
async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
|
|
416
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
417
|
+
const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
|
|
418
|
+
const set = /* @__PURE__ */ new Set();
|
|
419
|
+
for (const r of rows) {
|
|
420
|
+
const n = Number(r.instance_number);
|
|
421
|
+
if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
|
|
422
|
+
}
|
|
423
|
+
return set;
|
|
424
|
+
}
|
|
425
|
+
function isUniqueViolation(error) {
|
|
426
|
+
const code = error?.code ?? error?.errno;
|
|
427
|
+
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
428
|
+
}
|
|
429
|
+
function buildMetadata(options) {
|
|
430
|
+
const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
|
|
431
|
+
if (options.target) {
|
|
432
|
+
base.runnerTarget = options.target;
|
|
433
|
+
}
|
|
434
|
+
return toJsonColumn(Object.keys(base).length ? base : null);
|
|
435
|
+
}
|
|
436
|
+
function allocateInstanceNumber(occupied, explicit, maxSlots) {
|
|
437
|
+
if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
|
|
438
|
+
const e = Math.max(1, Math.floor(Number(explicit)));
|
|
439
|
+
if (occupied.has(e)) {
|
|
440
|
+
throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
|
|
441
|
+
}
|
|
442
|
+
if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
|
|
443
|
+
throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
|
|
444
|
+
}
|
|
445
|
+
return e;
|
|
446
|
+
}
|
|
447
|
+
const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
|
|
448
|
+
for (let n = 1; n <= cap; n++) {
|
|
449
|
+
if (!occupied.has(n)) return n;
|
|
450
|
+
}
|
|
451
|
+
throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
|
|
452
|
+
}
|
|
453
|
+
function defaultServiceName(groupBase, hostBase, instanceNumber) {
|
|
454
|
+
return `${groupBase}-${hostBase}-${instanceNumber}`;
|
|
455
|
+
}
|
|
456
|
+
async function registerInServicesRegistry(context, options) {
|
|
457
|
+
const db = getDb2(context);
|
|
458
|
+
const registryTable = queueToTableNames(options.queueName).registryTable;
|
|
459
|
+
const serviceGroup = options.serviceGroup.trim();
|
|
460
|
+
if (!serviceGroup) {
|
|
461
|
+
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
462
|
+
}
|
|
463
|
+
const serverName = import_node_os.default.hostname();
|
|
464
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
465
|
+
const meta = buildMetadata(options);
|
|
466
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
467
|
+
const hostBase = sanitizeNamePart(serverName);
|
|
468
|
+
const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
|
|
469
|
+
const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
|
|
470
|
+
if (maxAllowed > 0 && aliveCount >= maxAllowed) {
|
|
471
|
+
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
|
|
472
|
+
if (options.enforceMaxInstances) {
|
|
473
|
+
throw new Error(msg);
|
|
474
|
+
}
|
|
475
|
+
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
476
|
+
}
|
|
477
|
+
const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
|
|
478
|
+
const cutoff = new Date(Date.now() - options.staleMs);
|
|
479
|
+
const MAX_ATTEMPTS = 8;
|
|
480
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
481
|
+
const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
|
|
482
|
+
const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
|
|
483
|
+
const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
|
|
484
|
+
const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
485
|
+
if (existing) {
|
|
486
|
+
const lastSeen = new Date(existing.last_seen_at);
|
|
487
|
+
const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
|
|
488
|
+
if (isAlive) {
|
|
489
|
+
if (options.serviceName?.trim()) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
context.logger.warn?.(
|
|
495
|
+
`[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
|
|
496
|
+
);
|
|
497
|
+
if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
|
|
498
|
+
throw new Error(
|
|
499
|
+
`[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
await new Promise((r) => setTimeout(r, 50 + attempt * 30));
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
await db(registryTable).where({ id: existing.id }).update({
|
|
506
|
+
server_name: serverName,
|
|
507
|
+
pid,
|
|
508
|
+
metadata: meta,
|
|
509
|
+
service_group: serviceGroup,
|
|
510
|
+
instance_number: instanceNumber,
|
|
511
|
+
last_seen_at: db.fn.now()
|
|
512
|
+
});
|
|
513
|
+
const reg = {
|
|
514
|
+
serviceName: serviceNameRaw,
|
|
515
|
+
serviceGroup,
|
|
516
|
+
queueName: options.queueName,
|
|
517
|
+
target: options.target,
|
|
518
|
+
rowId: String(existing.id),
|
|
519
|
+
registryTable,
|
|
520
|
+
instanceNumber
|
|
521
|
+
};
|
|
522
|
+
context.servicesRegistry = reg;
|
|
523
|
+
context.runnerHeartbeat = reg;
|
|
524
|
+
context.logger.info?.(
|
|
525
|
+
`[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
526
|
+
);
|
|
527
|
+
return reg;
|
|
528
|
+
}
|
|
529
|
+
try {
|
|
530
|
+
const rows = await db(registryTable).insert({
|
|
531
|
+
queue_name: options.queueName,
|
|
532
|
+
service_group: serviceGroup,
|
|
533
|
+
instance_number: instanceNumber,
|
|
534
|
+
service_name: serviceNameRaw,
|
|
535
|
+
server_name: serverName,
|
|
536
|
+
pid,
|
|
537
|
+
metadata: meta,
|
|
538
|
+
last_seen_at: db.fn.now(),
|
|
539
|
+
created_at: db.fn.now()
|
|
540
|
+
}).returning(["id", "service_name"]);
|
|
541
|
+
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
542
|
+
let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
|
|
543
|
+
if (!rowId) {
|
|
544
|
+
const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
545
|
+
rowId = again?.id != null ? String(again.id) : "";
|
|
546
|
+
}
|
|
547
|
+
if (!rowId) continue;
|
|
548
|
+
const regNew = {
|
|
549
|
+
serviceName: String(row?.service_name ?? serviceNameRaw),
|
|
550
|
+
serviceGroup,
|
|
551
|
+
queueName: options.queueName,
|
|
552
|
+
target: options.target,
|
|
553
|
+
rowId,
|
|
554
|
+
registryTable,
|
|
555
|
+
instanceNumber
|
|
556
|
+
};
|
|
557
|
+
context.servicesRegistry = regNew;
|
|
558
|
+
context.runnerHeartbeat = regNew;
|
|
559
|
+
context.logger.info?.(
|
|
560
|
+
`[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
561
|
+
);
|
|
562
|
+
return regNew;
|
|
563
|
+
} catch (error) {
|
|
564
|
+
if (!isUniqueViolation(error)) {
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
throw new Error(
|
|
571
|
+
`[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
async function touchServicesRegistry(context, registration) {
|
|
575
|
+
const db = getDb2(context);
|
|
576
|
+
const serverName = import_node_os.default.hostname();
|
|
577
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
578
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
579
|
+
last_seen_at: db.fn.now(),
|
|
580
|
+
server_name: serverName,
|
|
581
|
+
pid
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
async function updateServicesRegistryMetadata(context, registration, patch) {
|
|
585
|
+
const db = getDb2(context);
|
|
586
|
+
const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
|
|
587
|
+
const prev = parseMetadataColumn(row?.metadata);
|
|
588
|
+
const merged = { ...prev, ...patch };
|
|
589
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
590
|
+
metadata: toJsonColumn(merged),
|
|
591
|
+
last_seen_at: db.fn.now()
|
|
592
|
+
});
|
|
593
|
+
context.logger.info?.(`[services-registry] metadata updated for ${registration.serviceName}`);
|
|
594
|
+
}
|
|
595
|
+
async function unregisterServicesRegistry(context, registration) {
|
|
596
|
+
const db = getDb2(context);
|
|
597
|
+
await db(registration.registryTable).where({ id: registration.rowId }).delete();
|
|
598
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
|
|
599
|
+
}
|
|
600
|
+
async function listServicesRegistry(context, options = { queueName: "tasks" }) {
|
|
601
|
+
const db = getDb2(context);
|
|
602
|
+
const staleMs = options.staleMs ?? 6e4;
|
|
603
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
604
|
+
const table = queueToTableNames(options.queueName).registryTable;
|
|
605
|
+
let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
|
|
606
|
+
if (options.serviceGroup?.trim()) {
|
|
607
|
+
q = q.where({ service_group: options.serviceGroup.trim() });
|
|
608
|
+
}
|
|
609
|
+
return await q;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// src/tasks/taskLogs.js
|
|
613
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
614
|
+
|
|
615
|
+
// src/filedatabase/index.js
|
|
255
616
|
var import_fs3 = __toESM(require("fs"), 1);
|
|
256
617
|
var import_path3 = __toESM(require("path"), 1);
|
|
257
618
|
|
|
258
|
-
// src/filedatabase/serializers.
|
|
619
|
+
// src/filedatabase/serializers.js
|
|
259
620
|
function detectDataType(data) {
|
|
260
621
|
if (Array.isArray(data)) {
|
|
261
622
|
return "json-array";
|
|
@@ -287,7 +648,7 @@ function deserializeData(rawData, dataType) {
|
|
|
287
648
|
}
|
|
288
649
|
}
|
|
289
650
|
|
|
290
|
-
// src/errors.
|
|
651
|
+
// src/errors.js
|
|
291
652
|
var FrameworkError = class extends Error {
|
|
292
653
|
constructor(message) {
|
|
293
654
|
super(message);
|
|
@@ -307,7 +668,7 @@ var FileDatabaseError = class extends FrameworkError {
|
|
|
307
668
|
}
|
|
308
669
|
};
|
|
309
670
|
|
|
310
|
-
// src/filedatabase/index.
|
|
671
|
+
// src/filedatabase/index.js
|
|
311
672
|
var FileDatabase = class _FileDatabase {
|
|
312
673
|
basePath;
|
|
313
674
|
namespace;
|
|
@@ -393,7 +754,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
393
754
|
if (errors.length) {
|
|
394
755
|
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
395
756
|
}
|
|
396
|
-
|
|
757
|
+
const parts = [this.basePath, this.namespace];
|
|
397
758
|
if (this.tableName) {
|
|
398
759
|
parts.push(...this.tableName.split("/"));
|
|
399
760
|
}
|
|
@@ -886,14 +1247,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
886
1247
|
* Prepare the instance for read or write operations
|
|
887
1248
|
* This discovers state and sets up internal members based on mode and current data
|
|
888
1249
|
*/
|
|
889
|
-
async prepare(
|
|
1250
|
+
async prepare(options) {
|
|
1251
|
+
const { write, read, version, deferInitialVersion } = options;
|
|
890
1252
|
if (write) {
|
|
891
1253
|
if (this.versioned) {
|
|
892
1254
|
if (this.currentVersion === null) {
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1255
|
+
if (!deferInitialVersion) {
|
|
1256
|
+
await this.makeNewVersion();
|
|
1257
|
+
this.metadata = this.getDefaultMetadata();
|
|
1258
|
+
this.metadata.version = this.currentVersion;
|
|
1259
|
+
this.makeNewFile();
|
|
1260
|
+
}
|
|
897
1261
|
} else {
|
|
898
1262
|
if (!this.metadata.files.length) {
|
|
899
1263
|
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
@@ -1003,7 +1367,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
1003
1367
|
if (options.forceNewVersion && !this.versioned) {
|
|
1004
1368
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
1005
1369
|
}
|
|
1006
|
-
await this.prepare({ write: true });
|
|
1370
|
+
await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
|
|
1007
1371
|
const incomingDataType = detectDataType(data);
|
|
1008
1372
|
this.metadata.dataType = incomingDataType;
|
|
1009
1373
|
if (options.forceNewVersion) {
|
|
@@ -1297,7 +1661,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
1297
1661
|
}
|
|
1298
1662
|
};
|
|
1299
1663
|
|
|
1300
|
-
// src/tasks/taskLogs.
|
|
1664
|
+
// src/tasks/taskLogs.js
|
|
1301
1665
|
function getLogsState(context) {
|
|
1302
1666
|
const holder = context;
|
|
1303
1667
|
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
@@ -1350,6 +1714,96 @@ function getLogsState(context) {
|
|
|
1350
1714
|
holder.__tasksLogsState = state;
|
|
1351
1715
|
return state;
|
|
1352
1716
|
}
|
|
1717
|
+
function ipcLogTargetKey(target) {
|
|
1718
|
+
const bp = target.basePath ?? "";
|
|
1719
|
+
const ns = target.namespace ?? "";
|
|
1720
|
+
return `${bp}::${ns}::${target.tableName}`;
|
|
1721
|
+
}
|
|
1722
|
+
function ipcFileLogsTableNameForSourceResource(source, resource) {
|
|
1723
|
+
const seg = (s) => {
|
|
1724
|
+
const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
1725
|
+
return t.length ? t : "x";
|
|
1726
|
+
};
|
|
1727
|
+
return `${seg(source)}/${seg(resource)}`;
|
|
1728
|
+
}
|
|
1729
|
+
async function readTaskIpcLogsSnapshot(context, options) {
|
|
1730
|
+
const holder = context;
|
|
1731
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
|
|
1732
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
|
|
1733
|
+
const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
|
|
1734
|
+
const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
|
|
1735
|
+
const fd = new FileDatabase({
|
|
1736
|
+
basePath,
|
|
1737
|
+
namespace,
|
|
1738
|
+
tableName,
|
|
1739
|
+
versioned: true,
|
|
1740
|
+
useMetadata: true,
|
|
1741
|
+
maxVersions: 30,
|
|
1742
|
+
pageSize: 2e3,
|
|
1743
|
+
logger: holder.logger
|
|
1744
|
+
});
|
|
1745
|
+
const versions = await fd.getVersions();
|
|
1746
|
+
if (versions.length === 0) {
|
|
1747
|
+
return { records: [], latestTs: null };
|
|
1748
|
+
}
|
|
1749
|
+
const latest = versions[versions.length - 1];
|
|
1750
|
+
const raw = await fd.read({ version: latest });
|
|
1751
|
+
const arr = Array.isArray(raw) ? raw : [];
|
|
1752
|
+
let filtered = arr;
|
|
1753
|
+
if (options.afterTs && String(options.afterTs).trim()) {
|
|
1754
|
+
const cut = String(options.afterTs).trim();
|
|
1755
|
+
filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
|
|
1756
|
+
}
|
|
1757
|
+
let latestTs = null;
|
|
1758
|
+
for (const r of filtered) {
|
|
1759
|
+
const ts = typeof r?.ts === "string" ? String(r.ts) : null;
|
|
1760
|
+
if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
|
|
1761
|
+
}
|
|
1762
|
+
const incremental = !!(options.afterTs && String(options.afterTs).trim());
|
|
1763
|
+
const maxReturn = incremental ? 1e4 : tail;
|
|
1764
|
+
const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
|
|
1765
|
+
return { records: sliced, latestTs };
|
|
1766
|
+
}
|
|
1767
|
+
function resolveIpcFileLogsDir(context, target) {
|
|
1768
|
+
const holder = context;
|
|
1769
|
+
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
1770
|
+
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
1771
|
+
const segments = target.tableName.split("/").filter(Boolean);
|
|
1772
|
+
return import_node_path.default.resolve(basePath, namespace, ...segments);
|
|
1773
|
+
}
|
|
1774
|
+
function getLogsStateForTarget(context, target) {
|
|
1775
|
+
const holder = context;
|
|
1776
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
1777
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
1778
|
+
if (!enabled) return null;
|
|
1779
|
+
if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
|
|
1780
|
+
const map = holder.__tasksLogsTargetStates;
|
|
1781
|
+
const key = ipcLogTargetKey(target);
|
|
1782
|
+
if (map.has(key)) return map.get(key);
|
|
1783
|
+
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
1784
|
+
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
1785
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
1786
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
1787
|
+
const db = new FileDatabase({
|
|
1788
|
+
basePath,
|
|
1789
|
+
namespace,
|
|
1790
|
+
tableName: target.tableName,
|
|
1791
|
+
versioned: true,
|
|
1792
|
+
useMetadata: true,
|
|
1793
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
1794
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
1795
|
+
logger: holder.logger
|
|
1796
|
+
});
|
|
1797
|
+
const state = {
|
|
1798
|
+
db,
|
|
1799
|
+
errorDb: null,
|
|
1800
|
+
queue: Promise.resolve(),
|
|
1801
|
+
initialized: false,
|
|
1802
|
+
errorInitialized: false
|
|
1803
|
+
};
|
|
1804
|
+
map.set(key, state);
|
|
1805
|
+
return state;
|
|
1806
|
+
}
|
|
1353
1807
|
function isErrorPayload(payload) {
|
|
1354
1808
|
if (!payload) return false;
|
|
1355
1809
|
if (typeof payload === "object") {
|
|
@@ -1369,14 +1823,26 @@ function buildLogRecord(task, payload) {
|
|
|
1369
1823
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1370
1824
|
opid: task.opid ?? null,
|
|
1371
1825
|
taskId: task.id,
|
|
1372
|
-
taskName: task.
|
|
1373
|
-
target: task.
|
|
1826
|
+
taskName: task.name,
|
|
1827
|
+
target: task.service_group,
|
|
1374
1828
|
source: typeof params.source === "string" ? params.source : null,
|
|
1375
1829
|
resource: typeof params.resource === "string" ? params.resource : null,
|
|
1376
1830
|
payload
|
|
1377
1831
|
};
|
|
1378
1832
|
}
|
|
1379
|
-
function appendTaskIpcLog(context, task, payload) {
|
|
1833
|
+
function appendTaskIpcLog(context, task, payload, target) {
|
|
1834
|
+
if (target) {
|
|
1835
|
+
const state2 = getLogsStateForTarget(context, target);
|
|
1836
|
+
if (!state2?.db) return;
|
|
1837
|
+
const record2 = buildLogRecord(task, payload);
|
|
1838
|
+
state2.queue = state2.queue.then(async () => {
|
|
1839
|
+
await state2.db.write([record2], { forceNewVersion: !state2.initialized });
|
|
1840
|
+
state2.initialized = true;
|
|
1841
|
+
}).catch((error) => {
|
|
1842
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
|
|
1843
|
+
});
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1380
1846
|
const state = getLogsState(context);
|
|
1381
1847
|
if (!state.db && !state.errorDb) return;
|
|
1382
1848
|
const record = buildLogRecord(task, payload);
|
|
@@ -1393,99 +1859,316 @@ function appendTaskIpcLog(context, task, payload) {
|
|
|
1393
1859
|
context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
|
|
1394
1860
|
});
|
|
1395
1861
|
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
let current = field;
|
|
1405
|
-
while (true) {
|
|
1406
|
-
const match = regex.exec(current);
|
|
1407
|
-
if (!match) break;
|
|
1408
|
-
const raw = match[0];
|
|
1409
|
-
let first = Number(match[1]);
|
|
1410
|
-
let last = Number(match[2]);
|
|
1411
|
-
if (last < first) {
|
|
1412
|
-
[first, last] = [last, first];
|
|
1413
|
-
}
|
|
1414
|
-
const values = [];
|
|
1415
|
-
for (let i = first; i <= last; i += 1) {
|
|
1416
|
-
values.push(i);
|
|
1862
|
+
async function flushTaskIpcLogs(context) {
|
|
1863
|
+
const holder = context;
|
|
1864
|
+
const promises = [];
|
|
1865
|
+
if (holder.__tasksLogsState?.queue) promises.push(holder.__tasksLogsState.queue);
|
|
1866
|
+
const map = holder.__tasksLogsTargetStates;
|
|
1867
|
+
if (map) {
|
|
1868
|
+
for (const s of map.values()) {
|
|
1869
|
+
if (s.queue) promises.push(s.queue);
|
|
1417
1870
|
}
|
|
1418
|
-
current = current.replace(raw, values.join(","));
|
|
1419
|
-
}
|
|
1420
|
-
return current;
|
|
1421
|
-
}
|
|
1422
|
-
function resolveSteps(field) {
|
|
1423
|
-
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
1424
|
-
if (!match) return field;
|
|
1425
|
-
const base = match[1];
|
|
1426
|
-
const step = Number(match[2]);
|
|
1427
|
-
if (!Number.isFinite(step) || step <= 0) return field;
|
|
1428
|
-
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
1429
|
-
}
|
|
1430
|
-
function convertPattern(pattern) {
|
|
1431
|
-
const parts = pattern.trim().split(/\s+/);
|
|
1432
|
-
if (parts.length !== 6) {
|
|
1433
|
-
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
1434
1871
|
}
|
|
1435
|
-
|
|
1436
|
-
}
|
|
1437
|
-
function fieldMatches(field, value) {
|
|
1438
|
-
const allowed = field.split(",").map((v) => Number(v));
|
|
1439
|
-
return allowed.includes(value);
|
|
1440
|
-
}
|
|
1441
|
-
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
1442
|
-
const parsed = convertPattern(pattern);
|
|
1443
|
-
return fieldMatches(parsed[0], date.getSeconds()) && fieldMatches(parsed[1], date.getMinutes()) && fieldMatches(parsed[2], date.getHours()) && fieldMatches(parsed[3], date.getDate()) && fieldMatches(parsed[4], date.getMonth() + 1) && fieldMatches(parsed[5], date.getDay());
|
|
1872
|
+
await Promise.all(promises);
|
|
1444
1873
|
}
|
|
1445
1874
|
|
|
1446
|
-
// src/tasks/
|
|
1447
|
-
var
|
|
1448
|
-
|
|
1449
|
-
|
|
1875
|
+
// src/tasks/AbstractTask.js
|
|
1876
|
+
var AbstractTask = class _AbstractTask {
|
|
1877
|
+
/**
|
|
1878
|
+
* Whether `send-task` should wait for completion (and print a result
|
|
1879
|
+
* report) when no explicit `--wait` / `--noWait` flag is given. Defaults
|
|
1880
|
+
* to false; short-lived probe tasks (e.g. `ping`) override to true.
|
|
1881
|
+
*
|
|
1882
|
+
* @type {boolean}
|
|
1883
|
+
*/
|
|
1884
|
+
static defaultWaitForResult = false;
|
|
1885
|
+
/**
|
|
1886
|
+
* @param {object} context Runner context (db, logger, params, emitter...).
|
|
1887
|
+
* @param {object} task Task row as claimed from the queue.
|
|
1888
|
+
*/
|
|
1450
1889
|
constructor(context, task) {
|
|
1451
1890
|
this.context = context;
|
|
1452
1891
|
this.task = task;
|
|
1453
1892
|
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Return a short reason string when the task should be deferred (e.g. "locked
|
|
1895
|
+
* by source"), or `false`/falsy when it is free to run. Default: always `false`.
|
|
1896
|
+
*
|
|
1897
|
+
* @returns {string | false | Promise<string | false>}
|
|
1898
|
+
*/
|
|
1454
1899
|
cantRunReason() {
|
|
1455
1900
|
return false;
|
|
1456
1901
|
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Called by the runner when a stop has been requested. Subclasses running
|
|
1904
|
+
* long loops should flip a flag here and check it between iterations.
|
|
1905
|
+
*
|
|
1906
|
+
* @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
|
|
1907
|
+
*/
|
|
1457
1908
|
requestStop(_allowanceMs) {
|
|
1458
1909
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1910
|
+
/**
|
|
1911
|
+
* Perform the task. Must be implemented by subclasses.
|
|
1912
|
+
*
|
|
1913
|
+
* @param {(progress: unknown) => Promise<void>} _reportProgress
|
|
1914
|
+
* Updates the DB `progress` column. Accepts any serializable value;
|
|
1915
|
+
* strings are stored verbatim, objects are JSON-stringified.
|
|
1916
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
1917
|
+
*/
|
|
1918
|
+
async run(_reportProgress) {
|
|
1919
|
+
throw new Error("AbstractTask.run must be implemented by subclass");
|
|
1466
1920
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1921
|
+
/**
|
|
1922
|
+
* Resolve a complete row payload for this task — envelope fields (queue,
|
|
1923
|
+
* priority, targeting, schedule…) plus the inner `params` blob produced by
|
|
1924
|
+
* {@link AbstractTask.resolveCustomParams}. Output shape matches
|
|
1925
|
+
* {@link enqueueTask}'s `options` argument, so the typical call is:
|
|
1926
|
+
*
|
|
1927
|
+
* const payload = await TaskClass.resolveParams(context, { name });
|
|
1928
|
+
* await enqueueTask(context, payload);
|
|
1929
|
+
*
|
|
1930
|
+
* Validation failures throw {@link ParamError} so the script aborts before
|
|
1931
|
+
* a malformed row hits the DB.
|
|
1932
|
+
*
|
|
1933
|
+
* @param {object} context
|
|
1934
|
+
* @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
|
|
1935
|
+
* Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
|
|
1936
|
+
* `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
|
|
1937
|
+
* `nextRunAt`, plus `params` (object — overlay onto inner blob).
|
|
1938
|
+
* @returns {Promise<object>}
|
|
1939
|
+
*/
|
|
1940
|
+
static async resolveParams(context, overrides = {}) {
|
|
1941
|
+
const main = _AbstractTask._resolveMainFields(context, overrides);
|
|
1942
|
+
const params = await this.resolveCustomParams(context, overrides);
|
|
1943
|
+
return { ...main, params };
|
|
1480
1944
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1945
|
+
/**
|
|
1946
|
+
* Resolve the inner JSON blob stored in the `params` column. Default
|
|
1947
|
+
* implementation passes through `--paramsJson` (parsed as a JSON object)
|
|
1948
|
+
* overlaid with `overrides.params` when supplied; returns `null` when
|
|
1949
|
+
* neither is provided.
|
|
1950
|
+
*
|
|
1951
|
+
* Subclasses with typed fields should override and call
|
|
1952
|
+
* {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
|
|
1953
|
+
* resolution, then validate and throw {@link ParamError} on bad input.
|
|
1954
|
+
*
|
|
1955
|
+
* @param {object} context
|
|
1956
|
+
* @param {Record<string, unknown>} [overrides]
|
|
1957
|
+
* @returns {Promise<object|null>}
|
|
1958
|
+
*/
|
|
1959
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
1960
|
+
return _AbstractTask._defaultParamsBlob(context, overrides);
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Read main task envelope fields from `context.params` (CLI/env), with
|
|
1964
|
+
* any matching key on `overrides` taking precedence. Internal; called by
|
|
1965
|
+
* {@link AbstractTask.resolveParams}.
|
|
1966
|
+
*
|
|
1967
|
+
* @param {object} context
|
|
1968
|
+
* @param {Record<string, unknown>} [overrides]
|
|
1969
|
+
* @returns {object}
|
|
1970
|
+
*/
|
|
1971
|
+
static _resolveMainFields(context, overrides = {}) {
|
|
1972
|
+
const defs = {
|
|
1973
|
+
queueName: "string default tasks",
|
|
1974
|
+
priority: "number default 50",
|
|
1975
|
+
serviceGroup: "string",
|
|
1976
|
+
serviceName: "string",
|
|
1977
|
+
instanceNumber: "number",
|
|
1978
|
+
serverName: "string",
|
|
1979
|
+
opid: "string",
|
|
1980
|
+
schedule: "string"
|
|
1981
|
+
};
|
|
1982
|
+
const cli = context.params.getAllForModule("task-envelope", defs);
|
|
1983
|
+
const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
|
|
1984
|
+
if (!name) {
|
|
1985
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
1986
|
+
}
|
|
1987
|
+
let instanceNumber;
|
|
1988
|
+
const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
|
|
1989
|
+
if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
|
|
1990
|
+
const n = Number(rawInstance);
|
|
1991
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
1992
|
+
throw new ParamError("--instanceNumber must be a positive integer when set");
|
|
1993
|
+
}
|
|
1994
|
+
instanceNumber = n;
|
|
1995
|
+
} else {
|
|
1996
|
+
instanceNumber = null;
|
|
1997
|
+
}
|
|
1998
|
+
const priorityRaw = overrides.priority ?? cli.priority ?? 50;
|
|
1999
|
+
const priority = Number(priorityRaw);
|
|
2000
|
+
if (!Number.isFinite(priority)) {
|
|
2001
|
+
throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
|
|
2002
|
+
}
|
|
2003
|
+
return {
|
|
2004
|
+
name,
|
|
2005
|
+
queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
|
|
2006
|
+
priority,
|
|
2007
|
+
serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
|
|
2008
|
+
serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
|
|
2009
|
+
instanceNumber,
|
|
2010
|
+
serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
|
|
2011
|
+
opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
|
|
2012
|
+
schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
|
|
2013
|
+
nextRunAt: overrides.nextRunAt ?? null
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Default inner-params resolver: parses `--paramsJson` (must be a JSON
|
|
2018
|
+
* object), then overlays `overrides.params` on top. Returns `null` when
|
|
2019
|
+
* neither is provided.
|
|
2020
|
+
*
|
|
2021
|
+
* @param {object} context
|
|
2022
|
+
* @param {Record<string, unknown>} [overrides]
|
|
2023
|
+
* @returns {object|null}
|
|
2024
|
+
*/
|
|
2025
|
+
static _defaultParamsBlob(context, overrides = {}) {
|
|
2026
|
+
const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
|
|
2027
|
+
const fromJson = parseParamsJson(cli.paramsJson);
|
|
2028
|
+
const fromOverride = pickParamsObject(overrides);
|
|
2029
|
+
if (!fromJson && !fromOverride) return null;
|
|
2030
|
+
return { ...fromJson ?? {}, ...fromOverride ?? {} };
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
|
|
2034
|
+
* params (per `defs`) plus `--paramsJson` under a module namespace, then
|
|
2035
|
+
* merges them with explicit `overrides.params` in increasing priority:
|
|
2036
|
+
*
|
|
2037
|
+
* typed CLI flags → --paramsJson → overrides.params
|
|
2038
|
+
*
|
|
2039
|
+
* Undefined values are dropped so defaults declared in `defs` aren't
|
|
2040
|
+
* overwritten by missing-flag noise. Returns the merged object; the
|
|
2041
|
+
* caller is responsible for validation and throwing `ParamError`.
|
|
2042
|
+
*
|
|
2043
|
+
* @param {object} context
|
|
2044
|
+
* @param {string} moduleName Namespace for `--showUsedParams` grouping.
|
|
2045
|
+
* @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
|
|
2046
|
+
* @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
|
|
2047
|
+
* @returns {Record<string, unknown>}
|
|
2048
|
+
*/
|
|
2049
|
+
static _mergeTypedParams(context, moduleName, defs, overrides = {}) {
|
|
2050
|
+
const fullDefs = { ...defs, paramsJson: "string" };
|
|
2051
|
+
const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
|
|
2052
|
+
const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
|
|
2053
|
+
const fromCli = {};
|
|
2054
|
+
for (const [k, v] of Object.entries(cliRaw)) {
|
|
2055
|
+
if (k === "paramsJson") continue;
|
|
2056
|
+
if (v !== void 0 && v !== null) fromCli[k] = v;
|
|
2057
|
+
}
|
|
2058
|
+
const fromOverride = pickParamsObject(overrides) ?? {};
|
|
2059
|
+
return { ...fromCli, ...fromJson, ...fromOverride };
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
function emptyToUndef(s) {
|
|
2063
|
+
if (s === void 0 || s === null) return void 0;
|
|
2064
|
+
if (typeof s !== "string") return s;
|
|
2065
|
+
const t = s.trim();
|
|
2066
|
+
return t.length ? t : void 0;
|
|
2067
|
+
}
|
|
2068
|
+
function parseParamsJson(raw) {
|
|
2069
|
+
if (raw == null) return null;
|
|
2070
|
+
const t = String(raw).trim();
|
|
2071
|
+
if (!t) return null;
|
|
2072
|
+
let parsed;
|
|
2073
|
+
try {
|
|
2074
|
+
parsed = JSON.parse(t);
|
|
2075
|
+
} catch (e) {
|
|
2076
|
+
throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
|
|
2077
|
+
}
|
|
2078
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2079
|
+
throw new ParamError("--paramsJson must be a JSON object");
|
|
2080
|
+
}
|
|
2081
|
+
return parsed;
|
|
2082
|
+
}
|
|
2083
|
+
function pickParamsObject(overrides) {
|
|
2084
|
+
const p = overrides?.params;
|
|
2085
|
+
if (p && typeof p === "object" && !Array.isArray(p)) return p;
|
|
2086
|
+
return void 0;
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// src/tasks/coreTasks/TaskPing.js
|
|
2090
|
+
var TaskPing = class extends AbstractTask {
|
|
2091
|
+
/** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
|
|
2092
|
+
static defaultWaitForResult = true;
|
|
2093
|
+
/** Ping takes no params. */
|
|
2094
|
+
static async resolveCustomParams() {
|
|
2095
|
+
return null;
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* @returns {Promise<{ success: true, results: "pong" }>}
|
|
2099
|
+
*/
|
|
2100
|
+
async run() {
|
|
2101
|
+
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
2102
|
+
return { success: true, results: "pong" };
|
|
2103
|
+
}
|
|
2104
|
+
};
|
|
2105
|
+
|
|
2106
|
+
// src/tasks/coreTasks/TaskSampleProcess.js
|
|
2107
|
+
var TaskSampleProcess = class extends AbstractTask {
|
|
2108
|
+
/**
|
|
2109
|
+
* @param {object} context
|
|
2110
|
+
* @param {Record<string, unknown>} [overrides]
|
|
2111
|
+
* @returns {Promise<{ total: number, delay: number, name?: string }>}
|
|
2112
|
+
*/
|
|
2113
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
2114
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
|
|
2115
|
+
total: "number default 10",
|
|
2116
|
+
delay: "number default 1000",
|
|
2117
|
+
name: "string"
|
|
2118
|
+
}, overrides);
|
|
2119
|
+
const total = Number(merged.total);
|
|
2120
|
+
const delay = Number(merged.delay);
|
|
2121
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
2122
|
+
throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
|
|
2123
|
+
}
|
|
2124
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
2125
|
+
throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
|
|
2126
|
+
}
|
|
2127
|
+
const out = { total, delay };
|
|
2128
|
+
if (typeof merged.name === "string" && merged.name.trim()) {
|
|
2129
|
+
out.name = merged.name.trim();
|
|
2130
|
+
}
|
|
2131
|
+
return out;
|
|
2132
|
+
}
|
|
2133
|
+
/**
|
|
2134
|
+
* @param {object} context
|
|
2135
|
+
* @param {object} task
|
|
2136
|
+
*/
|
|
2137
|
+
constructor(context, task) {
|
|
2138
|
+
super(context, task);
|
|
2139
|
+
this.stopRequested = false;
|
|
2140
|
+
this.stopAllowanceMs = 0;
|
|
2141
|
+
this.stopDecisionLogged = false;
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* Runner-facing stop signal. Records the allowance window so the main loop
|
|
2145
|
+
* can decide per-iteration whether to finish or abort early.
|
|
2146
|
+
*
|
|
2147
|
+
* @param {number} allowanceMs
|
|
2148
|
+
*/
|
|
2149
|
+
requestStop(allowanceMs) {
|
|
2150
|
+
this.stopRequested = true;
|
|
2151
|
+
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
2152
|
+
this.context.logger.warn?.(
|
|
2153
|
+
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Iterate `total` times, sleeping `delay` ms between ticks and reporting
|
|
2158
|
+
* progress every iteration. Validates params up front; invalid values short-
|
|
2159
|
+
* circuit to a structured failure without starting the loop.
|
|
2160
|
+
*
|
|
2161
|
+
* @param {(progress: object) => Promise<void>} reportProgress
|
|
2162
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
2163
|
+
*/
|
|
2164
|
+
async run(reportProgress) {
|
|
2165
|
+
const totalRaw = this.task?.params?.total ?? 10;
|
|
2166
|
+
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
2167
|
+
const nameRaw = this.task?.params?.name;
|
|
2168
|
+
const total = Number(totalRaw);
|
|
2169
|
+
const delay = Number(delayRaw);
|
|
2170
|
+
const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "sampleProcess";
|
|
2171
|
+
const errors = [];
|
|
1489
2172
|
if (!Number.isInteger(total) || total <= 0) {
|
|
1490
2173
|
errors.push('param "total" must be a positive integer');
|
|
1491
2174
|
}
|
|
@@ -1559,7 +2242,7 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
1559
2242
|
}
|
|
1560
2243
|
};
|
|
1561
2244
|
|
|
1562
|
-
// src/tasks/coreTasks/TaskShellCommand.
|
|
2245
|
+
// src/tasks/coreTasks/TaskShellCommand.js
|
|
1563
2246
|
var import_node_child_process = require("child_process");
|
|
1564
2247
|
function runShellCommand(command, cwd) {
|
|
1565
2248
|
return new Promise((resolve, reject) => {
|
|
@@ -1589,7 +2272,27 @@ function runShellCommand(command, cwd) {
|
|
|
1589
2272
|
});
|
|
1590
2273
|
});
|
|
1591
2274
|
}
|
|
1592
|
-
var TaskShellCommand = class extends
|
|
2275
|
+
var TaskShellCommand = class extends AbstractTask {
|
|
2276
|
+
/**
|
|
2277
|
+
* @param {object} context
|
|
2278
|
+
* @param {Record<string, unknown>} [overrides]
|
|
2279
|
+
* @returns {Promise<{ command: string, cwd?: string }>}
|
|
2280
|
+
*/
|
|
2281
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
2282
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
|
|
2283
|
+
command: "string",
|
|
2284
|
+
cwd: "string"
|
|
2285
|
+
}, overrides);
|
|
2286
|
+
const command = typeof merged.command === "string" ? merged.command.trim() : "";
|
|
2287
|
+
if (!command) {
|
|
2288
|
+
throw new ParamError('shellCommand: param "command" must be a non-empty string');
|
|
2289
|
+
}
|
|
2290
|
+
const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
|
|
2291
|
+
return cwd ? { command, cwd } : { command };
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2294
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
2295
|
+
*/
|
|
1593
2296
|
async run() {
|
|
1594
2297
|
const params = this.task?.params;
|
|
1595
2298
|
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
@@ -1638,8 +2341,8 @@ var TaskShellCommand = class extends TaskMaster {
|
|
|
1638
2341
|
}
|
|
1639
2342
|
};
|
|
1640
2343
|
|
|
1641
|
-
// src/tasks/coreTasks/TaskSystemInfo.
|
|
1642
|
-
var
|
|
2344
|
+
// src/tasks/coreTasks/TaskSystemInfo.js
|
|
2345
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
1643
2346
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
1644
2347
|
function toGb(valueBytes) {
|
|
1645
2348
|
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
@@ -1658,13 +2361,22 @@ async function getDiskStats() {
|
|
|
1658
2361
|
free: toGb(free)
|
|
1659
2362
|
};
|
|
1660
2363
|
}
|
|
1661
|
-
var TaskSystemInfo = class extends
|
|
2364
|
+
var TaskSystemInfo = class extends AbstractTask {
|
|
2365
|
+
/** Same UX expectation as `ping` — short probe, print the result. */
|
|
2366
|
+
static defaultWaitForResult = true;
|
|
2367
|
+
/** systemInfo takes no params. */
|
|
2368
|
+
static async resolveCustomParams() {
|
|
2369
|
+
return null;
|
|
2370
|
+
}
|
|
2371
|
+
/**
|
|
2372
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
2373
|
+
*/
|
|
1662
2374
|
async run() {
|
|
1663
2375
|
try {
|
|
1664
|
-
const totalMemory =
|
|
1665
|
-
const freeMemory =
|
|
2376
|
+
const totalMemory = import_node_os2.default.totalmem();
|
|
2377
|
+
const freeMemory = import_node_os2.default.freemem();
|
|
1666
2378
|
const usedMemory = totalMemory - freeMemory;
|
|
1667
|
-
const cpus =
|
|
2379
|
+
const cpus = import_node_os2.default.cpus();
|
|
1668
2380
|
const cpuUtilization = cpus.map((cpu) => {
|
|
1669
2381
|
const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
|
|
1670
2382
|
const usage = (total - cpu.times.idle) / total * 100;
|
|
@@ -1690,10 +2402,10 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
1690
2402
|
utilization: cpuUtilization
|
|
1691
2403
|
},
|
|
1692
2404
|
runtime: {
|
|
1693
|
-
platform:
|
|
1694
|
-
arch:
|
|
1695
|
-
uptimeSec:
|
|
1696
|
-
hostname:
|
|
2405
|
+
platform: import_node_os2.default.platform(),
|
|
2406
|
+
arch: import_node_os2.default.arch(),
|
|
2407
|
+
uptimeSec: import_node_os2.default.uptime(),
|
|
2408
|
+
hostname: import_node_os2.default.hostname()
|
|
1697
2409
|
}
|
|
1698
2410
|
};
|
|
1699
2411
|
this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
|
|
@@ -1710,8 +2422,31 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
1710
2422
|
}
|
|
1711
2423
|
};
|
|
1712
2424
|
|
|
1713
|
-
// src/tasks/coreTasks/TaskSumAB.
|
|
1714
|
-
var TaskSumAB = class extends
|
|
2425
|
+
// src/tasks/coreTasks/TaskSumAB.js
|
|
2426
|
+
var TaskSumAB = class extends AbstractTask {
|
|
2427
|
+
/** Short, deterministic — wait by default so callers see the sum. */
|
|
2428
|
+
static defaultWaitForResult = true;
|
|
2429
|
+
/**
|
|
2430
|
+
* @param {object} context
|
|
2431
|
+
* @param {Record<string, unknown>} [overrides]
|
|
2432
|
+
* @returns {Promise<{ a: number, b: number }>}
|
|
2433
|
+
*/
|
|
2434
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
2435
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
|
|
2436
|
+
a: "number",
|
|
2437
|
+
b: "number"
|
|
2438
|
+
}, overrides);
|
|
2439
|
+
if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
|
|
2440
|
+
throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
|
|
2441
|
+
}
|
|
2442
|
+
if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
|
|
2443
|
+
throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
|
|
2444
|
+
}
|
|
2445
|
+
return { a: merged.a, b: merged.b };
|
|
2446
|
+
}
|
|
2447
|
+
/**
|
|
2448
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
2449
|
+
*/
|
|
1715
2450
|
async run() {
|
|
1716
2451
|
const a = this.task?.params?.a;
|
|
1717
2452
|
const b = this.task?.params?.b;
|
|
@@ -1742,8 +2477,46 @@ var TaskSumAB = class extends TaskMaster {
|
|
|
1742
2477
|
}
|
|
1743
2478
|
};
|
|
1744
2479
|
|
|
1745
|
-
// src/tasks/coreTasks/TaskStopRunner.
|
|
1746
|
-
var TaskStopRunner = class extends
|
|
2480
|
+
// src/tasks/coreTasks/TaskStopRunner.js
|
|
2481
|
+
var TaskStopRunner = class extends AbstractTask {
|
|
2482
|
+
/**
|
|
2483
|
+
* Stop tasks must target a concrete instance — without `serviceName` the
|
|
2484
|
+
* row would race against any worker on the queue. Layered on top of the
|
|
2485
|
+
* envelope built by {@link AbstractTask.resolveParams}.
|
|
2486
|
+
*
|
|
2487
|
+
* @param {object} context
|
|
2488
|
+
* @param {Record<string, unknown>} [overrides]
|
|
2489
|
+
* @returns {Promise<object>}
|
|
2490
|
+
*/
|
|
2491
|
+
static async resolveParams(context, overrides = {}) {
|
|
2492
|
+
const main = await super.resolveParams(context, overrides);
|
|
2493
|
+
if (!main.serviceName) {
|
|
2494
|
+
throw new ParamError(
|
|
2495
|
+
"stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
return main;
|
|
2499
|
+
}
|
|
2500
|
+
/**
|
|
2501
|
+
* @param {object} context
|
|
2502
|
+
* @param {Record<string, unknown>} [overrides]
|
|
2503
|
+
* @returns {Promise<{ allowanceMs: number }>}
|
|
2504
|
+
*/
|
|
2505
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
2506
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
|
|
2507
|
+
allowanceMs: "number default 5000"
|
|
2508
|
+
}, overrides);
|
|
2509
|
+
const allowanceMs = Number(merged.allowanceMs);
|
|
2510
|
+
if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
|
|
2511
|
+
throw new ParamError(
|
|
2512
|
+
`stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
return { allowanceMs };
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2518
|
+
* @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
|
|
2519
|
+
*/
|
|
1747
2520
|
async run() {
|
|
1748
2521
|
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
1749
2522
|
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
@@ -1758,175 +2531,399 @@ var TaskStopRunner = class extends TaskMaster {
|
|
|
1758
2531
|
}
|
|
1759
2532
|
};
|
|
1760
2533
|
|
|
1761
|
-
// src/tasks/
|
|
2534
|
+
// src/tasks/coreTasks/TaskGetLogs.js
|
|
2535
|
+
var TaskGetLogs = class extends AbstractTask {
|
|
2536
|
+
/**
|
|
2537
|
+
* @param {object} context
|
|
2538
|
+
* @param {Record<string, unknown>} [overrides]
|
|
2539
|
+
* @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
|
|
2540
|
+
*/
|
|
2541
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
2542
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
|
|
2543
|
+
source: "string",
|
|
2544
|
+
resource: "string",
|
|
2545
|
+
tail: "number default 100",
|
|
2546
|
+
afterTs: "string"
|
|
2547
|
+
}, overrides);
|
|
2548
|
+
const source = typeof merged.source === "string" ? merged.source.trim() : "";
|
|
2549
|
+
const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
|
|
2550
|
+
if (!source) throw new ParamError('getLogs: param "source" is required');
|
|
2551
|
+
if (!resource) throw new ParamError('getLogs: param "resource" is required');
|
|
2552
|
+
let tail = Number(merged.tail);
|
|
2553
|
+
if (!Number.isFinite(tail) || tail < 1) tail = 100;
|
|
2554
|
+
tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
|
|
2555
|
+
const out = { source, resource, tail };
|
|
2556
|
+
if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
|
|
2557
|
+
out.afterTs = merged.afterTs.trim();
|
|
2558
|
+
}
|
|
2559
|
+
return out;
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* @param {unknown} _reportProgress Unused (single-shot read, no progress events).
|
|
2563
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
2564
|
+
*/
|
|
2565
|
+
async run(_reportProgress) {
|
|
2566
|
+
const p = this.task.params ?? {};
|
|
2567
|
+
const source = String(p.source ?? "").trim();
|
|
2568
|
+
const resource = String(p.resource ?? "").trim();
|
|
2569
|
+
const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
|
|
2570
|
+
const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
|
|
2571
|
+
if (!source || !resource) {
|
|
2572
|
+
return {
|
|
2573
|
+
success: false,
|
|
2574
|
+
results: { error: 'getLogs requires params "source" and "resource"' }
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
try {
|
|
2578
|
+
const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
|
|
2579
|
+
source,
|
|
2580
|
+
resource,
|
|
2581
|
+
tail,
|
|
2582
|
+
afterTs
|
|
2583
|
+
});
|
|
2584
|
+
return {
|
|
2585
|
+
success: true,
|
|
2586
|
+
results: { records, latestTs, source, resource }
|
|
2587
|
+
};
|
|
2588
|
+
} catch (e) {
|
|
2589
|
+
return {
|
|
2590
|
+
success: false,
|
|
2591
|
+
results: { error: e?.message ?? String(e) }
|
|
2592
|
+
};
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
};
|
|
2596
|
+
|
|
2597
|
+
// src/tasks/TasksRegistry.js
|
|
1762
2598
|
var TasksRegistry = class _TasksRegistry {
|
|
1763
|
-
|
|
2599
|
+
/**
|
|
2600
|
+
* @param {Record<string, Function>} [initial] Optional seed entries to copy in.
|
|
2601
|
+
*/
|
|
1764
2602
|
constructor(initial) {
|
|
2603
|
+
this.map = {};
|
|
1765
2604
|
if (initial) {
|
|
1766
2605
|
this.addMany(initial);
|
|
1767
2606
|
}
|
|
1768
2607
|
}
|
|
2608
|
+
/**
|
|
2609
|
+
* Build a registry pre-populated with every core task plus legacy aliases.
|
|
2610
|
+
* Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
|
|
2611
|
+
*
|
|
2612
|
+
* @returns {TasksRegistry}
|
|
2613
|
+
*/
|
|
1769
2614
|
static withCoreTasks() {
|
|
1770
|
-
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
|
|
2615
|
+
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("info", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner).add("getLogs", TaskGetLogs);
|
|
1771
2616
|
}
|
|
2617
|
+
/**
|
|
2618
|
+
* Register a single task class under a name. Overwrites any previous entry.
|
|
2619
|
+
*
|
|
2620
|
+
* @param {string} taskName
|
|
2621
|
+
* @param {Function} taskClass Subclass of `AbstractTask`.
|
|
2622
|
+
* @returns {this}
|
|
2623
|
+
*/
|
|
1772
2624
|
add(taskName, taskClass) {
|
|
1773
2625
|
this.map[taskName] = taskClass;
|
|
1774
2626
|
return this;
|
|
1775
2627
|
}
|
|
2628
|
+
/**
|
|
2629
|
+
* Bulk-register a name → class map. Later calls override earlier ones.
|
|
2630
|
+
*
|
|
2631
|
+
* @param {Record<string, Function>} entries
|
|
2632
|
+
* @returns {this}
|
|
2633
|
+
*/
|
|
1776
2634
|
addMany(entries) {
|
|
1777
2635
|
for (const [name, klass] of Object.entries(entries)) {
|
|
1778
2636
|
this.add(name, klass);
|
|
1779
2637
|
}
|
|
1780
2638
|
return this;
|
|
1781
2639
|
}
|
|
2640
|
+
/**
|
|
2641
|
+
* Look up a task class by name. Returns `undefined` when the name is unknown;
|
|
2642
|
+
* the runner treats that as "some other worker may handle this" and skips.
|
|
2643
|
+
*
|
|
2644
|
+
* @param {string} taskName
|
|
2645
|
+
* @returns {Function | undefined}
|
|
2646
|
+
*/
|
|
1782
2647
|
get(taskName) {
|
|
1783
2648
|
return this.map[taskName];
|
|
1784
2649
|
}
|
|
2650
|
+
/**
|
|
2651
|
+
* Strict variant of {@link get}: throws {@link ParamError} (with the list
|
|
2652
|
+
* of supported names) when `taskName` is unknown. Use from enqueuer code
|
|
2653
|
+
* paths where an unknown name is a hard CLI/programmer error.
|
|
2654
|
+
*
|
|
2655
|
+
* @param {string} taskName
|
|
2656
|
+
* @returns {Function}
|
|
2657
|
+
*/
|
|
2658
|
+
requireClass(taskName) {
|
|
2659
|
+
const TaskClass = taskName ? this.map[taskName] : void 0;
|
|
2660
|
+
if (!TaskClass) {
|
|
2661
|
+
const supported = this.listSupportedTasks().join(", ") || "(none)";
|
|
2662
|
+
throw new ParamError(
|
|
2663
|
+
`Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
return TaskClass;
|
|
2667
|
+
}
|
|
2668
|
+
/**
|
|
2669
|
+
* Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
|
|
2670
|
+
* from `overrides` or `context.params`, look up the class, and delegate
|
|
2671
|
+
* to its static {@link AbstractTask.resolveParams} with `name` seeded into
|
|
2672
|
+
* the overrides. The returned object is shaped for {@link enqueueTask}.
|
|
2673
|
+
*
|
|
2674
|
+
* Validation failures (unknown task, missing required custom params, etc.)
|
|
2675
|
+
* surface as {@link ParamError} so the caller aborts cleanly before any
|
|
2676
|
+
* row is inserted.
|
|
2677
|
+
*
|
|
2678
|
+
* @param {object} context
|
|
2679
|
+
* @param {Record<string, unknown>} [overrides]
|
|
2680
|
+
* @returns {Promise<object>}
|
|
2681
|
+
*/
|
|
2682
|
+
async resolveTaskParams(context, overrides = {}) {
|
|
2683
|
+
const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
|
|
2684
|
+
const fromCli = context.params.get("name", "string");
|
|
2685
|
+
const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
|
|
2686
|
+
const name = overrideName || cliName;
|
|
2687
|
+
if (!name) {
|
|
2688
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
2689
|
+
}
|
|
2690
|
+
const TaskClass = this.requireClass(name);
|
|
2691
|
+
return TaskClass.resolveParams(context, { ...overrides, name });
|
|
2692
|
+
}
|
|
2693
|
+
/**
|
|
2694
|
+
* Names of every registered task, sorted alphabetically (useful for CLI output
|
|
2695
|
+
* and allowlist sanity checks).
|
|
2696
|
+
*
|
|
2697
|
+
* @returns {string[]}
|
|
2698
|
+
*/
|
|
1785
2699
|
listSupportedTasks() {
|
|
1786
2700
|
return Object.keys(this.map).sort();
|
|
1787
2701
|
}
|
|
2702
|
+
/**
|
|
2703
|
+
* Shallow copy of the internal map, for handing to `addMany` on another registry
|
|
2704
|
+
* or for serialization.
|
|
2705
|
+
*
|
|
2706
|
+
* @returns {Record<string, Function>}
|
|
2707
|
+
*/
|
|
1788
2708
|
toObject() {
|
|
1789
2709
|
return { ...this.map };
|
|
1790
2710
|
}
|
|
1791
2711
|
};
|
|
1792
2712
|
|
|
1793
|
-
// src/tasks/
|
|
2713
|
+
// src/tasks/serviceTaskAllowlist.js
|
|
2714
|
+
var SERVICE_TASK_NAMES = [
|
|
2715
|
+
"ping",
|
|
2716
|
+
"stop",
|
|
2717
|
+
"stopRunner",
|
|
2718
|
+
"shellCommand",
|
|
2719
|
+
"systemInfo",
|
|
2720
|
+
"info",
|
|
2721
|
+
"getLogs"
|
|
2722
|
+
];
|
|
2723
|
+
function normalizeAllowedTasks(value) {
|
|
2724
|
+
if (!value) return void 0;
|
|
2725
|
+
if (Array.isArray(value)) {
|
|
2726
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
2727
|
+
return out2.length ? out2 : void 0;
|
|
2728
|
+
}
|
|
2729
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
2730
|
+
return out.length ? out : void 0;
|
|
2731
|
+
}
|
|
2732
|
+
function mergeAllowedTasksWithServiceTasks(names) {
|
|
2733
|
+
const set = /* @__PURE__ */ new Set([...SERVICE_TASK_NAMES, ...names ?? []]);
|
|
2734
|
+
return Array.from(set).sort();
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
// src/tasks/taskScriptRunner.js
|
|
1794
2738
|
var import_node_child_process2 = require("child_process");
|
|
2739
|
+
var MAX_PROGRESS_TEXT_LEN = 4e3;
|
|
1795
2740
|
function toCliArgs(args = []) {
|
|
1796
2741
|
return args.filter((a) => typeof a === "string" && a.length > 0);
|
|
1797
2742
|
}
|
|
1798
2743
|
function formatChildLogPrefix(task) {
|
|
1799
|
-
return `${task.
|
|
2744
|
+
return `${task.name}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
|
|
1800
2745
|
}
|
|
1801
|
-
|
|
1802
|
-
|
|
2746
|
+
function isProgressPayload(payload) {
|
|
2747
|
+
if (!payload || typeof payload !== "object") return false;
|
|
2748
|
+
if (payload.level !== "progress") return false;
|
|
2749
|
+
const count = Number(payload.count);
|
|
2750
|
+
const total = Number(payload.total);
|
|
2751
|
+
return Number.isFinite(count) && Number.isFinite(total) && total > 0;
|
|
2752
|
+
}
|
|
2753
|
+
function formatProgressText(payload, fallbackPrefix) {
|
|
2754
|
+
const pfx = payload.prefix ? `${payload.prefix} ` : fallbackPrefix ? `${fallbackPrefix} ` : "";
|
|
2755
|
+
const label = typeof payload.message === "string" && payload.message ? `${payload.message} ` : "";
|
|
2756
|
+
return `${pfx}${label}${payload.count}/${payload.total}`;
|
|
2757
|
+
}
|
|
2758
|
+
function forwardChildLogToParent(context, prefix, message) {
|
|
2759
|
+
if (!message || typeof message !== "object") return;
|
|
2760
|
+
const text = typeof message.message === "string" ? message.message : null;
|
|
2761
|
+
if (!text) return;
|
|
2762
|
+
const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
|
|
2763
|
+
const line = `[child:${prefix}] ${text}`;
|
|
2764
|
+
const logger = context.logger;
|
|
2765
|
+
switch (level) {
|
|
2766
|
+
case "error":
|
|
2767
|
+
case "fatal":
|
|
2768
|
+
logger.error?.(line);
|
|
2769
|
+
return;
|
|
2770
|
+
case "warn":
|
|
2771
|
+
case "warning":
|
|
2772
|
+
logger.warn?.(line);
|
|
2773
|
+
return;
|
|
2774
|
+
case "debug":
|
|
2775
|
+
logger.debug?.(line);
|
|
2776
|
+
return;
|
|
2777
|
+
case "info":
|
|
2778
|
+
default:
|
|
2779
|
+
logger.info?.(line);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
function buildNodeArgs(scriptPath, cliArgs) {
|
|
1803
2783
|
const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
|
|
1804
2784
|
const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
2785
|
+
return hasTsRuntimeInParent ? [...inheritedExecArgs, scriptPath, ...cliArgs] : ["--import", "tsx", scriptPath, ...cliArgs];
|
|
2786
|
+
}
|
|
2787
|
+
function resolveTasksTableName(context) {
|
|
2788
|
+
return context.tasksQueueName || context.params?.get?.("table") || "tasks";
|
|
2789
|
+
}
|
|
2790
|
+
function announceIpcFileLogsTarget(context, options) {
|
|
2791
|
+
if (!options.ipcFileLogs) return;
|
|
2792
|
+
const logsDir = resolveIpcFileLogsDir(context, options.ipcFileLogs);
|
|
2793
|
+
const enabledRaw = context.params?.get?.("tasksLogsEnabled");
|
|
2794
|
+
const logsEnabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
2795
|
+
context.logger.info?.(
|
|
2796
|
+
`[tasks] IPC file logs: ${logsDir}` + (logsEnabled ? "" : " (tasksLogsEnabled=false; not persisted)")
|
|
1819
2797
|
);
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
let
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
|
|
1833
|
-
}).catch((error) => {
|
|
1834
|
-
context.logger.warn?.(
|
|
1835
|
-
`[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
1836
|
-
);
|
|
1837
|
-
});
|
|
1838
|
-
if (options.onProgress) {
|
|
1839
|
-
progressCallbackChain = progressCallbackChain.then(async () => {
|
|
1840
|
-
await options.onProgress?.(text.slice(0, 4e3));
|
|
1841
|
-
}).catch((error) => {
|
|
1842
|
-
context.logger.warn?.(
|
|
1843
|
-
`[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
1844
|
-
);
|
|
2798
|
+
}
|
|
2799
|
+
function createSerializedQueue() {
|
|
2800
|
+
let chain = Promise.resolve();
|
|
2801
|
+
return {
|
|
2802
|
+
push(fn) {
|
|
2803
|
+
chain = chain.then(fn, () => {
|
|
2804
|
+
}).catch(() => {
|
|
2805
|
+
});
|
|
2806
|
+
return chain;
|
|
2807
|
+
},
|
|
2808
|
+
drain() {
|
|
2809
|
+
return chain.catch(() => {
|
|
1845
2810
|
});
|
|
1846
2811
|
}
|
|
1847
2812
|
};
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
2813
|
+
}
|
|
2814
|
+
async function runNodeTaskScript(context, options) {
|
|
2815
|
+
const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
|
|
2816
|
+
const nodeArgs = buildNodeArgs(options.scriptPath, cliArgs);
|
|
2817
|
+
const child = (0, import_node_child_process2.spawn)(process.execPath, nodeArgs, {
|
|
2818
|
+
cwd: options.cwd || process.cwd(),
|
|
2819
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
2820
|
+
env: {
|
|
2821
|
+
...process.env,
|
|
2822
|
+
TASK_ID: options.task.id,
|
|
2823
|
+
TASK_NAME: options.task.name,
|
|
2824
|
+
TASK_OPID: options.task.opid || ""
|
|
2825
|
+
}
|
|
2826
|
+
});
|
|
2827
|
+
announceIpcFileLogsTarget(context, options);
|
|
2828
|
+
const prefix = formatChildLogPrefix(options.task);
|
|
2829
|
+
const tasksTable = resolveTasksTableName(context);
|
|
2830
|
+
const progressQueue = createSerializedQueue();
|
|
2831
|
+
const state = {
|
|
2832
|
+
stdout: "",
|
|
2833
|
+
stderr: "",
|
|
2834
|
+
workerResult: null,
|
|
2835
|
+
hadErrorMessage: false
|
|
1861
2836
|
};
|
|
1862
|
-
|
|
2837
|
+
const writeProgress = (text) => {
|
|
2838
|
+
const trimmed = typeof text === "string" ? text.slice(0, MAX_PROGRESS_TEXT_LEN) : "";
|
|
2839
|
+
if (!trimmed) return;
|
|
2840
|
+
progressQueue.push(async () => {
|
|
2841
|
+
const db = context.db;
|
|
2842
|
+
if (db) {
|
|
2843
|
+
try {
|
|
2844
|
+
await db(tasksTable).where({ id: options.task.id }).update({ progress: trimmed });
|
|
2845
|
+
} catch (error) {
|
|
2846
|
+
context.logger.warn?.(
|
|
2847
|
+
`[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
2848
|
+
);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
if (options.onProgress) {
|
|
2852
|
+
try {
|
|
2853
|
+
await options.onProgress(trimmed);
|
|
2854
|
+
} catch (error) {
|
|
2855
|
+
context.logger.warn?.(
|
|
2856
|
+
`[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
2857
|
+
);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
});
|
|
2861
|
+
};
|
|
2862
|
+
child.stdout?.on("data", (chunk) => {
|
|
1863
2863
|
const text = String(chunk);
|
|
1864
|
-
stdout += text;
|
|
2864
|
+
state.stdout += text;
|
|
1865
2865
|
if (text.trim()) {
|
|
1866
2866
|
context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
1867
|
-
updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
|
|
1868
2867
|
}
|
|
1869
2868
|
});
|
|
1870
|
-
child.stderr
|
|
2869
|
+
child.stderr?.on("data", (chunk) => {
|
|
1871
2870
|
const text = String(chunk);
|
|
1872
|
-
stderr += text;
|
|
2871
|
+
state.stderr += text;
|
|
1873
2872
|
if (text.trim()) {
|
|
1874
2873
|
context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
1875
2874
|
}
|
|
1876
2875
|
});
|
|
1877
2876
|
child.on("message", (message) => {
|
|
1878
2877
|
if (message && typeof message === "object" && "__taskWorkerResult" in message) {
|
|
1879
|
-
workerResult = message.__taskWorkerResult;
|
|
2878
|
+
state.workerResult = message.__taskWorkerResult;
|
|
1880
2879
|
return;
|
|
1881
2880
|
}
|
|
1882
2881
|
if (message && typeof message === "object") {
|
|
1883
2882
|
const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
|
|
1884
2883
|
if (level === "error" || level === "fatal") {
|
|
1885
|
-
hadErrorMessage = true;
|
|
2884
|
+
state.hadErrorMessage = true;
|
|
1886
2885
|
}
|
|
1887
2886
|
}
|
|
1888
|
-
appendTaskIpcLog(context, options.task, message);
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
const countNum = Number(String(message.count).trim());
|
|
1894
|
-
const totalNum = Number(message.total);
|
|
1895
|
-
if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
|
|
1896
|
-
context.logger.progress(message.message || "progress", {
|
|
1897
|
-
prefix: message.prefix || prefix,
|
|
1898
|
-
count: countNum,
|
|
1899
|
-
total: totalNum
|
|
1900
|
-
});
|
|
1901
|
-
} else {
|
|
1902
|
-
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
1903
|
-
}
|
|
1904
|
-
} else {
|
|
1905
|
-
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
1906
|
-
}
|
|
2887
|
+
appendTaskIpcLog(context, options.task, message, options.ipcFileLogs);
|
|
2888
|
+
try {
|
|
2889
|
+
options.onChildIpcMessage?.(message);
|
|
2890
|
+
} catch (e) {
|
|
2891
|
+
context.logger.warn?.(`[tasks] onChildIpcMessage failed: ${e?.message ?? String(e)}`);
|
|
1907
2892
|
}
|
|
2893
|
+
if (isProgressPayload(message)) {
|
|
2894
|
+
context.logger.progress(message.message || "progress", {
|
|
2895
|
+
prefix: message.prefix || prefix,
|
|
2896
|
+
count: Number(message.count),
|
|
2897
|
+
total: Number(message.total)
|
|
2898
|
+
});
|
|
2899
|
+
writeProgress(formatProgressText(message, prefix));
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
forwardChildLogToParent(context, prefix, message);
|
|
1908
2903
|
});
|
|
1909
2904
|
return await new Promise((resolve, reject) => {
|
|
1910
2905
|
child.on("error", (error) => reject(error));
|
|
1911
2906
|
child.on("close", (exitCode, signal) => {
|
|
1912
|
-
|
|
2907
|
+
void (async () => {
|
|
2908
|
+
await flushTaskIpcLogs(context);
|
|
2909
|
+
await progressQueue.drain();
|
|
1913
2910
|
resolve({
|
|
1914
2911
|
exitCode,
|
|
1915
2912
|
signal,
|
|
1916
|
-
stdout: stdout.trim(),
|
|
1917
|
-
stderr: stderr.trim(),
|
|
1918
|
-
workerResult,
|
|
1919
|
-
hadErrorMessage
|
|
2913
|
+
stdout: state.stdout.trim(),
|
|
2914
|
+
stderr: state.stderr.trim(),
|
|
2915
|
+
workerResult: state.workerResult,
|
|
2916
|
+
hadErrorMessage: state.hadErrorMessage
|
|
1920
2917
|
});
|
|
1921
|
-
});
|
|
2918
|
+
})();
|
|
1922
2919
|
});
|
|
1923
2920
|
});
|
|
1924
2921
|
}
|
|
1925
2922
|
|
|
1926
|
-
// src/tasks/index.
|
|
2923
|
+
// src/tasks/index.js
|
|
1927
2924
|
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
1928
2925
|
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
1929
|
-
function
|
|
2926
|
+
function getDb3(context) {
|
|
1930
2927
|
const db = context.db;
|
|
1931
2928
|
if (!db) {
|
|
1932
2929
|
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
@@ -1938,22 +2935,13 @@ function normalizeRegistry(registry) {
|
|
|
1938
2935
|
if (registry instanceof TasksRegistry) return registry;
|
|
1939
2936
|
return new TasksRegistry().addMany(registry);
|
|
1940
2937
|
}
|
|
1941
|
-
function
|
|
1942
|
-
if (!value) return void 0;
|
|
1943
|
-
if (Array.isArray(value)) {
|
|
1944
|
-
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
1945
|
-
return out2.length ? out2 : void 0;
|
|
1946
|
-
}
|
|
1947
|
-
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
1948
|
-
return out.length ? out : void 0;
|
|
1949
|
-
}
|
|
1950
|
-
async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
|
|
2938
|
+
async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
|
|
1951
2939
|
return enqueueTask(context, {
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
task: "stopRunner",
|
|
2940
|
+
queueName,
|
|
2941
|
+
name: "stopRunner",
|
|
1955
2942
|
params: { allowanceMs },
|
|
1956
|
-
priority:
|
|
2943
|
+
priority: 0,
|
|
2944
|
+
serviceGroup
|
|
1957
2945
|
});
|
|
1958
2946
|
}
|
|
1959
2947
|
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
@@ -1970,19 +2958,21 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
|
|
|
1970
2958
|
context.emitter.emit("stop", allowanceMs);
|
|
1971
2959
|
}
|
|
1972
2960
|
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
1973
|
-
const db =
|
|
1974
|
-
const taskName = row.
|
|
2961
|
+
const db = getDb3(context);
|
|
2962
|
+
const taskName = row.name;
|
|
1975
2963
|
const TaskClass = registry.get(taskName);
|
|
1976
|
-
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
1977
2964
|
if (!TaskClass) {
|
|
1978
2965
|
const err = { message: `Unknown task "${taskName}"` };
|
|
1979
|
-
await db(historyTable).insert(
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2966
|
+
await db(historyTable).insert(
|
|
2967
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
2968
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
2969
|
+
success: false,
|
|
2970
|
+
status: "failed",
|
|
2971
|
+
status_changed_at: db.fn.now(),
|
|
2972
|
+
params: toJsonColumn(row.params),
|
|
2973
|
+
results: toJsonColumn(err)
|
|
2974
|
+
})
|
|
2975
|
+
);
|
|
1986
2976
|
if (row.schedule) {
|
|
1987
2977
|
await db(tasksTable).where({ id: row.id }).update({
|
|
1988
2978
|
started_at: null,
|
|
@@ -1990,7 +2980,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
1990
2980
|
success: false,
|
|
1991
2981
|
results: toJsonColumn(err),
|
|
1992
2982
|
past_due: null,
|
|
1993
|
-
|
|
2983
|
+
status: "paused",
|
|
2984
|
+
status_changed_at: db.fn.now(),
|
|
1994
2985
|
progress: LOCKED_BY_ERROR_MESSAGE
|
|
1995
2986
|
});
|
|
1996
2987
|
} else {
|
|
@@ -2017,13 +3008,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
2017
3008
|
} finally {
|
|
2018
3009
|
runningTaskInstances.delete(row.id);
|
|
2019
3010
|
}
|
|
2020
|
-
await db(historyTable).insert(
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
3011
|
+
await db(historyTable).insert(
|
|
3012
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
3013
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
3014
|
+
success,
|
|
3015
|
+
status: success ? "completed" : "failed",
|
|
3016
|
+
status_changed_at: db.fn.now(),
|
|
3017
|
+
params: toJsonColumn(row.params),
|
|
3018
|
+
results: toJsonColumn(results)
|
|
3019
|
+
})
|
|
3020
|
+
);
|
|
2027
3021
|
if (!success) {
|
|
2028
3022
|
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
2029
3023
|
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
@@ -2038,19 +3032,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
2038
3032
|
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
2039
3033
|
appendTaskIpcLog(context, row, {
|
|
2040
3034
|
level: "error",
|
|
2041
|
-
message: `[tasks] task failed: ${row.
|
|
3035
|
+
message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
2042
3036
|
details: results
|
|
2043
3037
|
});
|
|
2044
3038
|
}
|
|
2045
3039
|
if (row.schedule) {
|
|
2046
3040
|
if (success) {
|
|
3041
|
+
let nextRunAt = null;
|
|
3042
|
+
try {
|
|
3043
|
+
nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
|
|
3044
|
+
} catch (e) {
|
|
3045
|
+
context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
|
|
3046
|
+
}
|
|
2047
3047
|
await db(tasksTable).where({ id: row.id }).update({
|
|
2048
3048
|
started_at: null,
|
|
2049
3049
|
completed_at: /* @__PURE__ */ new Date(),
|
|
2050
3050
|
success,
|
|
2051
3051
|
results: toJsonColumn(results),
|
|
2052
3052
|
progress: null,
|
|
2053
|
-
past_due: null
|
|
3053
|
+
past_due: null,
|
|
3054
|
+
status: "idle",
|
|
3055
|
+
status_changed_at: db.fn.now(),
|
|
3056
|
+
next_run_at: nextRunAt,
|
|
3057
|
+
// Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
|
|
3058
|
+
service_name: null,
|
|
3059
|
+
server_name: null,
|
|
3060
|
+
instance_number: null
|
|
2054
3061
|
});
|
|
2055
3062
|
} else {
|
|
2056
3063
|
await db(tasksTable).where({ id: row.id }).update({
|
|
@@ -2058,7 +3065,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
2058
3065
|
completed_at: /* @__PURE__ */ new Date(),
|
|
2059
3066
|
success,
|
|
2060
3067
|
results: toJsonColumn(results),
|
|
2061
|
-
|
|
3068
|
+
status: "paused",
|
|
3069
|
+
status_changed_at: db.fn.now(),
|
|
2062
3070
|
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
2063
3071
|
past_due: null
|
|
2064
3072
|
});
|
|
@@ -2070,209 +3078,402 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
2070
3078
|
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
2071
3079
|
return { stopRunnerRequested, stopAllowanceMs };
|
|
2072
3080
|
}
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
3081
|
+
function shuffleTaskRowsInPlace(rows) {
|
|
3082
|
+
for (let i = rows.length - 1; i > 0; i--) {
|
|
3083
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
3084
|
+
const t = rows[i];
|
|
3085
|
+
rows[i] = rows[j];
|
|
3086
|
+
rows[j] = t;
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
|
|
3090
|
+
const db = getDb3(context);
|
|
3091
|
+
let query = db(tasksTable).where({ status: "idle" }).where(function() {
|
|
3092
|
+
this.whereNull("service_group").orWhere({ service_group: serviceGroup });
|
|
3093
|
+
}).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "asc" }]).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
3094
|
if (taskNames && taskNames.length > 0) {
|
|
2077
|
-
query = query.whereIn("
|
|
3095
|
+
query = query.whereIn("name", taskNames);
|
|
3096
|
+
}
|
|
3097
|
+
if (runnerIdentity) {
|
|
3098
|
+
query = query.where(function() {
|
|
3099
|
+
this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
|
|
3100
|
+
}).where(function() {
|
|
3101
|
+
this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
|
|
3102
|
+
}).where(function() {
|
|
3103
|
+
this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
|
|
3104
|
+
});
|
|
3105
|
+
} else {
|
|
3106
|
+
query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
|
|
2078
3107
|
}
|
|
2079
3108
|
const candidates = await query;
|
|
3109
|
+
shuffleTaskRowsInPlace(candidates);
|
|
2080
3110
|
for (const row of candidates) {
|
|
2081
3111
|
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
2082
3112
|
continue;
|
|
2083
3113
|
}
|
|
2084
|
-
const TaskClass = registry.get(row.
|
|
2085
|
-
if (TaskClass) {
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
3114
|
+
const TaskClass = registry.get(row.name);
|
|
3115
|
+
if (!TaskClass) {
|
|
3116
|
+
continue;
|
|
3117
|
+
}
|
|
3118
|
+
const taskInstance = new TaskClass(context, row);
|
|
3119
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
3120
|
+
if (reason) {
|
|
3121
|
+
if (!row.past_due) {
|
|
3122
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
3123
|
+
past_due: db.fn.now(),
|
|
3124
|
+
progress: String(reason)
|
|
3125
|
+
});
|
|
2096
3126
|
}
|
|
3127
|
+
continue;
|
|
2097
3128
|
}
|
|
2098
|
-
const
|
|
3129
|
+
const claimPatch = {
|
|
3130
|
+
started_at: db.fn.now(),
|
|
3131
|
+
status: "running",
|
|
3132
|
+
status_changed_at: db.fn.now()
|
|
3133
|
+
};
|
|
3134
|
+
if (runnerIdentity) {
|
|
3135
|
+
claimPatch.service_name = runnerIdentity.service_name;
|
|
3136
|
+
claimPatch.server_name = runnerIdentity.server_name;
|
|
3137
|
+
claimPatch.instance_number = runnerIdentity.instance_number;
|
|
3138
|
+
}
|
|
3139
|
+
const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
|
|
2099
3140
|
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
2100
3141
|
if (claimed) return claimed;
|
|
2101
3142
|
}
|
|
2102
3143
|
return null;
|
|
2103
3144
|
}
|
|
2104
3145
|
async function runTasksLoop(context, options) {
|
|
2105
|
-
const
|
|
3146
|
+
const queueName = options.queueName ?? "tasks";
|
|
2106
3147
|
const target = options.target;
|
|
2107
3148
|
const pollMs = options.pollMs ?? 1e3;
|
|
2108
|
-
const
|
|
3149
|
+
const claimJitterMs = options.claimJitterMs ?? 0;
|
|
3150
|
+
const maxParallel = options.maxParallel ?? 32;
|
|
2109
3151
|
const scanLimit = options.scanLimit ?? 100;
|
|
2110
3152
|
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
2111
3153
|
const registry = normalizeRegistry(options.registry);
|
|
2112
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
3154
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
2113
3155
|
if (!target) throw new Error("runTasksLoop: target is required");
|
|
3156
|
+
context.tasksQueueName = queueName;
|
|
2114
3157
|
const runningPromises = /* @__PURE__ */ new Set();
|
|
2115
3158
|
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
2116
3159
|
let runningStopControlPromise = null;
|
|
2117
3160
|
let stopRequested = false;
|
|
2118
3161
|
let stopAllowanceMs = 5e3;
|
|
2119
|
-
context.
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
)
|
|
2130
|
-
|
|
2131
|
-
|
|
3162
|
+
context.tasksRunnerStop = false;
|
|
3163
|
+
let registryReg = null;
|
|
3164
|
+
let registryInterval = null;
|
|
3165
|
+
let runnerIdentity = null;
|
|
3166
|
+
const hbGroup = options.runnerServiceGroup?.trim();
|
|
3167
|
+
if (hbGroup) {
|
|
3168
|
+
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
3169
|
+
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
3170
|
+
const defaultMeta = {
|
|
3171
|
+
component: "tasks-runner",
|
|
3172
|
+
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
3173
|
+
};
|
|
3174
|
+
registryReg = await registerInServicesRegistry(context, {
|
|
3175
|
+
queueName,
|
|
3176
|
+
target,
|
|
3177
|
+
serviceGroup: hbGroup,
|
|
3178
|
+
serviceName: options.runnerServiceName,
|
|
3179
|
+
instanceNumber: options.runnerInstanceNumber,
|
|
3180
|
+
staleMs,
|
|
3181
|
+
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
3182
|
+
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
3183
|
+
metadata: options.runnerMetadata ?? defaultMeta
|
|
3184
|
+
});
|
|
3185
|
+
runnerIdentity = {
|
|
3186
|
+
service_name: registryReg.serviceName,
|
|
3187
|
+
server_name: import_node_os3.default.hostname(),
|
|
3188
|
+
instance_number: registryReg.instanceNumber
|
|
3189
|
+
};
|
|
3190
|
+
registryInterval = setInterval(() => {
|
|
3191
|
+
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
3192
|
+
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
3193
|
+
});
|
|
3194
|
+
}, hbIntervalMs);
|
|
3195
|
+
}
|
|
3196
|
+
try {
|
|
3197
|
+
while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
|
|
3198
|
+
if (!runningStopControlPromise) {
|
|
3199
|
+
const claimedStopTask = await claimNextRunnableTask(
|
|
2132
3200
|
context,
|
|
2133
3201
|
tasksTable,
|
|
2134
|
-
|
|
2135
|
-
claimedStopTask,
|
|
3202
|
+
target,
|
|
2136
3203
|
registry,
|
|
2137
|
-
|
|
2138
|
-
|
|
3204
|
+
10,
|
|
3205
|
+
["stopRunner", "stop"],
|
|
3206
|
+
runnerIdentity
|
|
3207
|
+
);
|
|
3208
|
+
if (claimedStopTask) {
|
|
3209
|
+
runningStopControlPromise = executeClaimedTask(
|
|
3210
|
+
context,
|
|
3211
|
+
tasksTable,
|
|
3212
|
+
historyTable,
|
|
3213
|
+
claimedStopTask,
|
|
3214
|
+
registry,
|
|
3215
|
+
runningTaskInstances
|
|
3216
|
+
).then(async (outcome) => {
|
|
3217
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
3218
|
+
stopRequested = true;
|
|
3219
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
3220
|
+
context.tasksRunnerStop = true;
|
|
3221
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
3222
|
+
}
|
|
3223
|
+
}).finally(() => {
|
|
3224
|
+
runningStopControlPromise = null;
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
if (claimJitterMs > 0) {
|
|
3229
|
+
await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
|
|
3230
|
+
}
|
|
3231
|
+
while (runningPromises.size < maxParallel) {
|
|
3232
|
+
const claimed = await claimNextRunnableTask(
|
|
3233
|
+
context,
|
|
3234
|
+
tasksTable,
|
|
3235
|
+
target,
|
|
3236
|
+
registry,
|
|
3237
|
+
scanLimit,
|
|
3238
|
+
allowedTasks,
|
|
3239
|
+
runnerIdentity
|
|
3240
|
+
);
|
|
3241
|
+
if (!claimed) break;
|
|
3242
|
+
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
2139
3243
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
2140
3244
|
stopRequested = true;
|
|
2141
3245
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
2142
|
-
context.
|
|
3246
|
+
context.tasksRunnerStop = true;
|
|
2143
3247
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
2144
3248
|
}
|
|
2145
3249
|
}).finally(() => {
|
|
2146
|
-
|
|
3250
|
+
runningPromises.delete(p);
|
|
2147
3251
|
});
|
|
3252
|
+
runningPromises.add(p);
|
|
3253
|
+
}
|
|
3254
|
+
const wakePromises = [...runningPromises];
|
|
3255
|
+
if (runningStopControlPromise) {
|
|
3256
|
+
wakePromises.push(runningStopControlPromise);
|
|
3257
|
+
}
|
|
3258
|
+
if (wakePromises.length === 0) {
|
|
3259
|
+
await sleepMs(pollMs);
|
|
3260
|
+
} else {
|
|
3261
|
+
const safe = wakePromises.map((p) => p.catch(() => void 0));
|
|
3262
|
+
await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
|
|
2148
3263
|
}
|
|
2149
3264
|
}
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
context,
|
|
2153
|
-
tasksTable,
|
|
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);
|
|
3265
|
+
if (context.isStop() && !stopRequested) {
|
|
3266
|
+
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
2171
3267
|
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
3268
|
+
if (runningPromises.size > 0) {
|
|
3269
|
+
if (stopRequested) {
|
|
3270
|
+
await Promise.race([
|
|
3271
|
+
Promise.allSettled(Array.from(runningPromises)),
|
|
3272
|
+
sleepMs(stopAllowanceMs).then(() => {
|
|
3273
|
+
context.logger.warn?.(
|
|
3274
|
+
`[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
|
|
3275
|
+
);
|
|
3276
|
+
})
|
|
3277
|
+
]);
|
|
3278
|
+
} else {
|
|
3279
|
+
await Promise.allSettled(Array.from(runningPromises));
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
} finally {
|
|
3283
|
+
if (registryInterval) {
|
|
3284
|
+
clearInterval(registryInterval);
|
|
3285
|
+
registryInterval = null;
|
|
3286
|
+
}
|
|
3287
|
+
if (registryReg) {
|
|
3288
|
+
await unregisterServicesRegistry(context, registryReg).catch((err) => {
|
|
3289
|
+
context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
|
|
3290
|
+
});
|
|
3291
|
+
registryReg = null;
|
|
3292
|
+
delete context.servicesRegistry;
|
|
3293
|
+
delete context.runnerHeartbeat;
|
|
2189
3294
|
}
|
|
2190
3295
|
}
|
|
2191
3296
|
}
|
|
2192
3297
|
async function waitForTaskResult(context, taskId, options = {}) {
|
|
2193
|
-
const db =
|
|
2194
|
-
const
|
|
3298
|
+
const db = getDb3(context);
|
|
3299
|
+
const queueName = options.queueName ?? "tasks";
|
|
2195
3300
|
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
2196
3301
|
const pollMs = options.pollMs ?? 500;
|
|
2197
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
3302
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
2198
3303
|
const deadline = Date.now() + timeoutMs;
|
|
3304
|
+
const waitStartedAt = /* @__PURE__ */ new Date();
|
|
3305
|
+
let cachedNameOpid = null;
|
|
3306
|
+
async function historySinceWait(name, opid) {
|
|
3307
|
+
let q = db(historyTable).where({ name }).where("completed_at", ">=", waitStartedAt);
|
|
3308
|
+
if (opid == null || opid === "") {
|
|
3309
|
+
q = q.whereNull("opid");
|
|
3310
|
+
} else {
|
|
3311
|
+
q = q.where({ opid });
|
|
3312
|
+
}
|
|
3313
|
+
return await q.orderBy("completed_at", "desc").first();
|
|
3314
|
+
}
|
|
2199
3315
|
while (Date.now() <= deadline) {
|
|
2200
|
-
const
|
|
2201
|
-
if (
|
|
3316
|
+
const legacy = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
|
|
3317
|
+
if (legacy) {
|
|
3318
|
+
return legacy;
|
|
3319
|
+
}
|
|
2202
3320
|
const pending = await db(tasksTable).where({ id: taskId }).first();
|
|
2203
|
-
if (
|
|
2204
|
-
|
|
2205
|
-
|
|
3321
|
+
if (pending) {
|
|
3322
|
+
cachedNameOpid = { name: pending.name, opid: pending.opid };
|
|
3323
|
+
const done = await historySinceWait(pending.name, pending.opid);
|
|
3324
|
+
if (done) {
|
|
3325
|
+
return done;
|
|
3326
|
+
}
|
|
3327
|
+
} else if (cachedNameOpid) {
|
|
3328
|
+
const done = await historySinceWait(cachedNameOpid.name, cachedNameOpid.opid);
|
|
3329
|
+
if (done) {
|
|
3330
|
+
return done;
|
|
3331
|
+
}
|
|
3332
|
+
return null;
|
|
3333
|
+
} else {
|
|
3334
|
+
return null;
|
|
2206
3335
|
}
|
|
2207
3336
|
await sleepMs(pollMs);
|
|
2208
3337
|
}
|
|
2209
3338
|
return null;
|
|
2210
3339
|
}
|
|
2211
3340
|
var TasksManager = class _TasksManager {
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
3341
|
+
/**
|
|
3342
|
+
* @param {object} context
|
|
3343
|
+
* @param {{
|
|
3344
|
+
* queueName?: string,
|
|
3345
|
+
* target?: string,
|
|
3346
|
+
* recreateTaskTables?: boolean,
|
|
3347
|
+
* pollMs?: number,
|
|
3348
|
+
* claimJitterMs?: number,
|
|
3349
|
+
* maxParallel?: number,
|
|
3350
|
+
* scanLimit?: number,
|
|
3351
|
+
* allowedTasks?: string | string[],
|
|
3352
|
+
* registry?: TasksRegistry | Record<string, Function>,
|
|
3353
|
+
* runnerServiceGroup?: string,
|
|
3354
|
+
* runnerServiceName?: string,
|
|
3355
|
+
* runnerInstanceNumber?: number,
|
|
3356
|
+
* runnerHeartbeatIntervalMs?: number,
|
|
3357
|
+
* runnerHeartbeatStaleMs?: number,
|
|
3358
|
+
* runnerGroupMaxInstances?: number,
|
|
3359
|
+
* runnerEnforceMaxInstances?: boolean,
|
|
3360
|
+
* runnerMetadata?: Record<string, unknown>,
|
|
3361
|
+
* }} [options]
|
|
3362
|
+
*/
|
|
2221
3363
|
constructor(context, options = {}) {
|
|
2222
3364
|
this.context = context;
|
|
2223
|
-
this.
|
|
3365
|
+
this.queueName = options.queueName ?? "tasks";
|
|
2224
3366
|
this.target = options.target ?? "localRunner";
|
|
2225
3367
|
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
2226
3368
|
this.pollMs = options.pollMs ?? 1e3;
|
|
3369
|
+
this.claimJitterMs = options.claimJitterMs ?? 0;
|
|
2227
3370
|
this.maxParallel = options.maxParallel ?? 1;
|
|
2228
3371
|
this.scanLimit = options.scanLimit ?? 100;
|
|
2229
3372
|
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
2230
3373
|
this.registry = normalizeRegistry(options.registry);
|
|
3374
|
+
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
3375
|
+
this.runnerServiceName = options.runnerServiceName;
|
|
3376
|
+
this.runnerInstanceNumber = options.runnerInstanceNumber;
|
|
3377
|
+
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
3378
|
+
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
3379
|
+
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
3380
|
+
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
3381
|
+
this.runnerMetadata = options.runnerMetadata;
|
|
2231
3382
|
}
|
|
3383
|
+
/**
|
|
3384
|
+
* Preferred factory: reads defaults from `context.params` (module namespace
|
|
3385
|
+
* `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
|
|
3386
|
+
* and inline options in one consistent resolver.
|
|
3387
|
+
*
|
|
3388
|
+
* @param {object} context
|
|
3389
|
+
* @param {ConstructorParameters<typeof TasksManager>[1]} [options]
|
|
3390
|
+
* @returns {TasksManager}
|
|
3391
|
+
*/
|
|
2232
3392
|
static init(context, options = {}) {
|
|
2233
3393
|
const defs = {
|
|
2234
3394
|
table: "string default tasks",
|
|
2235
3395
|
target: "string default localRunner",
|
|
2236
3396
|
recreateTaskTables: "boolean default false",
|
|
2237
3397
|
pollMs: "number default 1000",
|
|
3398
|
+
claimJitterMs: "number default 0",
|
|
2238
3399
|
maxParallel: "number default 1",
|
|
2239
3400
|
scanLimit: "number default 100",
|
|
2240
|
-
allowedTasks: "string"
|
|
3401
|
+
allowedTasks: "string",
|
|
3402
|
+
runnerServiceGroup: "string",
|
|
3403
|
+
runnerServiceName: "string",
|
|
3404
|
+
runnerInstanceNumber: "number",
|
|
3405
|
+
runnerHeartbeatIntervalMs: "number default 10000",
|
|
3406
|
+
runnerHeartbeatStaleMs: "number default 45000",
|
|
3407
|
+
runnerGroupMaxInstances: "number",
|
|
3408
|
+
runnerEnforceMaxInstances: "boolean default true"
|
|
2241
3409
|
};
|
|
2242
|
-
const discovered = context.params.getAllForModule(defs);
|
|
3410
|
+
const discovered = context.params.getAllForModule("tasks", defs);
|
|
2243
3411
|
const resolved = {
|
|
2244
|
-
|
|
3412
|
+
queueName: discovered.table,
|
|
2245
3413
|
target: discovered.target,
|
|
2246
3414
|
recreateTaskTables: discovered.recreateTaskTables,
|
|
2247
3415
|
pollMs: discovered.pollMs,
|
|
3416
|
+
claimJitterMs: discovered.claimJitterMs,
|
|
2248
3417
|
maxParallel: discovered.maxParallel,
|
|
2249
3418
|
scanLimit: discovered.scanLimit,
|
|
2250
3419
|
allowedTasks: discovered.allowedTasks,
|
|
3420
|
+
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
3421
|
+
runnerServiceName: discovered.runnerServiceName,
|
|
3422
|
+
runnerInstanceNumber: discovered.runnerInstanceNumber,
|
|
3423
|
+
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
3424
|
+
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
3425
|
+
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
3426
|
+
runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
|
|
2251
3427
|
...options
|
|
2252
3428
|
};
|
|
2253
3429
|
return new _TasksManager(context, resolved);
|
|
2254
3430
|
}
|
|
3431
|
+
/**
|
|
3432
|
+
* Idempotently ensure the three backing tables exist for this queue.
|
|
3433
|
+
*
|
|
3434
|
+
* @param {{ recreate?: boolean }} [options]
|
|
3435
|
+
* @returns {Promise<void>}
|
|
3436
|
+
*/
|
|
2255
3437
|
async ensureTaskTables(options = {}) {
|
|
2256
3438
|
await ensureTaskTables(this.context, {
|
|
2257
|
-
|
|
3439
|
+
queueName: this.queueName,
|
|
2258
3440
|
recreate: options.recreate ?? this.recreateTaskTables
|
|
2259
3441
|
});
|
|
2260
3442
|
}
|
|
3443
|
+
/**
|
|
3444
|
+
* Start the runner loop using this manager's resolved config. Per-call
|
|
3445
|
+
* options override the stored defaults, but `runnerMetadata` still falls
|
|
3446
|
+
* through when omitted.
|
|
3447
|
+
*
|
|
3448
|
+
* @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
|
|
3449
|
+
* @returns {Promise<void>}
|
|
3450
|
+
*/
|
|
2261
3451
|
async runTasksLoop(options = {}) {
|
|
2262
3452
|
await runTasksLoop(this.context, {
|
|
2263
|
-
|
|
3453
|
+
queueName: options.queueName ?? this.queueName,
|
|
2264
3454
|
target: options.target ?? this.target,
|
|
2265
3455
|
pollMs: options.pollMs ?? this.pollMs,
|
|
3456
|
+
claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
|
|
2266
3457
|
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
2267
3458
|
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
2268
3459
|
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
2269
|
-
registry: options.registry ?? this.registry
|
|
3460
|
+
registry: options.registry ?? this.registry,
|
|
3461
|
+
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
3462
|
+
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
3463
|
+
runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
|
|
3464
|
+
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
3465
|
+
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
3466
|
+
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
3467
|
+
runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
|
|
3468
|
+
runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
|
|
2270
3469
|
});
|
|
2271
3470
|
}
|
|
2272
3471
|
};
|
|
2273
3472
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2274
3473
|
0 && (module.exports = {
|
|
2275
|
-
|
|
3474
|
+
AbstractTask,
|
|
3475
|
+
SERVICE_TASK_NAMES,
|
|
3476
|
+
TaskGetLogs,
|
|
2276
3477
|
TaskPing,
|
|
2277
3478
|
TaskSampleProcess,
|
|
2278
3479
|
TaskShellCommand,
|
|
@@ -2282,13 +3483,36 @@ var TasksManager = class _TasksManager {
|
|
|
2282
3483
|
TasksManager,
|
|
2283
3484
|
TasksRegistry,
|
|
2284
3485
|
appendTaskIpcLog,
|
|
3486
|
+
convertPattern,
|
|
2285
3487
|
defaultTasksRegistry,
|
|
2286
3488
|
enqueueStopTask,
|
|
2287
3489
|
enqueueTask,
|
|
2288
3490
|
ensureTaskTables,
|
|
3491
|
+
flushTaskIpcLogs,
|
|
3492
|
+
ipcFileLogsTableNameForSourceResource,
|
|
3493
|
+
listAliveRunnerHeartbeats,
|
|
3494
|
+
listServicesRegistry,
|
|
3495
|
+
matchesParsedPattern,
|
|
3496
|
+
mergeAllowedTasksWithServiceTasks,
|
|
3497
|
+
nextTimeMatch,
|
|
3498
|
+
normalizeAllowedTasks,
|
|
2289
3499
|
queueToTableNames,
|
|
3500
|
+
readTaskIpcLogsSnapshot,
|
|
3501
|
+
registerInServicesRegistry,
|
|
3502
|
+
registerRunnerHeartbeat,
|
|
3503
|
+
resolveAsterisks,
|
|
3504
|
+
resolveIpcFileLogsDir,
|
|
3505
|
+
resolveRanges,
|
|
3506
|
+
resolveSteps,
|
|
2290
3507
|
runNodeTaskScript,
|
|
2291
3508
|
runTasksLoop,
|
|
3509
|
+
taskHistoryInsertFromQueueRow,
|
|
3510
|
+
timeMatcher,
|
|
3511
|
+
touchRunnerHeartbeat,
|
|
3512
|
+
touchServicesRegistry,
|
|
3513
|
+
unregisterRunnerHeartbeat,
|
|
3514
|
+
unregisterServicesRegistry,
|
|
3515
|
+
updateServicesRegistryMetadata,
|
|
2292
3516
|
updateTaskProgress,
|
|
2293
3517
|
waitForTaskResult
|
|
2294
3518
|
});
|