@h-ai/scheduler 0.1.0-alpha.55 → 0.1.0-alpha.57
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.en.md +227 -0
- package/README.ja.md +227 -0
- package/README.md +9 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +85 -36
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -2,10 +2,11 @@ import { z } from 'zod';
|
|
|
2
2
|
import { cache } from '@h-ai/cache';
|
|
3
3
|
import { core, ok, err } from '@h-ai/core';
|
|
4
4
|
import { reldb, BaseReldbCrudRepository } from '@h-ai/reldb';
|
|
5
|
-
import { createHash } from 'crypto';
|
|
5
|
+
import { randomUUID, createHash } from 'crypto';
|
|
6
6
|
import { Script } from 'vm';
|
|
7
7
|
import { Worker } from 'worker_threads';
|
|
8
8
|
import { Cron } from 'croner';
|
|
9
|
+
import { isDeepStrictEqual } from 'util';
|
|
9
10
|
|
|
10
11
|
// src/scheduler-config.ts
|
|
11
12
|
var SchedulerConfigSchema = z.object({
|
|
@@ -133,8 +134,8 @@ var SchedulerLogRepository = class extends BaseReldbCrudRepository {
|
|
|
133
134
|
{ fieldName: "status", columnName: "status", def: { type: "TEXT", notNull: true }, select: true, create: true, update: false },
|
|
134
135
|
{ fieldName: "result", columnName: "result", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
135
136
|
{ fieldName: "error", columnName: "error", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
136
|
-
{ fieldName: "startedAt", columnName: "started_at", def: { type: "
|
|
137
|
-
{ fieldName: "finishedAt", columnName: "finished_at", def: { type: "
|
|
137
|
+
{ fieldName: "startedAt", columnName: "started_at", def: { type: "BIGINT", notNull: true }, select: true, create: true, update: false },
|
|
138
|
+
{ fieldName: "finishedAt", columnName: "finished_at", def: { type: "BIGINT", notNull: true }, select: true, create: true, update: false },
|
|
138
139
|
{ fieldName: "duration", columnName: "duration", def: { type: "INTEGER", notNull: true }, select: true, create: true, update: false }
|
|
139
140
|
]
|
|
140
141
|
});
|
|
@@ -215,8 +216,8 @@ var SchedulerLogRepository = class extends BaseReldbCrudRepository {
|
|
|
215
216
|
status: row.status,
|
|
216
217
|
result: row.result ?? null,
|
|
217
218
|
error: row.error ?? null,
|
|
218
|
-
startedAt: row.startedAt,
|
|
219
|
-
finishedAt: row.finishedAt,
|
|
219
|
+
startedAt: Number(row.startedAt),
|
|
220
|
+
finishedAt: Number(row.finishedAt),
|
|
220
221
|
duration: row.duration
|
|
221
222
|
}))
|
|
222
223
|
});
|
|
@@ -417,8 +418,29 @@ var SchedulerTaskRepository = class extends BaseReldbCrudRepository {
|
|
|
417
418
|
);
|
|
418
419
|
}
|
|
419
420
|
}
|
|
420
|
-
|
|
421
|
+
/** 删除任务;自动清理携带执行定义,在同一条 DELETE 内原子比较,保留并发编辑。 */
|
|
422
|
+
async deleteTask(taskId, expected) {
|
|
421
423
|
try {
|
|
424
|
+
if (expected) {
|
|
425
|
+
const conditions = ["task_id = ?", "task_name = ?", "cron = ?", "COALESCE(timezone, '') = ?", "COALESCE(description, '') = ?", "enabled = ?", "delete_after_run = ?"];
|
|
426
|
+
const postgres = this.db.config?.type === "postgresql";
|
|
427
|
+
const enabled = expected.enabled !== false;
|
|
428
|
+
const deleteAfterRun = expected.deleteAfterRun === true;
|
|
429
|
+
const params = [taskId, expected.name, expected.cron, expected.timezone ?? "", expected.description ?? "", postgres ? enabled : Number(enabled), postgres ? deleteAfterRun : Number(deleteAfterRun)];
|
|
430
|
+
const jsonValue = this.db.config?.type === "mysql" ? "CAST(? AS JSON)" : this.db.config?.type === "postgresql" ? "CAST(? AS JSONB)" : "?";
|
|
431
|
+
const jsonFields = [["retry", expected.retry], ["params", expected.params ?? {}], ["handler", expected.handler]];
|
|
432
|
+
for (const [column, value] of jsonFields) {
|
|
433
|
+
if (value === void 0 || value === null) {
|
|
434
|
+
conditions.push(`(${column} IS NULL OR ${column} = ${jsonValue})`);
|
|
435
|
+
params.push("null");
|
|
436
|
+
} else {
|
|
437
|
+
conditions.push(`${column} = ${jsonValue}`);
|
|
438
|
+
params.push(JSON.stringify(value));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const deleted = await this.db.sql.execute(`DELETE FROM ${SCHEDULER_TASK_TABLE} WHERE ${conditions.join(" AND ")}`, params);
|
|
442
|
+
return deleted.success ? ok(deleted.data.changes > 0) : deleted;
|
|
443
|
+
}
|
|
422
444
|
const rows = await this.findAll({ where: "task_id = ?", params: [taskId], limit: 1 });
|
|
423
445
|
if (!rows.success) {
|
|
424
446
|
return err(
|
|
@@ -428,7 +450,7 @@ var SchedulerTaskRepository = class extends BaseReldbCrudRepository {
|
|
|
428
450
|
);
|
|
429
451
|
}
|
|
430
452
|
if (rows.data.length === 0)
|
|
431
|
-
return ok(
|
|
453
|
+
return ok(false);
|
|
432
454
|
const deleteResult = await this.deleteById(rows.data[0].id);
|
|
433
455
|
if (!deleteResult.success) {
|
|
434
456
|
logger2.error("Failed to delete task definition", { taskId, error: deleteResult.error.message });
|
|
@@ -438,7 +460,7 @@ var SchedulerTaskRepository = class extends BaseReldbCrudRepository {
|
|
|
438
460
|
deleteResult.error
|
|
439
461
|
);
|
|
440
462
|
}
|
|
441
|
-
return ok(
|
|
463
|
+
return ok(deleteResult.data.changes > 0);
|
|
442
464
|
} catch (error) {
|
|
443
465
|
logger2.error("Failed to delete task definition", { taskId, error });
|
|
444
466
|
return err(
|
|
@@ -598,8 +620,9 @@ function resolveTaskType(task, hooks) {
|
|
|
598
620
|
return "hook";
|
|
599
621
|
return "hook";
|
|
600
622
|
}
|
|
601
|
-
function createTaskContext(task, trigger) {
|
|
623
|
+
function createTaskContext(task, trigger, occurrenceId) {
|
|
602
624
|
return {
|
|
625
|
+
...occurrenceId ? { occurrenceId } : {},
|
|
603
626
|
task,
|
|
604
627
|
taskId: task.id,
|
|
605
628
|
params: task.params ?? {},
|
|
@@ -803,10 +826,10 @@ async function executeHookTask(task, context, hooks, startedAt, signal) {
|
|
|
803
826
|
);
|
|
804
827
|
}
|
|
805
828
|
}
|
|
806
|
-
async function executeTask(task, trigger, hooks, signal = new AbortController().signal) {
|
|
829
|
+
async function executeTask(task, trigger, hooks, signal = new AbortController().signal, occurrenceId) {
|
|
807
830
|
const startedAt = Date.now();
|
|
808
831
|
const taskType = resolveTaskType(task, hooks);
|
|
809
|
-
const context = createTaskContext(task, trigger);
|
|
832
|
+
const context = createTaskContext(task, trigger, occurrenceId);
|
|
810
833
|
await notifyTaskStart(hooks, { task, trigger, startedAt, signal });
|
|
811
834
|
let executionResult;
|
|
812
835
|
let shouldNotifyInterrupted = false;
|
|
@@ -815,7 +838,7 @@ async function executeTask(task, trigger, hooks, signal = new AbortController().
|
|
|
815
838
|
shouldNotifyInterrupted = true;
|
|
816
839
|
} else if (!task.handler) {
|
|
817
840
|
executionResult = await executeHookTask(task, context, hooks, startedAt, signal);
|
|
818
|
-
shouldNotifyInterrupted = !executionResult.success;
|
|
841
|
+
shouldNotifyInterrupted = !hooks.onTaskExecute || !executionResult.success && executionResult.error.cause instanceof Error && executionResult.error.cause.name === "SchedulerTaskSkippedError";
|
|
819
842
|
} else if (task.handler.kind === "api") {
|
|
820
843
|
executionResult = await executeApiTask(task, context, signal);
|
|
821
844
|
} else {
|
|
@@ -866,6 +889,7 @@ var taskRegistry = /* @__PURE__ */ new Map();
|
|
|
866
889
|
var cronCache = /* @__PURE__ */ new Map();
|
|
867
890
|
var persistedTaskIds = /* @__PURE__ */ new Set();
|
|
868
891
|
var definitionEpoch = 0;
|
|
892
|
+
var activeRefresh;
|
|
869
893
|
var currentHooks = {};
|
|
870
894
|
function getTaskRegistry() {
|
|
871
895
|
return taskRegistry;
|
|
@@ -904,6 +928,7 @@ function getHooks() {
|
|
|
904
928
|
}
|
|
905
929
|
function resetTaskState() {
|
|
906
930
|
definitionEpoch++;
|
|
931
|
+
activeRefresh = void 0;
|
|
907
932
|
persistedTaskIds.clear();
|
|
908
933
|
taskRegistry.clear();
|
|
909
934
|
cronCache.clear();
|
|
@@ -940,6 +965,18 @@ async function loadPersistedTasks(taskRepo2) {
|
|
|
940
965
|
if (!taskRepo2)
|
|
941
966
|
return ok(void 0);
|
|
942
967
|
const epoch = definitionEpoch;
|
|
968
|
+
if (activeRefresh?.repository === taskRepo2 && activeRefresh.epoch === epoch)
|
|
969
|
+
return activeRefresh.promise;
|
|
970
|
+
const refresh = { repository: taskRepo2, epoch, promise: refreshPersistedTasks(taskRepo2, epoch) };
|
|
971
|
+
activeRefresh = refresh;
|
|
972
|
+
try {
|
|
973
|
+
return await refresh.promise;
|
|
974
|
+
} finally {
|
|
975
|
+
if (activeRefresh === refresh)
|
|
976
|
+
activeRefresh = void 0;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
async function refreshPersistedTasks(taskRepo2, epoch) {
|
|
943
980
|
const loadResult = await taskRepo2.loadTasks();
|
|
944
981
|
if (!loadResult.success)
|
|
945
982
|
return loadResult;
|
|
@@ -1006,14 +1043,10 @@ async function registerTask(task, taskRepo2) {
|
|
|
1006
1043
|
retry: retryResult.data,
|
|
1007
1044
|
params: task.params ?? {}
|
|
1008
1045
|
};
|
|
1009
|
-
setTask(task.id, normalizedTask);
|
|
1010
|
-
setCron(task.id, cronResult.data);
|
|
1011
1046
|
if (taskRepo2) {
|
|
1012
1047
|
const saveResult = await taskRepo2.saveTask(normalizedTask);
|
|
1013
1048
|
if (!saveResult.success) {
|
|
1014
|
-
logger4.warn("Failed to persist task definition
|
|
1015
|
-
deleteTask(task.id);
|
|
1016
|
-
deleteCron(task.id);
|
|
1049
|
+
logger4.warn("Failed to persist task definition", { taskId: task.id, error: saveResult.error.message });
|
|
1017
1050
|
return err(
|
|
1018
1051
|
HaiSchedulerError.DB_SAVE_FAILED,
|
|
1019
1052
|
schedulerM("scheduler_dbSaveFailed", { params: { error: saveResult.error.message } }),
|
|
@@ -1022,19 +1055,19 @@ async function registerTask(task, taskRepo2) {
|
|
|
1022
1055
|
}
|
|
1023
1056
|
persistedTaskIds.add(task.id);
|
|
1024
1057
|
}
|
|
1058
|
+
setTask(task.id, normalizedTask);
|
|
1059
|
+
setCron(task.id, cronResult.data);
|
|
1025
1060
|
return ok(void 0);
|
|
1026
1061
|
}
|
|
1027
|
-
async function unregisterTask(taskId, taskRepo2) {
|
|
1062
|
+
async function unregisterTask(taskId, taskRepo2, expected) {
|
|
1028
1063
|
if (!hasTask(taskId)) {
|
|
1029
1064
|
return err(
|
|
1030
1065
|
HaiSchedulerError.TASK_NOT_FOUND,
|
|
1031
1066
|
schedulerM("scheduler_taskNotFound", { params: { taskId } })
|
|
1032
1067
|
);
|
|
1033
1068
|
}
|
|
1034
|
-
deleteTask(taskId);
|
|
1035
|
-
deleteCron(taskId);
|
|
1036
1069
|
if (taskRepo2) {
|
|
1037
|
-
const deleteResult = await taskRepo2.deleteTask(taskId);
|
|
1070
|
+
const deleteResult = await taskRepo2.deleteTask(taskId, expected);
|
|
1038
1071
|
if (!deleteResult.success) {
|
|
1039
1072
|
logger4.warn("Failed to delete persisted task definition", { taskId, error: deleteResult.error.message });
|
|
1040
1073
|
return err(
|
|
@@ -1043,7 +1076,12 @@ async function unregisterTask(taskId, taskRepo2) {
|
|
|
1043
1076
|
deleteResult.error
|
|
1044
1077
|
);
|
|
1045
1078
|
}
|
|
1079
|
+
if (expected && !deleteResult.data)
|
|
1080
|
+
return ok(void 0);
|
|
1046
1081
|
}
|
|
1082
|
+
deleteTask(taskId);
|
|
1083
|
+
deleteCron(taskId);
|
|
1084
|
+
persistedTaskIds.delete(taskId);
|
|
1047
1085
|
return ok(void 0);
|
|
1048
1086
|
}
|
|
1049
1087
|
async function updateRegisteredTask(taskId, updates, taskRepo2) {
|
|
@@ -1054,14 +1092,14 @@ async function updateRegisteredTask(taskId, updates, taskRepo2) {
|
|
|
1054
1092
|
schedulerM("scheduler_taskNotFound", { params: { taskId } })
|
|
1055
1093
|
);
|
|
1056
1094
|
}
|
|
1057
|
-
|
|
1095
|
+
let updatedCron = getCron(taskId);
|
|
1058
1096
|
if (updates.cron !== void 0 || updates.timezone !== void 0) {
|
|
1059
1097
|
const cronExpression = updates.cron ?? existingTask.cron;
|
|
1060
1098
|
const timezone = updates.timezone === null ? void 0 : updates.timezone ?? existingTask.timezone;
|
|
1061
1099
|
const cronResult = parseCronExpression(cronExpression, timezone);
|
|
1062
1100
|
if (!cronResult.success)
|
|
1063
1101
|
return cronResult;
|
|
1064
|
-
|
|
1102
|
+
updatedCron = cronResult.data;
|
|
1065
1103
|
}
|
|
1066
1104
|
let normalizedRetry;
|
|
1067
1105
|
if (updates.retry !== void 0) {
|
|
@@ -1074,7 +1112,7 @@ async function updateRegisteredTask(taskId, updates, taskRepo2) {
|
|
|
1074
1112
|
normalizedRetry = retryResult.data;
|
|
1075
1113
|
}
|
|
1076
1114
|
}
|
|
1077
|
-
const normalizedDescription = updates.description !== void 0 ? normalizeTaskDescription(updates.description) : void 0;
|
|
1115
|
+
const normalizedDescription = updates.description !== void 0 ? normalizeTaskDescription(updates.description) ?? "" : void 0;
|
|
1078
1116
|
const normalizedUpdates = {
|
|
1079
1117
|
...updates,
|
|
1080
1118
|
...updates.description !== void 0 ? { description: normalizedDescription } : {},
|
|
@@ -1083,7 +1121,7 @@ async function updateRegisteredTask(taskId, updates, taskRepo2) {
|
|
|
1083
1121
|
const updatedTask = {
|
|
1084
1122
|
...existingTask,
|
|
1085
1123
|
...normalizedUpdates.name !== void 0 ? { name: normalizedUpdates.name } : {},
|
|
1086
|
-
...normalizedUpdates.description !== void 0 ? { description: normalizedUpdates.description } : {},
|
|
1124
|
+
...normalizedUpdates.description !== void 0 ? { description: normalizedUpdates.description || void 0 } : {},
|
|
1087
1125
|
...normalizedUpdates.cron !== void 0 ? { cron: normalizedUpdates.cron } : {},
|
|
1088
1126
|
...normalizedUpdates.timezone !== void 0 ? { timezone: normalizedUpdates.timezone ?? void 0 } : {},
|
|
1089
1127
|
...normalizedUpdates.enabled !== void 0 ? { enabled: normalizedUpdates.enabled } : {},
|
|
@@ -1092,14 +1130,10 @@ async function updateRegisteredTask(taskId, updates, taskRepo2) {
|
|
|
1092
1130
|
...normalizedUpdates.params !== void 0 ? { params: normalizedUpdates.params } : {},
|
|
1093
1131
|
...normalizedUpdates.handler !== void 0 ? { handler: normalizedUpdates.handler ?? void 0 } : {}
|
|
1094
1132
|
};
|
|
1095
|
-
setTask(taskId, updatedTask);
|
|
1096
1133
|
if (taskRepo2) {
|
|
1097
1134
|
const updateResult = await taskRepo2.updateTask(taskId, normalizedUpdates);
|
|
1098
1135
|
if (!updateResult.success) {
|
|
1099
|
-
logger4.warn("Failed to update persisted task definition
|
|
1100
|
-
setTask(taskId, existingTask);
|
|
1101
|
-
if ((updates.cron !== void 0 || updates.timezone !== void 0) && existingCron)
|
|
1102
|
-
setCron(taskId, existingCron);
|
|
1136
|
+
logger4.warn("Failed to update persisted task definition", { taskId, error: updateResult.error.message });
|
|
1103
1137
|
return err(
|
|
1104
1138
|
HaiSchedulerError.DB_SAVE_FAILED,
|
|
1105
1139
|
schedulerM("scheduler_dbSaveFailed", { params: { error: updateResult.error.message } }),
|
|
@@ -1107,6 +1141,9 @@ async function updateRegisteredTask(taskId, updates, taskRepo2) {
|
|
|
1107
1141
|
);
|
|
1108
1142
|
}
|
|
1109
1143
|
}
|
|
1144
|
+
setTask(taskId, updatedTask);
|
|
1145
|
+
if (updatedCron)
|
|
1146
|
+
setCron(taskId, updatedCron);
|
|
1110
1147
|
return ok(void 0);
|
|
1111
1148
|
}
|
|
1112
1149
|
async function queryTaskLogs(logRepo2, options) {
|
|
@@ -1227,6 +1264,7 @@ function runTask(task, minuteTimestamp, trigger = { type: "manual", source: null
|
|
|
1227
1264
|
async function runTaskExecution(task, minuteTimestamp, trigger, signal) {
|
|
1228
1265
|
const hooks = getHooks();
|
|
1229
1266
|
const repository = currentTaskRepo;
|
|
1267
|
+
const occurrenceId = minuteTimestamp === void 0 ? `manual:${randomUUID()}` : `scheduled:${minuteTimestamp}`;
|
|
1230
1268
|
const lockKey = minuteTimestamp !== void 0 ? `hai:scheduler:${task.id}:${minuteTimestamp}` : void 0;
|
|
1231
1269
|
if (cache.isInitialized && lockKey) {
|
|
1232
1270
|
const lockResult = await waitWithSignal(() => cache.lock.acquire(lockKey, { ttl: currentLockTtlSec, owner: currentNodeId }), signal).catch(() => null);
|
|
@@ -1264,7 +1302,7 @@ async function runTaskExecution(task, minuteTimestamp, trigger, signal) {
|
|
|
1264
1302
|
}
|
|
1265
1303
|
runningTasks.add(task.id);
|
|
1266
1304
|
try {
|
|
1267
|
-
|
|
1305
|
+
let maxAttempts = Math.max(1, task.retry?.maxAttempts ?? 1);
|
|
1268
1306
|
let log = null;
|
|
1269
1307
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1270
1308
|
if (signal.aborted) {
|
|
@@ -1278,12 +1316,14 @@ async function runTaskExecution(task, minuteTimestamp, trigger, signal) {
|
|
|
1278
1316
|
}
|
|
1279
1317
|
const latest = getTask(task.id);
|
|
1280
1318
|
const stillDue = minuteTimestamp === void 0 || getCron(task.id)?.match(new Date(minuteTimestamp * 6e4));
|
|
1281
|
-
if (!latest || latest.enabled === false || !stillDue) {
|
|
1319
|
+
if (!latest || latest.enabled === false || !stillDue || attempt > 1 && !isDeepStrictEqual(latest, task)) {
|
|
1282
1320
|
log = await interruptTask(task, trigger, schedulerM("scheduler_taskNoLongerDue", { params: { taskId: task.id } }), hooks, signal);
|
|
1283
1321
|
break;
|
|
1284
1322
|
}
|
|
1285
1323
|
task = latest;
|
|
1286
|
-
|
|
1324
|
+
if (attempt === 1)
|
|
1325
|
+
maxAttempts = Math.max(1, task.retry?.maxAttempts ?? 1);
|
|
1326
|
+
log = await executeTask(task, trigger, hooks, signal, occurrenceId);
|
|
1287
1327
|
if (log.status !== "failed")
|
|
1288
1328
|
break;
|
|
1289
1329
|
if (attempt >= maxAttempts)
|
|
@@ -1312,14 +1352,17 @@ async function runTaskExecution(task, minuteTimestamp, trigger, signal) {
|
|
|
1312
1352
|
logger5.debug("Task execution interrupted", { taskId: task.id, error: log.error });
|
|
1313
1353
|
else
|
|
1314
1354
|
logger5.debug("Task execution succeeded", { taskId: task.id, duration: log.duration });
|
|
1315
|
-
if (task.deleteAfterRun === true && !signal.aborted && log.status
|
|
1316
|
-
const
|
|
1355
|
+
if (task.deleteAfterRun === true && !signal.aborted && log.status === "success") {
|
|
1356
|
+
const refreshed = await loadPersistedTasks(repository);
|
|
1357
|
+
if (!refreshed.success || !isDeepStrictEqual(getTask(task.id), task))
|
|
1358
|
+
return log;
|
|
1359
|
+
const unregisterResult = await unregisterTask(task.id, repository, task);
|
|
1317
1360
|
if (!unregisterResult.success) {
|
|
1318
1361
|
logger5.warn("Failed to auto-delete one-time task after run", {
|
|
1319
1362
|
taskId: task.id,
|
|
1320
1363
|
error: unregisterResult.error.message
|
|
1321
1364
|
});
|
|
1322
|
-
} else {
|
|
1365
|
+
} else if (!getTask(task.id)) {
|
|
1323
1366
|
logger5.info("Auto-deleted one-time task after run", { taskId: task.id });
|
|
1324
1367
|
}
|
|
1325
1368
|
}
|
|
@@ -1437,6 +1480,12 @@ var scheduler = {
|
|
|
1437
1480
|
initInProgress = false;
|
|
1438
1481
|
}
|
|
1439
1482
|
},
|
|
1483
|
+
/** 刷新数据库任务投影,不执行任何任务。 */
|
|
1484
|
+
async refreshTasks() {
|
|
1485
|
+
if (!currentConfig)
|
|
1486
|
+
return notInitialized.result();
|
|
1487
|
+
return loadPersistedTasks(taskRepo);
|
|
1488
|
+
},
|
|
1440
1489
|
async register(task) {
|
|
1441
1490
|
if (!currentConfig)
|
|
1442
1491
|
return notInitialized.result();
|