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