@nmakarov/cli-toolkit 0.21.0 → 0.25.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/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 +1370 -623
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +1415 -667
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +173 -158
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +172 -151
- 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 -37
- package/dist/http-client2.cjs.map +1 -1
- package/dist/http-client2.js +34 -34
- package/dist/http-client2.js.map +1 -1
- package/dist/index.cjs +1831 -713
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1837 -713
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +93 -68
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +108 -82
- 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 +18 -9
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +18 -6
- 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 +1354 -501
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +1369 -527
- 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 +32 -47
- package/scripts/ssm/{parse-cli.ts → parse-cli.js} +4 -4
- package/scripts/ssm/{ssm-admin.ts → ssm-admin.js} +12 -12
- package/scripts/ssm/{ssm-pull.ts → ssm-pull.js} +10 -13
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,31 @@ __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,
|
|
47
51
|
listAliveRunnerHeartbeats: () => listServicesRegistry,
|
|
48
52
|
listServicesRegistry: () => listServicesRegistry,
|
|
53
|
+
matchesParsedPattern: () => matchesParsedPattern,
|
|
54
|
+
mergeAllowedTasksWithServiceTasks: () => mergeAllowedTasksWithServiceTasks,
|
|
55
|
+
nextTimeMatch: () => nextTimeMatch,
|
|
56
|
+
normalizeAllowedTasks: () => normalizeAllowedTasks,
|
|
49
57
|
queueToTableNames: () => queueToTableNames,
|
|
58
|
+
readTaskIpcLogsSnapshot: () => readTaskIpcLogsSnapshot,
|
|
50
59
|
registerInServicesRegistry: () => registerInServicesRegistry,
|
|
51
60
|
registerRunnerHeartbeat: () => registerInServicesRegistry,
|
|
61
|
+
resolveAsterisks: () => resolveAsterisks,
|
|
62
|
+
resolveIpcFileLogsDir: () => resolveIpcFileLogsDir,
|
|
63
|
+
resolveRanges: () => resolveRanges,
|
|
64
|
+
resolveSteps: () => resolveSteps,
|
|
52
65
|
runNodeTaskScript: () => runNodeTaskScript,
|
|
53
66
|
runTasksLoop: () => runTasksLoop,
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
taskHistoryInsertFromQueueRow: () => taskHistoryInsertFromQueueRow,
|
|
68
|
+
timeMatcher: () => timeMatcher,
|
|
56
69
|
touchRunnerHeartbeat: () => touchServicesRegistry,
|
|
57
70
|
touchServicesRegistry: () => touchServicesRegistry,
|
|
58
71
|
unregisterRunnerHeartbeat: () => unregisterServicesRegistry,
|
|
@@ -62,8 +75,9 @@ __export(tasks_exports, {
|
|
|
62
75
|
waitForTaskResult: () => waitForTaskResult
|
|
63
76
|
});
|
|
64
77
|
module.exports = __toCommonJS(tasks_exports);
|
|
78
|
+
var import_node_os3 = __toESM(require("os"), 1);
|
|
65
79
|
|
|
66
|
-
// src/utils/date-utils.
|
|
80
|
+
// src/utils/date-utils.js
|
|
67
81
|
function isTimestampFolder(folderName) {
|
|
68
82
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
|
|
69
83
|
if (!isoRegex.test(folderName)) {
|
|
@@ -73,7 +87,7 @@ function isTimestampFolder(folderName) {
|
|
|
73
87
|
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
74
88
|
}
|
|
75
89
|
|
|
76
|
-
// src/utils/fs-utils.
|
|
90
|
+
// src/utils/fs-utils.js
|
|
77
91
|
var import_fs = __toESM(require("fs"), 1);
|
|
78
92
|
var import_path = __toESM(require("path"), 1);
|
|
79
93
|
async function ensurePath(...pathParts) {
|
|
@@ -97,7 +111,7 @@ function getFileExtension(dataType) {
|
|
|
97
111
|
}
|
|
98
112
|
}
|
|
99
113
|
|
|
100
|
-
// src/utils/os-utils.
|
|
114
|
+
// src/utils/os-utils.js
|
|
101
115
|
var import_fs2 = __toESM(require("fs"), 1);
|
|
102
116
|
var import_path2 = __toESM(require("path"), 1);
|
|
103
117
|
var import_child_process = require("child_process");
|
|
@@ -126,7 +140,7 @@ function getFreeDiskSpace(targetPath) {
|
|
|
126
140
|
}
|
|
127
141
|
}
|
|
128
142
|
|
|
129
|
-
// src/utils/format-utils.
|
|
143
|
+
// src/utils/format-utils.js
|
|
130
144
|
function bytesToHumanReadable(bytes) {
|
|
131
145
|
if (bytes === 0) return "0 B";
|
|
132
146
|
const k = 1024;
|
|
@@ -135,7 +149,7 @@ function bytesToHumanReadable(bytes) {
|
|
|
135
149
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
136
150
|
}
|
|
137
151
|
|
|
138
|
-
// src/utils/core-utils.
|
|
152
|
+
// src/utils/core-utils.js
|
|
139
153
|
function sleepMs(ms) {
|
|
140
154
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
141
155
|
}
|
|
@@ -144,14 +158,82 @@ function toJsonColumn(value) {
|
|
|
144
158
|
return JSON.stringify(value);
|
|
145
159
|
}
|
|
146
160
|
|
|
147
|
-
// src/tasks/servicesRegistry.
|
|
148
|
-
var import_node_crypto2 = require("crypto");
|
|
149
|
-
var import_promises = require("fs/promises");
|
|
161
|
+
// src/tasks/servicesRegistry.js
|
|
150
162
|
var import_node_os = __toESM(require("os"), 1);
|
|
151
|
-
var import_node_path = __toESM(require("path"), 1);
|
|
152
163
|
|
|
153
|
-
// src/tasks/taskUtils.
|
|
164
|
+
// src/tasks/taskUtils.js
|
|
154
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
|
|
155
237
|
function getDb(context) {
|
|
156
238
|
const db = context.db;
|
|
157
239
|
if (!db) {
|
|
@@ -159,113 +241,117 @@ function getDb(context) {
|
|
|
159
241
|
}
|
|
160
242
|
return db;
|
|
161
243
|
}
|
|
162
|
-
function queueToTableNames(
|
|
163
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
|
|
164
|
-
throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
|
|
165
|
-
}
|
|
244
|
+
function queueToTableNames(queueName) {
|
|
166
245
|
return {
|
|
167
|
-
tasksTable:
|
|
168
|
-
historyTable: `${
|
|
246
|
+
tasksTable: queueName,
|
|
247
|
+
historyTable: `${queueName}_history`,
|
|
248
|
+
registryTable: `${queueName}_services_registry`
|
|
169
249
|
};
|
|
170
250
|
}
|
|
171
|
-
function
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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;
|
|
278
|
+
return {
|
|
279
|
+
...snapshot,
|
|
280
|
+
...overrides
|
|
281
|
+
};
|
|
176
282
|
}
|
|
177
283
|
async function ensureTaskTables(context, options = {}) {
|
|
178
|
-
const
|
|
284
|
+
const queueName = options.queueName ?? "tasks";
|
|
179
285
|
const recreate = options.recreate ?? false;
|
|
180
286
|
const db = getDb(context);
|
|
181
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
287
|
+
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
182
288
|
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
183
289
|
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
290
|
+
const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
|
|
184
291
|
if (recreate) {
|
|
185
292
|
await db.schema.dropTableIfExists(historyTable);
|
|
186
293
|
await db.schema.dropTableIfExists(tasksTable);
|
|
294
|
+
await db.schema.dropTableIfExists(registryTable);
|
|
187
295
|
}
|
|
188
296
|
if (needsTasks) {
|
|
189
297
|
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
190
298
|
await db.schema.createTable(tasksTable, (t) => {
|
|
191
|
-
t
|
|
192
|
-
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
193
|
-
t.timestamp("started_at");
|
|
194
|
-
t.timestamp("completed_at");
|
|
195
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
196
|
-
t.text("schedule");
|
|
197
|
-
t.timestamp("past_due").defaultTo(null);
|
|
198
|
-
t.text("target").notNullable();
|
|
199
|
-
t.text("task").notNullable();
|
|
200
|
-
t.json("params");
|
|
201
|
-
t.text("opid");
|
|
202
|
-
t.timestamp("paused_at").defaultTo(null);
|
|
203
|
-
t.text("progress");
|
|
204
|
-
t.boolean("success");
|
|
205
|
-
t.json("results");
|
|
206
|
-
});
|
|
207
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
208
|
-
t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
|
|
209
|
-
t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
|
|
210
|
-
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
299
|
+
defineTasksTable(t, db, tasksTable);
|
|
211
300
|
});
|
|
212
301
|
}
|
|
213
302
|
if (needsHistory) {
|
|
214
303
|
await db.schema.createTable(historyTable, (t) => {
|
|
215
|
-
t
|
|
216
|
-
t.timestamp("created_at").notNullable();
|
|
217
|
-
t.timestamp("started_at");
|
|
218
|
-
t.timestamp("completed_at");
|
|
219
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
220
|
-
t.text("schedule");
|
|
221
|
-
t.timestamp("past_due").defaultTo(null);
|
|
222
|
-
t.text("target").notNullable();
|
|
223
|
-
t.text("task").notNullable();
|
|
224
|
-
t.json("params");
|
|
225
|
-
t.text("opid");
|
|
226
|
-
t.text("progress");
|
|
227
|
-
t.boolean("success");
|
|
228
|
-
t.json("results");
|
|
229
|
-
});
|
|
230
|
-
await db.schema.alterTable(historyTable, (t) => {
|
|
231
|
-
t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
|
|
232
|
-
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
304
|
+
defineTasksTable(t, db, historyTable);
|
|
233
305
|
});
|
|
234
306
|
}
|
|
235
|
-
const registryTable = servicesRegistryTable(queue);
|
|
236
|
-
const needsRegistry = !await db.tableExists(registryTable);
|
|
237
307
|
if (needsRegistry) {
|
|
238
308
|
await db.schema.createTable(registryTable, (t) => {
|
|
239
309
|
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
240
|
-
t.
|
|
241
|
-
t.text("queue").notNullable();
|
|
310
|
+
t.text("queue_name").notNullable();
|
|
242
311
|
t.text("service_group").notNullable();
|
|
312
|
+
t.integer("instance_number").notNullable().defaultTo(1);
|
|
243
313
|
t.text("service_name").notNullable();
|
|
244
|
-
t.text("
|
|
245
|
-
t.text("hostname");
|
|
314
|
+
t.text("server_name").notNullable();
|
|
246
315
|
t.integer("pid");
|
|
247
316
|
t.json("metadata");
|
|
248
317
|
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
249
318
|
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
250
|
-
t.unique(["
|
|
251
|
-
t.index(["
|
|
252
|
-
t.index(["
|
|
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`);
|
|
253
322
|
});
|
|
254
323
|
}
|
|
255
324
|
}
|
|
256
325
|
async function enqueueTask(context, options) {
|
|
257
326
|
const db = getDb(context);
|
|
258
|
-
const
|
|
259
|
-
const { tasksTable } = queueToTableNames(
|
|
327
|
+
const queueName = options.queueName ?? "tasks";
|
|
328
|
+
const { tasksTable } = queueToTableNames(queueName);
|
|
260
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
|
+
}
|
|
261
341
|
await db(tasksTable).insert({
|
|
262
342
|
id,
|
|
263
|
-
|
|
264
|
-
task: options.task,
|
|
343
|
+
name,
|
|
265
344
|
params: toJsonColumn(options.params ?? null),
|
|
266
345
|
opid: options.opid ?? null,
|
|
267
|
-
priority: options.priority ??
|
|
268
|
-
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()
|
|
269
355
|
});
|
|
270
356
|
return id;
|
|
271
357
|
}
|
|
@@ -276,7 +362,7 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
|
276
362
|
});
|
|
277
363
|
}
|
|
278
364
|
|
|
279
|
-
// src/tasks/servicesRegistry.
|
|
365
|
+
// src/tasks/servicesRegistry.js
|
|
280
366
|
function getDb2(context) {
|
|
281
367
|
const db = context.db;
|
|
282
368
|
if (!db) {
|
|
@@ -290,7 +376,7 @@ function parseMetadataColumn(value) {
|
|
|
290
376
|
if (typeof value === "string") {
|
|
291
377
|
try {
|
|
292
378
|
const p = JSON.parse(value);
|
|
293
|
-
return p && typeof p === "object" && !Array.isArray(
|
|
379
|
+
return p && typeof p === "object" && !Array.isArray(value) ? p : {};
|
|
294
380
|
} catch {
|
|
295
381
|
return {};
|
|
296
382
|
}
|
|
@@ -300,6 +386,7 @@ function parseMetadataColumn(value) {
|
|
|
300
386
|
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
301
387
|
intake: 1,
|
|
302
388
|
harvest: 1,
|
|
389
|
+
harvester: 0,
|
|
303
390
|
loader: 0,
|
|
304
391
|
photos: 0,
|
|
305
392
|
photosprocessor: 0,
|
|
@@ -309,25 +396,6 @@ function sanitizeNamePart(raw) {
|
|
|
309
396
|
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
310
397
|
return s.slice(0, 80) || "runner";
|
|
311
398
|
}
|
|
312
|
-
function identityFilePath(identityDir, queue, serviceGroup) {
|
|
313
|
-
const safeQ = sanitizeNamePart(queue);
|
|
314
|
-
const safeG = sanitizeNamePart(serviceGroup);
|
|
315
|
-
return import_node_path.default.join(identityDir, `${safeQ}_${safeG}.json`);
|
|
316
|
-
}
|
|
317
|
-
async function readIdentityFile(filePath) {
|
|
318
|
-
try {
|
|
319
|
-
const text = await (0, import_promises.readFile)(filePath, "utf8");
|
|
320
|
-
const parsed = JSON.parse(text);
|
|
321
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
322
|
-
} catch {
|
|
323
|
-
return {};
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
async function writeIdentityFile(filePath, data) {
|
|
327
|
-
await (0, import_promises.mkdir)(import_node_path.default.dirname(filePath), { recursive: true });
|
|
328
|
-
await (0, import_promises.writeFile)(filePath, `${JSON.stringify(data, null, 2)}
|
|
329
|
-
`, "utf8");
|
|
330
|
-
}
|
|
331
399
|
function resolveMaxInstances(serviceGroup, override) {
|
|
332
400
|
if (override !== void 0 && Number.isFinite(override)) {
|
|
333
401
|
return Math.max(0, Math.floor(Number(override)));
|
|
@@ -335,179 +403,190 @@ function resolveMaxInstances(serviceGroup, override) {
|
|
|
335
403
|
const g = serviceGroup.trim().toLowerCase();
|
|
336
404
|
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
337
405
|
}
|
|
338
|
-
async function countAliveInGroup(db, registryTable,
|
|
406
|
+
async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
|
|
339
407
|
const cutoff = new Date(Date.now() - staleMs);
|
|
340
|
-
let q = db(registryTable).where({
|
|
341
|
-
if (
|
|
342
|
-
q = q.whereNot("
|
|
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);
|
|
343
411
|
}
|
|
344
412
|
const row = await q.count("id as count").first();
|
|
345
413
|
return Number(row?.count ?? 0);
|
|
346
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
|
+
}
|
|
347
425
|
function isUniqueViolation(error) {
|
|
348
426
|
const code = error?.code ?? error?.errno;
|
|
349
427
|
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
350
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
|
+
}
|
|
351
456
|
async function registerInServicesRegistry(context, options) {
|
|
352
457
|
const db = getDb2(context);
|
|
353
|
-
const registryTable =
|
|
458
|
+
const registryTable = queueToTableNames(options.queueName).registryTable;
|
|
354
459
|
const serviceGroup = options.serviceGroup.trim();
|
|
355
460
|
if (!serviceGroup) {
|
|
356
461
|
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
357
462
|
}
|
|
358
|
-
const
|
|
359
|
-
let identity = await readIdentityFile(identityPath);
|
|
360
|
-
let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : (0, import_node_crypto2.randomUUID)();
|
|
361
|
-
identity.instanceId = instanceId;
|
|
362
|
-
await writeIdentityFile(identityPath, identity);
|
|
363
|
-
const hostname = import_node_os.default.hostname();
|
|
463
|
+
const serverName = import_node_os.default.hostname();
|
|
364
464
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
365
|
-
const meta =
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
await db(registryTable).where({ instance_id: instanceId }).update({
|
|
369
|
-
target: options.target,
|
|
370
|
-
hostname,
|
|
371
|
-
pid,
|
|
372
|
-
metadata: meta,
|
|
373
|
-
last_seen_at: db.fn.now()
|
|
374
|
-
});
|
|
375
|
-
const serviceName = String(existing.service_name);
|
|
376
|
-
identity.serviceName = serviceName;
|
|
377
|
-
await writeIdentityFile(identityPath, identity);
|
|
378
|
-
const reg = {
|
|
379
|
-
instanceId,
|
|
380
|
-
serviceName,
|
|
381
|
-
serviceGroup,
|
|
382
|
-
queue: options.queue,
|
|
383
|
-
target: options.target,
|
|
384
|
-
rowId: String(existing.id)
|
|
385
|
-
};
|
|
386
|
-
context.servicesRegistry = reg;
|
|
387
|
-
context.runnerHeartbeat = reg;
|
|
388
|
-
context.logger.info?.(
|
|
389
|
-
`[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
|
|
390
|
-
);
|
|
391
|
-
return {
|
|
392
|
-
instanceId,
|
|
393
|
-
serviceName,
|
|
394
|
-
serviceGroup,
|
|
395
|
-
queue: options.queue,
|
|
396
|
-
target: options.target,
|
|
397
|
-
rowId: String(existing.id),
|
|
398
|
-
registryTable
|
|
399
|
-
};
|
|
400
|
-
}
|
|
465
|
+
const meta = buildMetadata(options);
|
|
466
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
467
|
+
const hostBase = sanitizeNamePart(serverName);
|
|
401
468
|
const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
options.queue,
|
|
406
|
-
serviceGroup,
|
|
407
|
-
options.staleMs,
|
|
408
|
-
instanceId
|
|
409
|
-
);
|
|
410
|
-
if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
|
|
411
|
-
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
|
|
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}).`;
|
|
412
472
|
if (options.enforceMaxInstances) {
|
|
413
473
|
throw new Error(msg);
|
|
414
474
|
}
|
|
415
475
|
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
416
476
|
}
|
|
417
|
-
const
|
|
418
|
-
const
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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;
|
|
437
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;
|
|
438
528
|
}
|
|
439
|
-
}
|
|
440
|
-
let inserted;
|
|
441
|
-
for (const candidate of eachServiceNameCandidate(baseCandidates)) {
|
|
442
529
|
try {
|
|
443
530
|
const rows = await db(registryTable).insert({
|
|
444
|
-
|
|
445
|
-
queue: options.queue,
|
|
531
|
+
queue_name: options.queueName,
|
|
446
532
|
service_group: serviceGroup,
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
533
|
+
instance_number: instanceNumber,
|
|
534
|
+
service_name: serviceNameRaw,
|
|
535
|
+
server_name: serverName,
|
|
450
536
|
pid,
|
|
451
537
|
metadata: meta,
|
|
452
|
-
last_seen_at: db.fn.now()
|
|
538
|
+
last_seen_at: db.fn.now(),
|
|
539
|
+
created_at: db.fn.now()
|
|
453
540
|
}).returning(["id", "service_name"]);
|
|
454
541
|
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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) : "";
|
|
458
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;
|
|
459
563
|
} catch (error) {
|
|
460
564
|
if (!isUniqueViolation(error)) {
|
|
461
565
|
throw error;
|
|
462
566
|
}
|
|
567
|
+
context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
|
|
463
568
|
}
|
|
464
569
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
`[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
identity.serviceName = inserted.service_name;
|
|
471
|
-
await writeIdentityFile(identityPath, identity);
|
|
472
|
-
const regNew = {
|
|
473
|
-
instanceId,
|
|
474
|
-
serviceName: inserted.service_name,
|
|
475
|
-
serviceGroup,
|
|
476
|
-
queue: options.queue,
|
|
477
|
-
target: options.target,
|
|
478
|
-
rowId: inserted.id
|
|
479
|
-
};
|
|
480
|
-
context.servicesRegistry = regNew;
|
|
481
|
-
context.runnerHeartbeat = regNew;
|
|
482
|
-
context.logger.info?.(
|
|
483
|
-
`[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
|
|
570
|
+
throw new Error(
|
|
571
|
+
`[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
|
|
484
572
|
);
|
|
485
|
-
return {
|
|
486
|
-
instanceId,
|
|
487
|
-
serviceName: inserted.service_name,
|
|
488
|
-
serviceGroup,
|
|
489
|
-
queue: options.queue,
|
|
490
|
-
target: options.target,
|
|
491
|
-
rowId: inserted.id,
|
|
492
|
-
registryTable
|
|
493
|
-
};
|
|
494
573
|
}
|
|
495
574
|
async function touchServicesRegistry(context, registration) {
|
|
496
575
|
const db = getDb2(context);
|
|
497
|
-
const
|
|
576
|
+
const serverName = import_node_os.default.hostname();
|
|
498
577
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
499
|
-
await db(registration.registryTable).where({
|
|
578
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
500
579
|
last_seen_at: db.fn.now(),
|
|
501
|
-
|
|
580
|
+
server_name: serverName,
|
|
502
581
|
pid
|
|
503
582
|
});
|
|
504
583
|
}
|
|
505
584
|
async function updateServicesRegistryMetadata(context, registration, patch) {
|
|
506
585
|
const db = getDb2(context);
|
|
507
|
-
const row = await db(registration.registryTable).where({
|
|
586
|
+
const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
|
|
508
587
|
const prev = parseMetadataColumn(row?.metadata);
|
|
509
588
|
const merged = { ...prev, ...patch };
|
|
510
|
-
await db(registration.registryTable).where({
|
|
589
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
511
590
|
metadata: toJsonColumn(merged),
|
|
512
591
|
last_seen_at: db.fn.now()
|
|
513
592
|
});
|
|
@@ -515,14 +594,14 @@ async function updateServicesRegistryMetadata(context, registration, patch) {
|
|
|
515
594
|
}
|
|
516
595
|
async function unregisterServicesRegistry(context, registration) {
|
|
517
596
|
const db = getDb2(context);
|
|
518
|
-
await db(registration.registryTable).where({
|
|
519
|
-
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName}
|
|
597
|
+
await db(registration.registryTable).where({ id: registration.rowId }).delete();
|
|
598
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
|
|
520
599
|
}
|
|
521
|
-
async function listServicesRegistry(context, options = {
|
|
600
|
+
async function listServicesRegistry(context, options = { queueName: "tasks" }) {
|
|
522
601
|
const db = getDb2(context);
|
|
523
602
|
const staleMs = options.staleMs ?? 6e4;
|
|
524
603
|
const cutoff = new Date(Date.now() - staleMs);
|
|
525
|
-
const table =
|
|
604
|
+
const table = queueToTableNames(options.queueName).registryTable;
|
|
526
605
|
let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
|
|
527
606
|
if (options.serviceGroup?.trim()) {
|
|
528
607
|
q = q.where({ service_group: options.serviceGroup.trim() });
|
|
@@ -530,11 +609,14 @@ async function listServicesRegistry(context, options = { queue: "tasks" }) {
|
|
|
530
609
|
return await q;
|
|
531
610
|
}
|
|
532
611
|
|
|
533
|
-
// src/
|
|
612
|
+
// src/tasks/taskLogs.js
|
|
613
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
614
|
+
|
|
615
|
+
// src/filedatabase/index.js
|
|
534
616
|
var import_fs3 = __toESM(require("fs"), 1);
|
|
535
617
|
var import_path3 = __toESM(require("path"), 1);
|
|
536
618
|
|
|
537
|
-
// src/filedatabase/serializers.
|
|
619
|
+
// src/filedatabase/serializers.js
|
|
538
620
|
function detectDataType(data) {
|
|
539
621
|
if (Array.isArray(data)) {
|
|
540
622
|
return "json-array";
|
|
@@ -566,7 +648,7 @@ function deserializeData(rawData, dataType) {
|
|
|
566
648
|
}
|
|
567
649
|
}
|
|
568
650
|
|
|
569
|
-
// src/errors.
|
|
651
|
+
// src/errors.js
|
|
570
652
|
var FrameworkError = class extends Error {
|
|
571
653
|
constructor(message) {
|
|
572
654
|
super(message);
|
|
@@ -586,7 +668,7 @@ var FileDatabaseError = class extends FrameworkError {
|
|
|
586
668
|
}
|
|
587
669
|
};
|
|
588
670
|
|
|
589
|
-
// src/filedatabase/index.
|
|
671
|
+
// src/filedatabase/index.js
|
|
590
672
|
var FileDatabase = class _FileDatabase {
|
|
591
673
|
basePath;
|
|
592
674
|
namespace;
|
|
@@ -672,7 +754,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
672
754
|
if (errors.length) {
|
|
673
755
|
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
674
756
|
}
|
|
675
|
-
|
|
757
|
+
const parts = [this.basePath, this.namespace];
|
|
676
758
|
if (this.tableName) {
|
|
677
759
|
parts.push(...this.tableName.split("/"));
|
|
678
760
|
}
|
|
@@ -1165,14 +1247,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
1165
1247
|
* Prepare the instance for read or write operations
|
|
1166
1248
|
* This discovers state and sets up internal members based on mode and current data
|
|
1167
1249
|
*/
|
|
1168
|
-
async prepare(
|
|
1250
|
+
async prepare(options) {
|
|
1251
|
+
const { write, read, version, deferInitialVersion } = options;
|
|
1169
1252
|
if (write) {
|
|
1170
1253
|
if (this.versioned) {
|
|
1171
1254
|
if (this.currentVersion === null) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1255
|
+
if (!deferInitialVersion) {
|
|
1256
|
+
await this.makeNewVersion();
|
|
1257
|
+
this.metadata = this.getDefaultMetadata();
|
|
1258
|
+
this.metadata.version = this.currentVersion;
|
|
1259
|
+
this.makeNewFile();
|
|
1260
|
+
}
|
|
1176
1261
|
} else {
|
|
1177
1262
|
if (!this.metadata.files.length) {
|
|
1178
1263
|
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
@@ -1282,7 +1367,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
1282
1367
|
if (options.forceNewVersion && !this.versioned) {
|
|
1283
1368
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
1284
1369
|
}
|
|
1285
|
-
await this.prepare({ write: true });
|
|
1370
|
+
await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
|
|
1286
1371
|
const incomingDataType = detectDataType(data);
|
|
1287
1372
|
this.metadata.dataType = incomingDataType;
|
|
1288
1373
|
if (options.forceNewVersion) {
|
|
@@ -1576,7 +1661,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
1576
1661
|
}
|
|
1577
1662
|
};
|
|
1578
1663
|
|
|
1579
|
-
// src/tasks/taskLogs.
|
|
1664
|
+
// src/tasks/taskLogs.js
|
|
1580
1665
|
function getLogsState(context) {
|
|
1581
1666
|
const holder = context;
|
|
1582
1667
|
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
@@ -1629,6 +1714,96 @@ function getLogsState(context) {
|
|
|
1629
1714
|
holder.__tasksLogsState = state;
|
|
1630
1715
|
return state;
|
|
1631
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
|
+
}
|
|
1632
1807
|
function isErrorPayload(payload) {
|
|
1633
1808
|
if (!payload) return false;
|
|
1634
1809
|
if (typeof payload === "object") {
|
|
@@ -1648,14 +1823,26 @@ function buildLogRecord(task, payload) {
|
|
|
1648
1823
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1649
1824
|
opid: task.opid ?? null,
|
|
1650
1825
|
taskId: task.id,
|
|
1651
|
-
taskName: task.
|
|
1652
|
-
target: task.
|
|
1826
|
+
taskName: task.name,
|
|
1827
|
+
target: task.service_group,
|
|
1653
1828
|
source: typeof params.source === "string" ? params.source : null,
|
|
1654
1829
|
resource: typeof params.resource === "string" ? params.resource : null,
|
|
1655
1830
|
payload
|
|
1656
1831
|
};
|
|
1657
1832
|
}
|
|
1658
|
-
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
|
+
}
|
|
1659
1846
|
const state = getLogsState(context);
|
|
1660
1847
|
if (!state.db && !state.errorDb) return;
|
|
1661
1848
|
const record = buildLogRecord(task, payload);
|
|
@@ -1672,84 +1859,293 @@ function appendTaskIpcLog(context, task, payload) {
|
|
|
1672
1859
|
context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
|
|
1673
1860
|
});
|
|
1674
1861
|
}
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
let current = field;
|
|
1684
|
-
while (true) {
|
|
1685
|
-
const match = regex.exec(current);
|
|
1686
|
-
if (!match) break;
|
|
1687
|
-
const raw = match[0];
|
|
1688
|
-
let first = Number(match[1]);
|
|
1689
|
-
let last = Number(match[2]);
|
|
1690
|
-
if (last < first) {
|
|
1691
|
-
[first, last] = [last, first];
|
|
1692
|
-
}
|
|
1693
|
-
const values = [];
|
|
1694
|
-
for (let i = first; i <= last; i += 1) {
|
|
1695
|
-
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);
|
|
1696
1870
|
}
|
|
1697
|
-
current = current.replace(raw, values.join(","));
|
|
1698
1871
|
}
|
|
1699
|
-
|
|
1700
|
-
}
|
|
1701
|
-
function resolveSteps(field) {
|
|
1702
|
-
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
1703
|
-
if (!match) return field;
|
|
1704
|
-
const base = match[1];
|
|
1705
|
-
const step = Number(match[2]);
|
|
1706
|
-
if (!Number.isFinite(step) || step <= 0) return field;
|
|
1707
|
-
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
1708
|
-
}
|
|
1709
|
-
function convertPattern(pattern) {
|
|
1710
|
-
const parts = pattern.trim().split(/\s+/);
|
|
1711
|
-
if (parts.length !== 6) {
|
|
1712
|
-
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
1713
|
-
}
|
|
1714
|
-
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
1715
|
-
}
|
|
1716
|
-
function fieldMatches(field, value) {
|
|
1717
|
-
const allowed = field.split(",").map((v) => Number(v));
|
|
1718
|
-
return allowed.includes(value);
|
|
1719
|
-
}
|
|
1720
|
-
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
1721
|
-
const parsed = convertPattern(pattern);
|
|
1722
|
-
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);
|
|
1723
1873
|
}
|
|
1724
1874
|
|
|
1725
|
-
// src/tasks/
|
|
1726
|
-
var
|
|
1727
|
-
|
|
1728
|
-
|
|
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
|
+
*/
|
|
1729
1889
|
constructor(context, task) {
|
|
1730
1890
|
this.context = context;
|
|
1731
1891
|
this.task = task;
|
|
1732
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
|
+
*/
|
|
1733
1899
|
cantRunReason() {
|
|
1734
1900
|
return false;
|
|
1735
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
|
+
*/
|
|
1736
1908
|
requestStop(_allowanceMs) {
|
|
1737
1909
|
}
|
|
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");
|
|
1920
|
+
}
|
|
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 };
|
|
1944
|
+
}
|
|
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
|
+
}
|
|
1738
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
|
+
}
|
|
1739
2088
|
|
|
1740
|
-
// src/tasks/coreTasks/TaskPing.
|
|
1741
|
-
var TaskPing = class extends
|
|
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
|
+
*/
|
|
1742
2100
|
async run() {
|
|
1743
2101
|
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
1744
2102
|
return { success: true, results: "pong" };
|
|
1745
2103
|
}
|
|
1746
2104
|
};
|
|
1747
2105
|
|
|
1748
|
-
// src/tasks/coreTasks/TaskSampleProcess.
|
|
1749
|
-
var TaskSampleProcess = class extends
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
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
|
+
*/
|
|
1753
2149
|
requestStop(allowanceMs) {
|
|
1754
2150
|
this.stopRequested = true;
|
|
1755
2151
|
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
@@ -1757,6 +2153,14 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
1757
2153
|
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
1758
2154
|
);
|
|
1759
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
|
+
*/
|
|
1760
2164
|
async run(reportProgress) {
|
|
1761
2165
|
const totalRaw = this.task?.params?.total ?? 10;
|
|
1762
2166
|
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
@@ -1838,7 +2242,7 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
1838
2242
|
}
|
|
1839
2243
|
};
|
|
1840
2244
|
|
|
1841
|
-
// src/tasks/coreTasks/TaskShellCommand.
|
|
2245
|
+
// src/tasks/coreTasks/TaskShellCommand.js
|
|
1842
2246
|
var import_node_child_process = require("child_process");
|
|
1843
2247
|
function runShellCommand(command, cwd) {
|
|
1844
2248
|
return new Promise((resolve, reject) => {
|
|
@@ -1868,7 +2272,27 @@ function runShellCommand(command, cwd) {
|
|
|
1868
2272
|
});
|
|
1869
2273
|
});
|
|
1870
2274
|
}
|
|
1871
|
-
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
|
+
*/
|
|
1872
2296
|
async run() {
|
|
1873
2297
|
const params = this.task?.params;
|
|
1874
2298
|
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
@@ -1917,9 +2341,9 @@ var TaskShellCommand = class extends TaskMaster {
|
|
|
1917
2341
|
}
|
|
1918
2342
|
};
|
|
1919
2343
|
|
|
1920
|
-
// src/tasks/coreTasks/TaskSystemInfo.
|
|
2344
|
+
// src/tasks/coreTasks/TaskSystemInfo.js
|
|
1921
2345
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
1922
|
-
var
|
|
2346
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
1923
2347
|
function toGb(valueBytes) {
|
|
1924
2348
|
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
1925
2349
|
}
|
|
@@ -1927,7 +2351,7 @@ function toMb(valueBytes) {
|
|
|
1927
2351
|
return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
|
|
1928
2352
|
}
|
|
1929
2353
|
async function getDiskStats() {
|
|
1930
|
-
const stats = await
|
|
2354
|
+
const stats = await import_promises.default.statfs("/");
|
|
1931
2355
|
const total = Number(stats.bsize) * Number(stats.blocks);
|
|
1932
2356
|
const free = Number(stats.bsize) * Number(stats.bavail);
|
|
1933
2357
|
const used = total - free;
|
|
@@ -1937,7 +2361,16 @@ async function getDiskStats() {
|
|
|
1937
2361
|
free: toGb(free)
|
|
1938
2362
|
};
|
|
1939
2363
|
}
|
|
1940
|
-
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
|
+
*/
|
|
1941
2374
|
async run() {
|
|
1942
2375
|
try {
|
|
1943
2376
|
const totalMemory = import_node_os2.default.totalmem();
|
|
@@ -1989,8 +2422,31 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
1989
2422
|
}
|
|
1990
2423
|
};
|
|
1991
2424
|
|
|
1992
|
-
// src/tasks/coreTasks/TaskSumAB.
|
|
1993
|
-
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
|
+
*/
|
|
1994
2450
|
async run() {
|
|
1995
2451
|
const a = this.task?.params?.a;
|
|
1996
2452
|
const b = this.task?.params?.b;
|
|
@@ -2021,8 +2477,46 @@ var TaskSumAB = class extends TaskMaster {
|
|
|
2021
2477
|
}
|
|
2022
2478
|
};
|
|
2023
2479
|
|
|
2024
|
-
// src/tasks/coreTasks/TaskStopRunner.
|
|
2025
|
-
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
|
+
*/
|
|
2026
2520
|
async run() {
|
|
2027
2521
|
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
2028
2522
|
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
@@ -2037,172 +2531,396 @@ var TaskStopRunner = class extends TaskMaster {
|
|
|
2037
2531
|
}
|
|
2038
2532
|
};
|
|
2039
2533
|
|
|
2040
|
-
// 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
|
|
2041
2598
|
var TasksRegistry = class _TasksRegistry {
|
|
2042
|
-
|
|
2599
|
+
/**
|
|
2600
|
+
* @param {Record<string, Function>} [initial] Optional seed entries to copy in.
|
|
2601
|
+
*/
|
|
2043
2602
|
constructor(initial) {
|
|
2603
|
+
this.map = {};
|
|
2044
2604
|
if (initial) {
|
|
2045
2605
|
this.addMany(initial);
|
|
2046
2606
|
}
|
|
2047
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
|
+
*/
|
|
2048
2614
|
static withCoreTasks() {
|
|
2049
|
-
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);
|
|
2050
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
|
+
*/
|
|
2051
2624
|
add(taskName, taskClass) {
|
|
2052
2625
|
this.map[taskName] = taskClass;
|
|
2053
2626
|
return this;
|
|
2054
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
|
+
*/
|
|
2055
2634
|
addMany(entries) {
|
|
2056
2635
|
for (const [name, klass] of Object.entries(entries)) {
|
|
2057
2636
|
this.add(name, klass);
|
|
2058
2637
|
}
|
|
2059
2638
|
return this;
|
|
2060
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
|
+
*/
|
|
2061
2647
|
get(taskName) {
|
|
2062
2648
|
return this.map[taskName];
|
|
2063
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
|
+
*/
|
|
2064
2699
|
listSupportedTasks() {
|
|
2065
2700
|
return Object.keys(this.map).sort();
|
|
2066
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
|
+
*/
|
|
2067
2708
|
toObject() {
|
|
2068
2709
|
return { ...this.map };
|
|
2069
2710
|
}
|
|
2070
2711
|
};
|
|
2071
2712
|
|
|
2072
|
-
// 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
|
|
2073
2738
|
var import_node_child_process2 = require("child_process");
|
|
2739
|
+
var MAX_PROGRESS_TEXT_LEN = 4e3;
|
|
2074
2740
|
function toCliArgs(args = []) {
|
|
2075
2741
|
return args.filter((a) => typeof a === "string" && a.length > 0);
|
|
2076
2742
|
}
|
|
2077
2743
|
function formatChildLogPrefix(task) {
|
|
2078
|
-
return `${task.
|
|
2744
|
+
return `${task.name}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
|
|
2079
2745
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
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) {
|
|
2082
2783
|
const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
|
|
2083
2784
|
const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
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)")
|
|
2098
2797
|
);
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
let
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
|
|
2112
|
-
}).catch((error) => {
|
|
2113
|
-
context.logger.warn?.(
|
|
2114
|
-
`[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
2115
|
-
);
|
|
2116
|
-
});
|
|
2117
|
-
if (options.onProgress) {
|
|
2118
|
-
progressCallbackChain = progressCallbackChain.then(async () => {
|
|
2119
|
-
await options.onProgress?.(text.slice(0, 4e3));
|
|
2120
|
-
}).catch((error) => {
|
|
2121
|
-
context.logger.warn?.(
|
|
2122
|
-
`[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
2123
|
-
);
|
|
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(() => {
|
|
2124
2810
|
});
|
|
2125
2811
|
}
|
|
2126
2812
|
};
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
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
|
|
2140
2836
|
};
|
|
2141
|
-
|
|
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) => {
|
|
2142
2863
|
const text = String(chunk);
|
|
2143
|
-
stdout += text;
|
|
2864
|
+
state.stdout += text;
|
|
2144
2865
|
if (text.trim()) {
|
|
2145
2866
|
context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
2146
|
-
updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
|
|
2147
2867
|
}
|
|
2148
2868
|
});
|
|
2149
|
-
child.stderr
|
|
2869
|
+
child.stderr?.on("data", (chunk) => {
|
|
2150
2870
|
const text = String(chunk);
|
|
2151
|
-
stderr += text;
|
|
2871
|
+
state.stderr += text;
|
|
2152
2872
|
if (text.trim()) {
|
|
2153
2873
|
context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
2154
2874
|
}
|
|
2155
2875
|
});
|
|
2156
2876
|
child.on("message", (message) => {
|
|
2157
2877
|
if (message && typeof message === "object" && "__taskWorkerResult" in message) {
|
|
2158
|
-
workerResult = message.__taskWorkerResult;
|
|
2878
|
+
state.workerResult = message.__taskWorkerResult;
|
|
2159
2879
|
return;
|
|
2160
2880
|
}
|
|
2161
2881
|
if (message && typeof message === "object") {
|
|
2162
2882
|
const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
|
|
2163
2883
|
if (level === "error" || level === "fatal") {
|
|
2164
|
-
hadErrorMessage = true;
|
|
2884
|
+
state.hadErrorMessage = true;
|
|
2165
2885
|
}
|
|
2166
2886
|
}
|
|
2167
|
-
appendTaskIpcLog(context, options.task, message);
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
const countNum = Number(String(message.count).trim());
|
|
2173
|
-
const totalNum = Number(message.total);
|
|
2174
|
-
if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
|
|
2175
|
-
context.logger.progress(message.message || "progress", {
|
|
2176
|
-
prefix: message.prefix || prefix,
|
|
2177
|
-
count: countNum,
|
|
2178
|
-
total: totalNum
|
|
2179
|
-
});
|
|
2180
|
-
} else {
|
|
2181
|
-
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
2182
|
-
}
|
|
2183
|
-
} else {
|
|
2184
|
-
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
2185
|
-
}
|
|
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)}`);
|
|
2186
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);
|
|
2187
2903
|
});
|
|
2188
2904
|
return await new Promise((resolve, reject) => {
|
|
2189
2905
|
child.on("error", (error) => reject(error));
|
|
2190
2906
|
child.on("close", (exitCode, signal) => {
|
|
2191
|
-
|
|
2907
|
+
void (async () => {
|
|
2908
|
+
await flushTaskIpcLogs(context);
|
|
2909
|
+
await progressQueue.drain();
|
|
2192
2910
|
resolve({
|
|
2193
2911
|
exitCode,
|
|
2194
2912
|
signal,
|
|
2195
|
-
stdout: stdout.trim(),
|
|
2196
|
-
stderr: stderr.trim(),
|
|
2197
|
-
workerResult,
|
|
2198
|
-
hadErrorMessage
|
|
2913
|
+
stdout: state.stdout.trim(),
|
|
2914
|
+
stderr: state.stderr.trim(),
|
|
2915
|
+
workerResult: state.workerResult,
|
|
2916
|
+
hadErrorMessage: state.hadErrorMessage
|
|
2199
2917
|
});
|
|
2200
|
-
});
|
|
2918
|
+
})();
|
|
2201
2919
|
});
|
|
2202
2920
|
});
|
|
2203
2921
|
}
|
|
2204
2922
|
|
|
2205
|
-
// src/tasks/index.
|
|
2923
|
+
// src/tasks/index.js
|
|
2206
2924
|
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
2207
2925
|
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
2208
2926
|
function getDb3(context) {
|
|
@@ -2217,22 +2935,13 @@ function normalizeRegistry(registry) {
|
|
|
2217
2935
|
if (registry instanceof TasksRegistry) return registry;
|
|
2218
2936
|
return new TasksRegistry().addMany(registry);
|
|
2219
2937
|
}
|
|
2220
|
-
function
|
|
2221
|
-
if (!value) return void 0;
|
|
2222
|
-
if (Array.isArray(value)) {
|
|
2223
|
-
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
2224
|
-
return out2.length ? out2 : void 0;
|
|
2225
|
-
}
|
|
2226
|
-
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
2227
|
-
return out.length ? out : void 0;
|
|
2228
|
-
}
|
|
2229
|
-
async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
|
|
2938
|
+
async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
|
|
2230
2939
|
return enqueueTask(context, {
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
task: "stopRunner",
|
|
2940
|
+
queueName,
|
|
2941
|
+
name: "stopRunner",
|
|
2234
2942
|
params: { allowanceMs },
|
|
2235
|
-
priority:
|
|
2943
|
+
priority: 0,
|
|
2944
|
+
serviceGroup
|
|
2236
2945
|
});
|
|
2237
2946
|
}
|
|
2238
2947
|
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
@@ -2250,18 +2959,20 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
|
|
|
2250
2959
|
}
|
|
2251
2960
|
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
2252
2961
|
const db = getDb3(context);
|
|
2253
|
-
const taskName = row.
|
|
2962
|
+
const taskName = row.name;
|
|
2254
2963
|
const TaskClass = registry.get(taskName);
|
|
2255
|
-
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
2256
2964
|
if (!TaskClass) {
|
|
2257
2965
|
const err = { message: `Unknown task "${taskName}"` };
|
|
2258
|
-
await db(historyTable).insert(
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
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
|
+
);
|
|
2265
2976
|
if (row.schedule) {
|
|
2266
2977
|
await db(tasksTable).where({ id: row.id }).update({
|
|
2267
2978
|
started_at: null,
|
|
@@ -2269,7 +2980,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
2269
2980
|
success: false,
|
|
2270
2981
|
results: toJsonColumn(err),
|
|
2271
2982
|
past_due: null,
|
|
2272
|
-
|
|
2983
|
+
status: "paused",
|
|
2984
|
+
status_changed_at: db.fn.now(),
|
|
2273
2985
|
progress: LOCKED_BY_ERROR_MESSAGE
|
|
2274
2986
|
});
|
|
2275
2987
|
} else {
|
|
@@ -2296,13 +3008,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
2296
3008
|
} finally {
|
|
2297
3009
|
runningTaskInstances.delete(row.id);
|
|
2298
3010
|
}
|
|
2299
|
-
await db(historyTable).insert(
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
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
|
+
);
|
|
2306
3021
|
if (!success) {
|
|
2307
3022
|
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
2308
3023
|
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
@@ -2317,19 +3032,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
2317
3032
|
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
2318
3033
|
appendTaskIpcLog(context, row, {
|
|
2319
3034
|
level: "error",
|
|
2320
|
-
message: `[tasks] task failed: ${row.
|
|
3035
|
+
message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
2321
3036
|
details: results
|
|
2322
3037
|
});
|
|
2323
3038
|
}
|
|
2324
3039
|
if (row.schedule) {
|
|
2325
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
|
+
}
|
|
2326
3047
|
await db(tasksTable).where({ id: row.id }).update({
|
|
2327
3048
|
started_at: null,
|
|
2328
3049
|
completed_at: /* @__PURE__ */ new Date(),
|
|
2329
3050
|
success,
|
|
2330
3051
|
results: toJsonColumn(results),
|
|
2331
3052
|
progress: null,
|
|
2332
|
-
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
|
|
2333
3061
|
});
|
|
2334
3062
|
} else {
|
|
2335
3063
|
await db(tasksTable).where({ id: row.id }).update({
|
|
@@ -2337,7 +3065,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
2337
3065
|
completed_at: /* @__PURE__ */ new Date(),
|
|
2338
3066
|
success,
|
|
2339
3067
|
results: toJsonColumn(results),
|
|
2340
|
-
|
|
3068
|
+
status: "paused",
|
|
3069
|
+
status_changed_at: db.fn.now(),
|
|
2341
3070
|
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
2342
3071
|
past_due: null
|
|
2343
3072
|
});
|
|
@@ -2349,58 +3078,93 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
2349
3078
|
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
2350
3079
|
return { stopRunnerRequested, stopAllowanceMs };
|
|
2351
3080
|
}
|
|
2352
|
-
|
|
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) {
|
|
2353
3090
|
const db = getDb3(context);
|
|
2354
|
-
let query = db(tasksTable).
|
|
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);
|
|
2355
3094
|
if (taskNames && taskNames.length > 0) {
|
|
2356
|
-
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");
|
|
2357
3107
|
}
|
|
2358
3108
|
const candidates = await query;
|
|
3109
|
+
shuffleTaskRowsInPlace(candidates);
|
|
2359
3110
|
for (const row of candidates) {
|
|
2360
3111
|
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
2361
3112
|
continue;
|
|
2362
3113
|
}
|
|
2363
|
-
const TaskClass = registry.get(row.
|
|
2364
|
-
if (TaskClass) {
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
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
|
+
});
|
|
2375
3126
|
}
|
|
3127
|
+
continue;
|
|
3128
|
+
}
|
|
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;
|
|
2376
3138
|
}
|
|
2377
|
-
const updated = await db(tasksTable).where({ id: row.id
|
|
3139
|
+
const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
|
|
2378
3140
|
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
2379
3141
|
if (claimed) return claimed;
|
|
2380
3142
|
}
|
|
2381
3143
|
return null;
|
|
2382
3144
|
}
|
|
2383
3145
|
async function runTasksLoop(context, options) {
|
|
2384
|
-
const
|
|
3146
|
+
const queueName = options.queueName ?? "tasks";
|
|
2385
3147
|
const target = options.target;
|
|
2386
3148
|
const pollMs = options.pollMs ?? 1e3;
|
|
2387
|
-
const
|
|
3149
|
+
const claimJitterMs = options.claimJitterMs ?? 0;
|
|
3150
|
+
const maxParallel = options.maxParallel ?? 32;
|
|
2388
3151
|
const scanLimit = options.scanLimit ?? 100;
|
|
2389
3152
|
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
2390
3153
|
const registry = normalizeRegistry(options.registry);
|
|
2391
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
3154
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
2392
3155
|
if (!target) throw new Error("runTasksLoop: target is required");
|
|
3156
|
+
context.tasksQueueName = queueName;
|
|
2393
3157
|
const runningPromises = /* @__PURE__ */ new Set();
|
|
2394
3158
|
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
2395
3159
|
let runningStopControlPromise = null;
|
|
2396
3160
|
let stopRequested = false;
|
|
2397
3161
|
let stopAllowanceMs = 5e3;
|
|
2398
|
-
context.
|
|
3162
|
+
context.tasksRunnerStop = false;
|
|
2399
3163
|
let registryReg = null;
|
|
2400
3164
|
let registryInterval = null;
|
|
3165
|
+
let runnerIdentity = null;
|
|
2401
3166
|
const hbGroup = options.runnerServiceGroup?.trim();
|
|
2402
3167
|
if (hbGroup) {
|
|
2403
|
-
const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
|
|
2404
3168
|
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
2405
3169
|
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
2406
3170
|
const defaultMeta = {
|
|
@@ -2408,16 +3172,21 @@ async function runTasksLoop(context, options) {
|
|
|
2408
3172
|
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
2409
3173
|
};
|
|
2410
3174
|
registryReg = await registerInServicesRegistry(context, {
|
|
2411
|
-
|
|
3175
|
+
queueName,
|
|
2412
3176
|
target,
|
|
2413
3177
|
serviceGroup: hbGroup,
|
|
2414
3178
|
serviceName: options.runnerServiceName,
|
|
2415
|
-
|
|
3179
|
+
instanceNumber: options.runnerInstanceNumber,
|
|
2416
3180
|
staleMs,
|
|
2417
3181
|
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
2418
3182
|
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
2419
3183
|
metadata: options.runnerMetadata ?? defaultMeta
|
|
2420
3184
|
});
|
|
3185
|
+
runnerIdentity = {
|
|
3186
|
+
service_name: registryReg.serviceName,
|
|
3187
|
+
server_name: import_node_os3.default.hostname(),
|
|
3188
|
+
instance_number: registryReg.instanceNumber
|
|
3189
|
+
};
|
|
2421
3190
|
registryInterval = setInterval(() => {
|
|
2422
3191
|
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
2423
3192
|
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
@@ -2425,7 +3194,7 @@ async function runTasksLoop(context, options) {
|
|
|
2425
3194
|
}, hbIntervalMs);
|
|
2426
3195
|
}
|
|
2427
3196
|
try {
|
|
2428
|
-
while (!context.isStop() && !stopRequested &&
|
|
3197
|
+
while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
|
|
2429
3198
|
if (!runningStopControlPromise) {
|
|
2430
3199
|
const claimedStopTask = await claimNextRunnableTask(
|
|
2431
3200
|
context,
|
|
@@ -2433,7 +3202,8 @@ async function runTasksLoop(context, options) {
|
|
|
2433
3202
|
target,
|
|
2434
3203
|
registry,
|
|
2435
3204
|
10,
|
|
2436
|
-
["stopRunner", "stop"]
|
|
3205
|
+
["stopRunner", "stop"],
|
|
3206
|
+
runnerIdentity
|
|
2437
3207
|
);
|
|
2438
3208
|
if (claimedStopTask) {
|
|
2439
3209
|
runningStopControlPromise = executeClaimedTask(
|
|
@@ -2447,7 +3217,7 @@ async function runTasksLoop(context, options) {
|
|
|
2447
3217
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
2448
3218
|
stopRequested = true;
|
|
2449
3219
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
2450
|
-
context.
|
|
3220
|
+
context.tasksRunnerStop = true;
|
|
2451
3221
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
2452
3222
|
}
|
|
2453
3223
|
}).finally(() => {
|
|
@@ -2455,6 +3225,9 @@ async function runTasksLoop(context, options) {
|
|
|
2455
3225
|
});
|
|
2456
3226
|
}
|
|
2457
3227
|
}
|
|
3228
|
+
if (claimJitterMs > 0) {
|
|
3229
|
+
await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
|
|
3230
|
+
}
|
|
2458
3231
|
while (runningPromises.size < maxParallel) {
|
|
2459
3232
|
const claimed = await claimNextRunnableTask(
|
|
2460
3233
|
context,
|
|
@@ -2462,14 +3235,15 @@ async function runTasksLoop(context, options) {
|
|
|
2462
3235
|
target,
|
|
2463
3236
|
registry,
|
|
2464
3237
|
scanLimit,
|
|
2465
|
-
allowedTasks
|
|
3238
|
+
allowedTasks,
|
|
3239
|
+
runnerIdentity
|
|
2466
3240
|
);
|
|
2467
3241
|
if (!claimed) break;
|
|
2468
3242
|
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
2469
3243
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
2470
3244
|
stopRequested = true;
|
|
2471
3245
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
2472
|
-
context.
|
|
3246
|
+
context.tasksRunnerStop = true;
|
|
2473
3247
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
2474
3248
|
}
|
|
2475
3249
|
}).finally(() => {
|
|
@@ -2477,7 +3251,16 @@ async function runTasksLoop(context, options) {
|
|
|
2477
3251
|
});
|
|
2478
3252
|
runningPromises.add(p);
|
|
2479
3253
|
}
|
|
2480
|
-
|
|
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)]);
|
|
3263
|
+
}
|
|
2481
3264
|
}
|
|
2482
3265
|
if (context.isStop() && !stopRequested) {
|
|
2483
3266
|
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
@@ -2513,89 +3296,130 @@ async function runTasksLoop(context, options) {
|
|
|
2513
3296
|
}
|
|
2514
3297
|
async function waitForTaskResult(context, taskId, options = {}) {
|
|
2515
3298
|
const db = getDb3(context);
|
|
2516
|
-
const
|
|
3299
|
+
const queueName = options.queueName ?? "tasks";
|
|
2517
3300
|
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
2518
3301
|
const pollMs = options.pollMs ?? 500;
|
|
2519
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
3302
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
2520
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
|
+
}
|
|
2521
3315
|
while (Date.now() <= deadline) {
|
|
2522
|
-
const
|
|
2523
|
-
if (
|
|
3316
|
+
const legacy = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
|
|
3317
|
+
if (legacy) {
|
|
3318
|
+
return legacy;
|
|
3319
|
+
}
|
|
2524
3320
|
const pending = await db(tasksTable).where({ id: taskId }).first();
|
|
2525
|
-
if (
|
|
2526
|
-
|
|
2527
|
-
|
|
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;
|
|
2528
3335
|
}
|
|
2529
3336
|
await sleepMs(pollMs);
|
|
2530
3337
|
}
|
|
2531
3338
|
return null;
|
|
2532
3339
|
}
|
|
2533
3340
|
var TasksManager = class _TasksManager {
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
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
|
+
*/
|
|
2551
3363
|
constructor(context, options = {}) {
|
|
2552
3364
|
this.context = context;
|
|
2553
|
-
this.
|
|
3365
|
+
this.queueName = options.queueName ?? "tasks";
|
|
2554
3366
|
this.target = options.target ?? "localRunner";
|
|
2555
3367
|
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
2556
3368
|
this.pollMs = options.pollMs ?? 1e3;
|
|
3369
|
+
this.claimJitterMs = options.claimJitterMs ?? 0;
|
|
2557
3370
|
this.maxParallel = options.maxParallel ?? 1;
|
|
2558
3371
|
this.scanLimit = options.scanLimit ?? 100;
|
|
2559
3372
|
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
2560
3373
|
this.registry = normalizeRegistry(options.registry);
|
|
2561
3374
|
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
2562
3375
|
this.runnerServiceName = options.runnerServiceName;
|
|
2563
|
-
this.
|
|
3376
|
+
this.runnerInstanceNumber = options.runnerInstanceNumber;
|
|
2564
3377
|
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
2565
3378
|
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
2566
3379
|
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
2567
3380
|
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
2568
3381
|
this.runnerMetadata = options.runnerMetadata;
|
|
2569
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
|
+
*/
|
|
2570
3392
|
static init(context, options = {}) {
|
|
2571
3393
|
const defs = {
|
|
2572
3394
|
table: "string default tasks",
|
|
2573
3395
|
target: "string default localRunner",
|
|
2574
3396
|
recreateTaskTables: "boolean default false",
|
|
2575
3397
|
pollMs: "number default 1000",
|
|
3398
|
+
claimJitterMs: "number default 0",
|
|
2576
3399
|
maxParallel: "number default 1",
|
|
2577
3400
|
scanLimit: "number default 100",
|
|
2578
3401
|
allowedTasks: "string",
|
|
2579
3402
|
runnerServiceGroup: "string",
|
|
2580
3403
|
runnerServiceName: "string",
|
|
2581
|
-
|
|
3404
|
+
runnerInstanceNumber: "number",
|
|
2582
3405
|
runnerHeartbeatIntervalMs: "number default 10000",
|
|
2583
3406
|
runnerHeartbeatStaleMs: "number default 45000",
|
|
2584
3407
|
runnerGroupMaxInstances: "number",
|
|
2585
3408
|
runnerEnforceMaxInstances: "boolean default true"
|
|
2586
3409
|
};
|
|
2587
|
-
const discovered = context.params.getAllForModule(defs);
|
|
3410
|
+
const discovered = context.params.getAllForModule("tasks", defs);
|
|
2588
3411
|
const resolved = {
|
|
2589
|
-
|
|
3412
|
+
queueName: discovered.table,
|
|
2590
3413
|
target: discovered.target,
|
|
2591
3414
|
recreateTaskTables: discovered.recreateTaskTables,
|
|
2592
3415
|
pollMs: discovered.pollMs,
|
|
3416
|
+
claimJitterMs: discovered.claimJitterMs,
|
|
2593
3417
|
maxParallel: discovered.maxParallel,
|
|
2594
3418
|
scanLimit: discovered.scanLimit,
|
|
2595
3419
|
allowedTasks: discovered.allowedTasks,
|
|
2596
3420
|
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
2597
3421
|
runnerServiceName: discovered.runnerServiceName,
|
|
2598
|
-
|
|
3422
|
+
runnerInstanceNumber: discovered.runnerInstanceNumber,
|
|
2599
3423
|
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
2600
3424
|
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
2601
3425
|
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
@@ -2604,24 +3428,39 @@ var TasksManager = class _TasksManager {
|
|
|
2604
3428
|
};
|
|
2605
3429
|
return new _TasksManager(context, resolved);
|
|
2606
3430
|
}
|
|
3431
|
+
/**
|
|
3432
|
+
* Idempotently ensure the three backing tables exist for this queue.
|
|
3433
|
+
*
|
|
3434
|
+
* @param {{ recreate?: boolean }} [options]
|
|
3435
|
+
* @returns {Promise<void>}
|
|
3436
|
+
*/
|
|
2607
3437
|
async ensureTaskTables(options = {}) {
|
|
2608
3438
|
await ensureTaskTables(this.context, {
|
|
2609
|
-
|
|
3439
|
+
queueName: this.queueName,
|
|
2610
3440
|
recreate: options.recreate ?? this.recreateTaskTables
|
|
2611
3441
|
});
|
|
2612
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
|
+
*/
|
|
2613
3451
|
async runTasksLoop(options = {}) {
|
|
2614
3452
|
await runTasksLoop(this.context, {
|
|
2615
|
-
|
|
3453
|
+
queueName: options.queueName ?? this.queueName,
|
|
2616
3454
|
target: options.target ?? this.target,
|
|
2617
3455
|
pollMs: options.pollMs ?? this.pollMs,
|
|
3456
|
+
claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
|
|
2618
3457
|
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
2619
3458
|
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
2620
3459
|
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
2621
3460
|
registry: options.registry ?? this.registry,
|
|
2622
3461
|
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
2623
3462
|
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
2624
|
-
|
|
3463
|
+
runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
|
|
2625
3464
|
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
2626
3465
|
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
2627
3466
|
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
@@ -2632,7 +3471,9 @@ var TasksManager = class _TasksManager {
|
|
|
2632
3471
|
};
|
|
2633
3472
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2634
3473
|
0 && (module.exports = {
|
|
2635
|
-
|
|
3474
|
+
AbstractTask,
|
|
3475
|
+
SERVICE_TASK_NAMES,
|
|
3476
|
+
TaskGetLogs,
|
|
2636
3477
|
TaskPing,
|
|
2637
3478
|
TaskSampleProcess,
|
|
2638
3479
|
TaskShellCommand,
|
|
@@ -2642,19 +3483,31 @@ var TasksManager = class _TasksManager {
|
|
|
2642
3483
|
TasksManager,
|
|
2643
3484
|
TasksRegistry,
|
|
2644
3485
|
appendTaskIpcLog,
|
|
3486
|
+
convertPattern,
|
|
2645
3487
|
defaultTasksRegistry,
|
|
2646
3488
|
enqueueStopTask,
|
|
2647
3489
|
enqueueTask,
|
|
2648
3490
|
ensureTaskTables,
|
|
3491
|
+
flushTaskIpcLogs,
|
|
3492
|
+
ipcFileLogsTableNameForSourceResource,
|
|
2649
3493
|
listAliveRunnerHeartbeats,
|
|
2650
3494
|
listServicesRegistry,
|
|
3495
|
+
matchesParsedPattern,
|
|
3496
|
+
mergeAllowedTasksWithServiceTasks,
|
|
3497
|
+
nextTimeMatch,
|
|
3498
|
+
normalizeAllowedTasks,
|
|
2651
3499
|
queueToTableNames,
|
|
3500
|
+
readTaskIpcLogsSnapshot,
|
|
2652
3501
|
registerInServicesRegistry,
|
|
2653
3502
|
registerRunnerHeartbeat,
|
|
3503
|
+
resolveAsterisks,
|
|
3504
|
+
resolveIpcFileLogsDir,
|
|
3505
|
+
resolveRanges,
|
|
3506
|
+
resolveSteps,
|
|
2654
3507
|
runNodeTaskScript,
|
|
2655
3508
|
runTasksLoop,
|
|
2656
|
-
|
|
2657
|
-
|
|
3509
|
+
taskHistoryInsertFromQueueRow,
|
|
3510
|
+
timeMatcher,
|
|
2658
3511
|
touchRunnerHeartbeat,
|
|
2659
3512
|
touchServicesRegistry,
|
|
2660
3513
|
unregisterRunnerHeartbeat,
|