@h-ai/scheduler 0.1.0-alpha5
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/LICENSE +202 -0
- package/README.md +201 -0
- package/dist/index.d.ts +364 -0
- package/dist/index.js +1416 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1416 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { cache } from '@h-ai/cache';
|
|
3
|
+
import { core, err, ok } from '@h-ai/core';
|
|
4
|
+
import { reldb, BaseReldbCrudRepository } from '@h-ai/reldb';
|
|
5
|
+
import { createHash } from 'crypto';
|
|
6
|
+
import { Script } from 'vm';
|
|
7
|
+
import { Cron } from 'croner';
|
|
8
|
+
|
|
9
|
+
// src/scheduler-config.ts
|
|
10
|
+
var SchedulerConfigSchema = z.object({
|
|
11
|
+
/** 是否启用数据库记录(默认 true,需要 @h-ai/reldb 已初始化) */
|
|
12
|
+
enableDb: z.boolean().default(true),
|
|
13
|
+
/** 调度检查间隔,单位毫秒(默认 1000,即每秒检查一次) */
|
|
14
|
+
tickInterval: z.number().int().min(100).default(1e3),
|
|
15
|
+
/** 分布式锁过期时间,单位毫秒(默认 300000,即 5 分钟) */
|
|
16
|
+
lockExpireMs: z.number().int().min(1e4).default(3e5),
|
|
17
|
+
/** 节点标识(默认自动生成 UUID,多节点部署时建议设置固定标识以便审计) */
|
|
18
|
+
nodeId: z.string().min(1).optional(),
|
|
19
|
+
/** 执行日志最大保留条数(启用 DB 时生效) */
|
|
20
|
+
maxLogs: z.number().int().min(1).optional(),
|
|
21
|
+
/** 执行日志保留天数(启用 DB 时生效) */
|
|
22
|
+
retentionDays: z.number().int().min(1).optional()
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// messages/en-US.json
|
|
26
|
+
var en_US_default = {
|
|
27
|
+
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
28
|
+
scheduler_notInitialized: "Scheduler not initialized, please call scheduler.init() first",
|
|
29
|
+
scheduler_initFailed: "Failed to initialize scheduler: {error}",
|
|
30
|
+
scheduler_alreadyRunning: "Scheduler is already running",
|
|
31
|
+
scheduler_notRunning: "Scheduler is not running",
|
|
32
|
+
scheduler_taskNotFound: "Task not found: {taskId}",
|
|
33
|
+
scheduler_taskAlreadyExists: "Task already exists: {taskId}",
|
|
34
|
+
scheduler_invalidCron: "Invalid cron expression: {expression}",
|
|
35
|
+
scheduler_executionFailed: "Task execution failed: {error}",
|
|
36
|
+
scheduler_jsExecutionFailed: "JS code execution failed: {error}",
|
|
37
|
+
scheduler_apiExecutionFailed: "API call failed: {error}",
|
|
38
|
+
scheduler_dbSaveFailed: "Failed to save execution log: {error}",
|
|
39
|
+
scheduler_dbNotInitialized: "Database not initialized, cannot save execution logs",
|
|
40
|
+
scheduler_configError: "Scheduler config validation failed: {error}",
|
|
41
|
+
scheduler_taskIdEmpty: "Task ID cannot be empty",
|
|
42
|
+
scheduler_taskRunning: "Task is currently running: {taskId}",
|
|
43
|
+
scheduler_invalidApiConfig: "Invalid api_config for task: {taskId}",
|
|
44
|
+
scheduler_lockAcquireFailed: "Failed to acquire distributed lock, another node already holds it: {taskId}",
|
|
45
|
+
scheduler_jsCompileFailed: "Failed to compile JS task: {error}",
|
|
46
|
+
scheduler_hookExecutionFailed: "Task lifecycle hook execution failed: {error}",
|
|
47
|
+
scheduler_taskHandlerMissing: "Task has no executable handler and no global execute hook is configured: {taskId}",
|
|
48
|
+
scheduler_invalidHandlerConfig: "Invalid task handler config: {taskId}",
|
|
49
|
+
scheduler_invalidRetryConfig: "Invalid retry policy config for task: {taskId}",
|
|
50
|
+
scheduler_reldbNotInitialized: "enableDb is true but @h-ai/reldb is not initialized, please call reldb.init() first"
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// messages/zh-CN.json
|
|
54
|
+
var zh_CN_default = {
|
|
55
|
+
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
56
|
+
scheduler_notInitialized: "\u5B9A\u65F6\u4EFB\u52A1\u6A21\u5757\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 scheduler.init()",
|
|
57
|
+
scheduler_initFailed: "\u5B9A\u65F6\u4EFB\u52A1\u6A21\u5757\u521D\u59CB\u5316\u5931\u8D25: {error}",
|
|
58
|
+
scheduler_alreadyRunning: "\u5B9A\u65F6\u4EFB\u52A1\u8C03\u5EA6\u5668\u5DF2\u5728\u8FD0\u884C\u4E2D",
|
|
59
|
+
scheduler_notRunning: "\u5B9A\u65F6\u4EFB\u52A1\u8C03\u5EA6\u5668\u672A\u8FD0\u884C",
|
|
60
|
+
scheduler_taskNotFound: "\u4EFB\u52A1\u672A\u627E\u5230: {taskId}",
|
|
61
|
+
scheduler_taskAlreadyExists: "\u4EFB\u52A1\u5DF2\u5B58\u5728: {taskId}",
|
|
62
|
+
scheduler_invalidCron: "\u65E0\u6548\u7684 cron \u8868\u8FBE\u5F0F: {expression}",
|
|
63
|
+
scheduler_executionFailed: "\u4EFB\u52A1\u6267\u884C\u5931\u8D25: {error}",
|
|
64
|
+
scheduler_jsExecutionFailed: "JS \u4EE3\u7801\u6267\u884C\u5931\u8D25: {error}",
|
|
65
|
+
scheduler_apiExecutionFailed: "API \u8C03\u7528\u5931\u8D25: {error}",
|
|
66
|
+
scheduler_dbSaveFailed: "\u4FDD\u5B58\u6267\u884C\u8BB0\u5F55\u5931\u8D25: {error}",
|
|
67
|
+
scheduler_dbNotInitialized: "\u6570\u636E\u5E93\u672A\u521D\u59CB\u5316\uFF0C\u65E0\u6CD5\u4FDD\u5B58\u6267\u884C\u8BB0\u5F55",
|
|
68
|
+
scheduler_configError: "\u8C03\u5EA6\u5668\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A{error}",
|
|
69
|
+
scheduler_taskIdEmpty: "\u4EFB\u52A1 ID \u4E0D\u80FD\u4E3A\u7A7A",
|
|
70
|
+
scheduler_taskRunning: "\u4EFB\u52A1\u6B63\u5728\u6267\u884C\u4E2D: {taskId}",
|
|
71
|
+
scheduler_invalidApiConfig: "\u4EFB\u52A1 api_config \u683C\u5F0F\u65E0\u6548: {taskId}",
|
|
72
|
+
scheduler_lockAcquireFailed: "\u83B7\u53D6\u5206\u5E03\u5F0F\u9501\u5931\u8D25\uFF0C\u5176\u4ED6\u8282\u70B9\u5DF2\u83B7\u9501: {taskId}",
|
|
73
|
+
scheduler_jsCompileFailed: "JS \u4EFB\u52A1\u7F16\u8BD1\u5931\u8D25: {error}",
|
|
74
|
+
scheduler_hookExecutionFailed: "\u4EFB\u52A1\u751F\u547D\u5468\u671F\u56DE\u8C03\u6267\u884C\u5931\u8D25: {error}",
|
|
75
|
+
scheduler_taskHandlerMissing: "\u4EFB\u52A1\u7F3A\u5C11\u53EF\u6267\u884C handler\uFF0C\u4E14\u672A\u914D\u7F6E\u5168\u5C40\u6267\u884C\u56DE\u8C03: {taskId}",
|
|
76
|
+
scheduler_invalidHandlerConfig: "\u4EFB\u52A1 handler \u914D\u7F6E\u65E0\u6548: {taskId}",
|
|
77
|
+
scheduler_invalidRetryConfig: "\u4EFB\u52A1\u91CD\u8BD5\u7B56\u7565\u914D\u7F6E\u65E0\u6548: {taskId}",
|
|
78
|
+
scheduler_reldbNotInitialized: "enableDb \u4E3A true \u4F46 @h-ai/reldb \u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 reldb.init()"
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/scheduler-i18n.ts
|
|
82
|
+
var schedulerM = core.i18n.createMessageGetter({
|
|
83
|
+
"zh-CN": zh_CN_default,
|
|
84
|
+
"en-US": en_US_default
|
|
85
|
+
});
|
|
86
|
+
var SchedulerErrorInfo = {
|
|
87
|
+
NOT_INITIALIZED: "010:500",
|
|
88
|
+
INIT_FAILED: "011:500",
|
|
89
|
+
CONFIG_ERROR: "012:500",
|
|
90
|
+
TASK_NOT_FOUND: "020:404",
|
|
91
|
+
TASK_ALREADY_EXISTS: "021:409",
|
|
92
|
+
INVALID_CRON: "022:400",
|
|
93
|
+
EXECUTION_FAILED: "023:500",
|
|
94
|
+
JS_EXECUTION_FAILED: "024:500",
|
|
95
|
+
API_EXECUTION_FAILED: "025:502",
|
|
96
|
+
DB_SAVE_FAILED: "026:500",
|
|
97
|
+
ALREADY_RUNNING: "027:409",
|
|
98
|
+
NOT_RUNNING: "028:400",
|
|
99
|
+
LOCK_ACQUIRE_FAILED: "029:409",
|
|
100
|
+
JS_COMPILE_FAILED: "030:500",
|
|
101
|
+
HOOK_EXECUTION_FAILED: "031:500"
|
|
102
|
+
};
|
|
103
|
+
var HaiSchedulerError = core.error.buildHaiErrorsDef("scheduler", SchedulerErrorInfo);
|
|
104
|
+
|
|
105
|
+
// src/repositories/scheduler-log-repository.ts
|
|
106
|
+
var logger = core.logger.child({ module: "scheduler", scope: "log-repository" });
|
|
107
|
+
var SCHEDULER_LOG_TABLE = "hai_scheduler_logs";
|
|
108
|
+
var SchedulerLogRepository = class extends BaseReldbCrudRepository {
|
|
109
|
+
constructor(db) {
|
|
110
|
+
super(db, {
|
|
111
|
+
table: SCHEDULER_LOG_TABLE,
|
|
112
|
+
idColumn: "id",
|
|
113
|
+
fields: [
|
|
114
|
+
{ fieldName: "id", columnName: "id", def: { type: "INTEGER", primaryKey: true, autoIncrement: true }, select: true, create: false, update: false },
|
|
115
|
+
{ fieldName: "taskId", columnName: "task_id", def: { type: "TEXT", notNull: true }, select: true, create: true, update: false },
|
|
116
|
+
{ fieldName: "taskName", columnName: "task_name", def: { type: "TEXT", notNull: true }, select: true, create: true, update: false },
|
|
117
|
+
{ fieldName: "taskType", columnName: "task_type", def: { type: "TEXT", notNull: true }, select: true, create: true, update: false },
|
|
118
|
+
{ fieldName: "triggerType", columnName: "trigger_type", def: { type: "TEXT", notNull: true }, select: true, create: true, update: false },
|
|
119
|
+
{ fieldName: "triggerSource", columnName: "trigger_source", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
120
|
+
{ fieldName: "status", columnName: "status", def: { type: "TEXT", notNull: true }, select: true, create: true, update: false },
|
|
121
|
+
{ fieldName: "result", columnName: "result", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
122
|
+
{ fieldName: "error", columnName: "error", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
123
|
+
{ fieldName: "startedAt", columnName: "started_at", def: { type: "INTEGER", notNull: true }, select: true, create: true, update: false },
|
|
124
|
+
{ fieldName: "finishedAt", columnName: "finished_at", def: { type: "INTEGER", notNull: true }, select: true, create: true, update: false },
|
|
125
|
+
{ fieldName: "duration", columnName: "duration", def: { type: "INTEGER", notNull: true }, select: true, create: true, update: false }
|
|
126
|
+
]
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
async saveLog(log) {
|
|
130
|
+
try {
|
|
131
|
+
const createResult = await this.create({
|
|
132
|
+
taskId: log.taskId,
|
|
133
|
+
taskName: log.taskName,
|
|
134
|
+
taskType: log.taskType,
|
|
135
|
+
triggerType: log.triggerType,
|
|
136
|
+
triggerSource: log.triggerSource,
|
|
137
|
+
status: log.status,
|
|
138
|
+
result: log.result,
|
|
139
|
+
error: log.error,
|
|
140
|
+
startedAt: log.startedAt,
|
|
141
|
+
finishedAt: log.finishedAt,
|
|
142
|
+
duration: log.duration
|
|
143
|
+
});
|
|
144
|
+
if (!createResult.success)
|
|
145
|
+
logger.error("Failed to save execution log", { taskId: log.taskId, error: createResult.error.message });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
logger.error("Failed to save execution log", { taskId: log.taskId, error });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async queryLogs(options) {
|
|
151
|
+
const { taskId, status, triggerType, triggerSource, startedAfter, startedBefore, pagination } = options ?? {};
|
|
152
|
+
const conditions = [];
|
|
153
|
+
const params = [];
|
|
154
|
+
if (taskId) {
|
|
155
|
+
conditions.push("task_id = ?");
|
|
156
|
+
params.push(taskId);
|
|
157
|
+
}
|
|
158
|
+
if (status) {
|
|
159
|
+
conditions.push("status = ?");
|
|
160
|
+
params.push(status);
|
|
161
|
+
}
|
|
162
|
+
if (triggerType) {
|
|
163
|
+
conditions.push("trigger_type = ?");
|
|
164
|
+
params.push(triggerType);
|
|
165
|
+
}
|
|
166
|
+
if (triggerSource) {
|
|
167
|
+
conditions.push("trigger_source = ?");
|
|
168
|
+
params.push(triggerSource);
|
|
169
|
+
}
|
|
170
|
+
if (startedAfter !== void 0) {
|
|
171
|
+
conditions.push("started_at >= ?");
|
|
172
|
+
params.push(startedAfter);
|
|
173
|
+
}
|
|
174
|
+
if (startedBefore !== void 0) {
|
|
175
|
+
conditions.push("started_at <= ?");
|
|
176
|
+
params.push(startedBefore);
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const pageResult = await this.findPage({
|
|
180
|
+
where: conditions.length > 0 ? conditions.join(" AND ") : void 0,
|
|
181
|
+
params: params.length > 0 ? params : void 0,
|
|
182
|
+
orderBy: "id DESC",
|
|
183
|
+
pagination,
|
|
184
|
+
overrides: { defaultPageSize: 20, maxPageSize: 200 }
|
|
185
|
+
});
|
|
186
|
+
if (!pageResult.success) {
|
|
187
|
+
return err(
|
|
188
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
189
|
+
schedulerM("scheduler_dbSaveFailed", { params: { error: pageResult.error.message } }),
|
|
190
|
+
pageResult.error
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return ok({
|
|
194
|
+
...pageResult.data,
|
|
195
|
+
items: pageResult.data.items.map((row) => ({
|
|
196
|
+
id: row.id,
|
|
197
|
+
taskId: row.taskId,
|
|
198
|
+
taskName: row.taskName,
|
|
199
|
+
taskType: row.taskType,
|
|
200
|
+
triggerType: row.triggerType,
|
|
201
|
+
triggerSource: row.triggerSource ?? null,
|
|
202
|
+
status: row.status,
|
|
203
|
+
result: row.result ?? null,
|
|
204
|
+
error: row.error ?? null,
|
|
205
|
+
startedAt: row.startedAt,
|
|
206
|
+
finishedAt: row.finishedAt,
|
|
207
|
+
duration: row.duration
|
|
208
|
+
}))
|
|
209
|
+
});
|
|
210
|
+
} catch (error) {
|
|
211
|
+
logger.error("Failed to query execution logs", { error });
|
|
212
|
+
return err(
|
|
213
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
214
|
+
schedulerM("scheduler_dbSaveFailed", {
|
|
215
|
+
params: { error: error instanceof Error ? error.message : String(error) }
|
|
216
|
+
}),
|
|
217
|
+
error
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async cleanupLogs(policy) {
|
|
222
|
+
const { maxLogs, retentionDays } = policy;
|
|
223
|
+
if (!maxLogs && !retentionDays)
|
|
224
|
+
return ok(void 0);
|
|
225
|
+
try {
|
|
226
|
+
if (retentionDays) {
|
|
227
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
228
|
+
const retentionResult = await this.sql().execute(
|
|
229
|
+
`DELETE FROM ${SCHEDULER_LOG_TABLE} WHERE started_at < ?`,
|
|
230
|
+
[cutoff]
|
|
231
|
+
);
|
|
232
|
+
if (!retentionResult.success) {
|
|
233
|
+
return err(
|
|
234
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
235
|
+
schedulerM("scheduler_dbSaveFailed", { params: { error: retentionResult.error.message } }),
|
|
236
|
+
retentionResult.error
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (maxLogs) {
|
|
241
|
+
const maxLogsResult = await this.sql().execute(
|
|
242
|
+
`DELETE FROM ${SCHEDULER_LOG_TABLE}
|
|
243
|
+
WHERE id NOT IN (
|
|
244
|
+
SELECT id FROM (
|
|
245
|
+
SELECT id
|
|
246
|
+
FROM ${SCHEDULER_LOG_TABLE}
|
|
247
|
+
ORDER BY started_at DESC, id DESC
|
|
248
|
+
LIMIT ?
|
|
249
|
+
) AS keep_logs
|
|
250
|
+
)`,
|
|
251
|
+
[maxLogs]
|
|
252
|
+
);
|
|
253
|
+
if (!maxLogsResult.success) {
|
|
254
|
+
return err(
|
|
255
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
256
|
+
schedulerM("scheduler_dbSaveFailed", { params: { error: maxLogsResult.error.message } }),
|
|
257
|
+
maxLogsResult.error
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return ok(void 0);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
logger.error("Failed to cleanup execution logs", { error, maxLogs, retentionDays });
|
|
264
|
+
return err(
|
|
265
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
266
|
+
schedulerM("scheduler_dbSaveFailed", {
|
|
267
|
+
params: { error: error instanceof Error ? error.message : String(error) }
|
|
268
|
+
}),
|
|
269
|
+
error
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
var logger2 = core.logger.child({ module: "scheduler", scope: "task-repository" });
|
|
275
|
+
var SCHEDULER_TASK_TABLE = "hai_scheduler_tasks";
|
|
276
|
+
var TaskParamsSchema = z.record(z.string(), z.unknown());
|
|
277
|
+
var TaskRetryPolicySchema = z.object({
|
|
278
|
+
maxAttempts: z.number().int().min(1),
|
|
279
|
+
backoffMs: z.array(z.number().int().min(0)).optional()
|
|
280
|
+
});
|
|
281
|
+
var ApiTaskConfigSchema = z.object({
|
|
282
|
+
kind: z.literal("api"),
|
|
283
|
+
url: z.string().min(1),
|
|
284
|
+
method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional(),
|
|
285
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
286
|
+
body: z.unknown().optional(),
|
|
287
|
+
timeout: z.number().int().positive().optional()
|
|
288
|
+
});
|
|
289
|
+
var JsTaskConfigSchema = z.object({
|
|
290
|
+
kind: z.literal("js"),
|
|
291
|
+
code: z.string().min(1),
|
|
292
|
+
timeout: z.number().int().positive().optional()
|
|
293
|
+
});
|
|
294
|
+
var TaskHandlerConfigSchema = z.discriminatedUnion("kind", [ApiTaskConfigSchema, JsTaskConfigSchema]);
|
|
295
|
+
var SchedulerTaskRepository = class extends BaseReldbCrudRepository {
|
|
296
|
+
constructor(db) {
|
|
297
|
+
super(db, {
|
|
298
|
+
table: SCHEDULER_TASK_TABLE,
|
|
299
|
+
idColumn: "id",
|
|
300
|
+
fields: [
|
|
301
|
+
{ fieldName: "id", columnName: "id", def: { type: "INTEGER", primaryKey: true, autoIncrement: true }, select: true, create: false, update: false },
|
|
302
|
+
{ fieldName: "taskId", columnName: "task_id", def: { type: "TEXT", notNull: true, unique: true }, select: true, create: true, update: false },
|
|
303
|
+
{ fieldName: "taskName", columnName: "task_name", def: { type: "TEXT", notNull: true }, select: true, create: true, update: true },
|
|
304
|
+
{ fieldName: "description", columnName: "description", def: { type: "TEXT" }, select: true, create: true, update: true },
|
|
305
|
+
{ fieldName: "cron", columnName: "cron", def: { type: "TEXT", notNull: true }, select: true, create: true, update: true },
|
|
306
|
+
{ fieldName: "enabled", columnName: "enabled", def: { type: "BOOLEAN", notNull: true, defaultValue: true }, select: true, create: true, update: true },
|
|
307
|
+
{ fieldName: "deleteAfterRun", columnName: "delete_after_run", def: { type: "BOOLEAN", notNull: true, defaultValue: false }, select: true, create: true, update: true },
|
|
308
|
+
{ fieldName: "retry", columnName: "retry", def: { type: "JSON" }, select: true, create: true, update: true },
|
|
309
|
+
{ fieldName: "params", columnName: "params", def: { type: "JSON" }, select: true, create: true, update: true },
|
|
310
|
+
{ fieldName: "handler", columnName: "handler", def: { type: "JSON" }, select: true, create: true, update: true },
|
|
311
|
+
{ fieldName: "createdAt", columnName: "created_at", def: { type: "TIMESTAMP" }, select: true, create: true, update: false },
|
|
312
|
+
{ fieldName: "updatedAt", columnName: "updated_at", def: { type: "TIMESTAMP" }, select: true, create: false, update: true }
|
|
313
|
+
]
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
async saveTask(task) {
|
|
317
|
+
try {
|
|
318
|
+
const createResult = await this.create({
|
|
319
|
+
taskId: task.id,
|
|
320
|
+
taskName: task.name,
|
|
321
|
+
description: task.description ?? null,
|
|
322
|
+
cron: task.cron,
|
|
323
|
+
enabled: task.enabled !== false,
|
|
324
|
+
deleteAfterRun: task.deleteAfterRun === true,
|
|
325
|
+
retry: task.retry ?? null,
|
|
326
|
+
params: task.params ?? {},
|
|
327
|
+
handler: task.handler ?? null,
|
|
328
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
329
|
+
});
|
|
330
|
+
if (!createResult.success) {
|
|
331
|
+
logger2.error("Failed to save task definition", { taskId: task.id, error: createResult.error.message });
|
|
332
|
+
return err(
|
|
333
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
334
|
+
schedulerM("scheduler_dbSaveFailed", { params: { error: createResult.error.message } }),
|
|
335
|
+
createResult.error
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return ok(void 0);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
logger2.error("Failed to save task definition", { taskId: task.id, error });
|
|
341
|
+
return err(
|
|
342
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
343
|
+
schedulerM("scheduler_dbSaveFailed", {
|
|
344
|
+
params: { error: error instanceof Error ? error.message : String(error) }
|
|
345
|
+
}),
|
|
346
|
+
error
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async updateTask(taskId, updates) {
|
|
351
|
+
try {
|
|
352
|
+
const rows = await this.findAll({ where: "task_id = ?", params: [taskId], limit: 1 });
|
|
353
|
+
if (!rows.success) {
|
|
354
|
+
return err(
|
|
355
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
356
|
+
schedulerM("scheduler_dbSaveFailed", { params: { error: rows.error.message } }),
|
|
357
|
+
rows.error
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
if (rows.data.length === 0)
|
|
361
|
+
return ok(void 0);
|
|
362
|
+
const data = {};
|
|
363
|
+
if (updates.name !== void 0)
|
|
364
|
+
data.taskName = updates.name;
|
|
365
|
+
if (updates.description !== void 0)
|
|
366
|
+
data.description = updates.description ?? null;
|
|
367
|
+
if (updates.cron !== void 0)
|
|
368
|
+
data.cron = updates.cron;
|
|
369
|
+
if (updates.enabled !== void 0)
|
|
370
|
+
data.enabled = updates.enabled;
|
|
371
|
+
if (updates.deleteAfterRun !== void 0)
|
|
372
|
+
data.deleteAfterRun = updates.deleteAfterRun;
|
|
373
|
+
if (updates.retry !== void 0)
|
|
374
|
+
data.retry = updates.retry ?? null;
|
|
375
|
+
if (updates.params !== void 0)
|
|
376
|
+
data.params = updates.params;
|
|
377
|
+
if (updates.handler !== void 0)
|
|
378
|
+
data.handler = updates.handler ?? null;
|
|
379
|
+
data.updatedAt = /* @__PURE__ */ new Date();
|
|
380
|
+
if (Object.keys(data).length <= 1)
|
|
381
|
+
return ok(void 0);
|
|
382
|
+
const updateResult = await this.updateById(rows.data[0].id, data);
|
|
383
|
+
if (!updateResult.success) {
|
|
384
|
+
logger2.error("Failed to update task definition", { taskId, error: updateResult.error.message });
|
|
385
|
+
return err(
|
|
386
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
387
|
+
schedulerM("scheduler_dbSaveFailed", { params: { error: updateResult.error.message } }),
|
|
388
|
+
updateResult.error
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
return ok(void 0);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
logger2.error("Failed to update task definition", { taskId, error });
|
|
394
|
+
return err(
|
|
395
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
396
|
+
schedulerM("scheduler_dbSaveFailed", {
|
|
397
|
+
params: { error: error instanceof Error ? error.message : String(error) }
|
|
398
|
+
}),
|
|
399
|
+
error
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async deleteTask(taskId) {
|
|
404
|
+
try {
|
|
405
|
+
const rows = await this.findAll({ where: "task_id = ?", params: [taskId], limit: 1 });
|
|
406
|
+
if (!rows.success) {
|
|
407
|
+
return err(
|
|
408
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
409
|
+
schedulerM("scheduler_dbSaveFailed", { params: { error: rows.error.message } }),
|
|
410
|
+
rows.error
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (rows.data.length === 0)
|
|
414
|
+
return ok(void 0);
|
|
415
|
+
const deleteResult = await this.deleteById(rows.data[0].id);
|
|
416
|
+
if (!deleteResult.success) {
|
|
417
|
+
logger2.error("Failed to delete task definition", { taskId, error: deleteResult.error.message });
|
|
418
|
+
return err(
|
|
419
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
420
|
+
schedulerM("scheduler_dbSaveFailed", { params: { error: deleteResult.error.message } }),
|
|
421
|
+
deleteResult.error
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
return ok(void 0);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
logger2.error("Failed to delete task definition", { taskId, error });
|
|
427
|
+
return err(
|
|
428
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
429
|
+
schedulerM("scheduler_dbSaveFailed", {
|
|
430
|
+
params: { error: error instanceof Error ? error.message : String(error) }
|
|
431
|
+
}),
|
|
432
|
+
error
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async loadTasks() {
|
|
437
|
+
try {
|
|
438
|
+
const rows = await this.findAll({ orderBy: "id ASC" });
|
|
439
|
+
if (!rows.success) {
|
|
440
|
+
return err(
|
|
441
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
442
|
+
schedulerM("scheduler_dbSaveFailed", { params: { error: rows.error.message } }),
|
|
443
|
+
rows.error
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
const tasks = [];
|
|
447
|
+
for (const row of rows.data) {
|
|
448
|
+
const paramsResult = TaskParamsSchema.safeParse(row.params ?? {});
|
|
449
|
+
if (!paramsResult.success) {
|
|
450
|
+
logger2.warn("Skipping task with invalid params", { taskId: row.taskId, error: paramsResult.error.message });
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const handlerResult = row.handler == null ? { success: true, data: void 0 } : TaskHandlerConfigSchema.safeParse(row.handler);
|
|
454
|
+
if (!handlerResult.success) {
|
|
455
|
+
logger2.warn("Skipping task with invalid handler config", { taskId: row.taskId, error: handlerResult.error.message });
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const retryResult = row.retry == null ? { success: true, data: void 0 } : TaskRetryPolicySchema.safeParse(row.retry);
|
|
459
|
+
if (!retryResult.success) {
|
|
460
|
+
logger2.warn("Skipping task with invalid retry policy", { taskId: row.taskId, error: retryResult.error.message });
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
tasks.push({
|
|
464
|
+
id: row.taskId,
|
|
465
|
+
name: row.taskName,
|
|
466
|
+
...row.description ? { description: row.description } : {},
|
|
467
|
+
cron: row.cron,
|
|
468
|
+
enabled: row.enabled,
|
|
469
|
+
deleteAfterRun: row.deleteAfterRun,
|
|
470
|
+
...retryResult.data ? { retry: retryResult.data } : {},
|
|
471
|
+
params: paramsResult.data,
|
|
472
|
+
...handlerResult.data ? { handler: handlerResult.data } : {}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
return ok(tasks);
|
|
476
|
+
} catch (error) {
|
|
477
|
+
logger2.error("Failed to load task definitions", { error });
|
|
478
|
+
return err(
|
|
479
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
480
|
+
schedulerM("scheduler_dbSaveFailed", {
|
|
481
|
+
params: { error: error instanceof Error ? error.message : String(error) }
|
|
482
|
+
}),
|
|
483
|
+
error
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var compiledHandlerCache = /* @__PURE__ */ new Map();
|
|
489
|
+
function createCacheKey(config) {
|
|
490
|
+
const keySource = `${config.code}:${config.timeout ?? 0}`;
|
|
491
|
+
return createHash("sha256").update(keySource).digest("hex");
|
|
492
|
+
}
|
|
493
|
+
function clearJsTaskHandlerCache() {
|
|
494
|
+
compiledHandlerCache.clear();
|
|
495
|
+
}
|
|
496
|
+
function compileJsTaskHandler(config) {
|
|
497
|
+
const cacheKey = createCacheKey(config);
|
|
498
|
+
const cachedHandler = compiledHandlerCache.get(cacheKey);
|
|
499
|
+
if (cachedHandler)
|
|
500
|
+
return ok(cachedHandler);
|
|
501
|
+
try {
|
|
502
|
+
const script = new Script(`(${config.code})`);
|
|
503
|
+
const candidate = script.runInNewContext({});
|
|
504
|
+
if (typeof candidate !== "function") {
|
|
505
|
+
return err(
|
|
506
|
+
HaiSchedulerError.JS_COMPILE_FAILED,
|
|
507
|
+
schedulerM("scheduler_jsCompileFailed", { params: { error: "Compiled result is not a function" } })
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const handler = async (context) => {
|
|
511
|
+
const functionResult = candidate(context);
|
|
512
|
+
if (!config.timeout || config.timeout <= 0)
|
|
513
|
+
return functionResult;
|
|
514
|
+
return await new Promise((resolve, reject) => {
|
|
515
|
+
const timer = setTimeout(() => {
|
|
516
|
+
reject(new Error(`JS task timed out after ${config.timeout}ms`));
|
|
517
|
+
}, config.timeout);
|
|
518
|
+
Promise.resolve(functionResult).then(
|
|
519
|
+
(result) => {
|
|
520
|
+
clearTimeout(timer);
|
|
521
|
+
resolve(result);
|
|
522
|
+
},
|
|
523
|
+
(error) => {
|
|
524
|
+
clearTimeout(timer);
|
|
525
|
+
reject(error);
|
|
526
|
+
}
|
|
527
|
+
);
|
|
528
|
+
});
|
|
529
|
+
};
|
|
530
|
+
compiledHandlerCache.set(cacheKey, handler);
|
|
531
|
+
return ok(handler);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
return err(
|
|
534
|
+
HaiSchedulerError.JS_COMPILE_FAILED,
|
|
535
|
+
schedulerM("scheduler_jsCompileFailed", {
|
|
536
|
+
params: { error: error instanceof Error ? error.message : String(error) }
|
|
537
|
+
}),
|
|
538
|
+
error
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/scheduler-executor.ts
|
|
544
|
+
var logger3 = core.logger.child({ module: "scheduler", scope: "executor" });
|
|
545
|
+
var currentLogRepo = null;
|
|
546
|
+
var currentCleanupPolicy = {};
|
|
547
|
+
function setLogRepository(repo, cleanupPolicy = {}) {
|
|
548
|
+
currentLogRepo = repo;
|
|
549
|
+
currentCleanupPolicy = { ...cleanupPolicy };
|
|
550
|
+
}
|
|
551
|
+
function sanitizeUrl(url) {
|
|
552
|
+
try {
|
|
553
|
+
const parsedUrl = new URL(url);
|
|
554
|
+
if (parsedUrl.password)
|
|
555
|
+
parsedUrl.password = "***";
|
|
556
|
+
if (parsedUrl.username)
|
|
557
|
+
parsedUrl.username = "***";
|
|
558
|
+
return parsedUrl.toString();
|
|
559
|
+
} catch {
|
|
560
|
+
return "(invalid url)";
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function resolveTaskType(task, hooks) {
|
|
564
|
+
if (task.handler)
|
|
565
|
+
return task.handler.kind;
|
|
566
|
+
if (hooks.onTaskExecute)
|
|
567
|
+
return "hook";
|
|
568
|
+
return "hook";
|
|
569
|
+
}
|
|
570
|
+
function createTaskContext(task, trigger) {
|
|
571
|
+
return {
|
|
572
|
+
task,
|
|
573
|
+
taskId: task.id,
|
|
574
|
+
params: task.params ?? {},
|
|
575
|
+
trigger
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
async function notifyTaskStart(hooks, event) {
|
|
579
|
+
if (!hooks.onTaskStart)
|
|
580
|
+
return;
|
|
581
|
+
try {
|
|
582
|
+
await hooks.onTaskStart(event);
|
|
583
|
+
} catch (error) {
|
|
584
|
+
logger3.warn("Task start hook failed", { taskId: event.task.id, error });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
async function notifyTaskInterrupted(hooks, event) {
|
|
588
|
+
if (!hooks.onTaskInterrupted)
|
|
589
|
+
return;
|
|
590
|
+
try {
|
|
591
|
+
await hooks.onTaskInterrupted(event);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
logger3.warn("Task interrupted hook failed", { taskId: event.task.id, error });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async function notifyTaskFinish(hooks, event) {
|
|
597
|
+
if (!hooks.onTaskFinish)
|
|
598
|
+
return;
|
|
599
|
+
try {
|
|
600
|
+
await hooks.onTaskFinish(event);
|
|
601
|
+
} catch (error) {
|
|
602
|
+
logger3.warn("Task finish hook failed", { taskId: event.task.id, error });
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function saveExecutionLog(task, trigger, taskType, status, startedAt, finishedAt, result, error) {
|
|
606
|
+
const log = {
|
|
607
|
+
id: 0,
|
|
608
|
+
taskId: task.id,
|
|
609
|
+
taskName: task.name,
|
|
610
|
+
taskType,
|
|
611
|
+
triggerType: trigger.type,
|
|
612
|
+
triggerSource: trigger.source,
|
|
613
|
+
status,
|
|
614
|
+
result,
|
|
615
|
+
error,
|
|
616
|
+
startedAt,
|
|
617
|
+
finishedAt,
|
|
618
|
+
duration: finishedAt - startedAt
|
|
619
|
+
};
|
|
620
|
+
if (!currentLogRepo)
|
|
621
|
+
return log;
|
|
622
|
+
await currentLogRepo.saveLog(log);
|
|
623
|
+
const cleanupResult = await currentLogRepo.cleanupLogs(currentCleanupPolicy);
|
|
624
|
+
if (!cleanupResult.success) {
|
|
625
|
+
logger3.warn("Failed to cleanup execution logs", {
|
|
626
|
+
taskId: task.id,
|
|
627
|
+
error: cleanupResult.error.message,
|
|
628
|
+
maxLogs: currentCleanupPolicy.maxLogs,
|
|
629
|
+
retentionDays: currentCleanupPolicy.retentionDays
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
return log;
|
|
633
|
+
}
|
|
634
|
+
async function saveInterruptedTaskLog(task, trigger, reason, taskType, startedAt, finishedAt) {
|
|
635
|
+
return saveExecutionLog(task, trigger, taskType, "interrupted", startedAt, finishedAt, null, reason);
|
|
636
|
+
}
|
|
637
|
+
async function interruptTask(task, trigger, reason, hooks) {
|
|
638
|
+
const startedAt = Date.now();
|
|
639
|
+
const taskType = resolveTaskType(task, hooks);
|
|
640
|
+
await notifyTaskStart(hooks, { task, trigger, startedAt });
|
|
641
|
+
const finishedAt = Date.now();
|
|
642
|
+
const interruptedLog = await saveInterruptedTaskLog(task, trigger, reason, taskType, startedAt, finishedAt);
|
|
643
|
+
await notifyTaskInterrupted(hooks, {
|
|
644
|
+
task,
|
|
645
|
+
trigger,
|
|
646
|
+
startedAt,
|
|
647
|
+
interruptedAt: finishedAt,
|
|
648
|
+
reason
|
|
649
|
+
});
|
|
650
|
+
return interruptedLog;
|
|
651
|
+
}
|
|
652
|
+
async function executeJsTask(task, context) {
|
|
653
|
+
const handler = task.handler;
|
|
654
|
+
if (!handler || handler.kind !== "js") {
|
|
655
|
+
return err(
|
|
656
|
+
HaiSchedulerError.EXECUTION_FAILED,
|
|
657
|
+
schedulerM("scheduler_invalidHandlerConfig", { params: { taskId: task.id } })
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
const compileResult = compileJsTaskHandler(handler);
|
|
661
|
+
if (!compileResult.success)
|
|
662
|
+
return compileResult;
|
|
663
|
+
try {
|
|
664
|
+
const result = await compileResult.data(context);
|
|
665
|
+
return ok(result !== void 0 ? JSON.stringify(result) : null);
|
|
666
|
+
} catch (error) {
|
|
667
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
668
|
+
logger3.error("JS task execution failed", { taskId: task.id, error: message });
|
|
669
|
+
return err(
|
|
670
|
+
HaiSchedulerError.JS_EXECUTION_FAILED,
|
|
671
|
+
schedulerM("scheduler_jsExecutionFailed", { params: { error: message } }),
|
|
672
|
+
error
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
async function executeApiTask(task, context) {
|
|
677
|
+
const handler = task.handler;
|
|
678
|
+
if (!handler || handler.kind !== "api") {
|
|
679
|
+
return err(
|
|
680
|
+
HaiSchedulerError.EXECUTION_FAILED,
|
|
681
|
+
schedulerM("scheduler_invalidHandlerConfig", { params: { taskId: task.id } })
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
const { url, method = "GET", headers, body, timeout = 3e4 } = handler;
|
|
685
|
+
const controller = new AbortController();
|
|
686
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
687
|
+
try {
|
|
688
|
+
const response = await fetch(url, {
|
|
689
|
+
method,
|
|
690
|
+
headers: {
|
|
691
|
+
...headers,
|
|
692
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
693
|
+
},
|
|
694
|
+
signal: controller.signal,
|
|
695
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
696
|
+
});
|
|
697
|
+
clearTimeout(timer);
|
|
698
|
+
const responseText = await response.text();
|
|
699
|
+
if (!response.ok) {
|
|
700
|
+
logger3.error("API task returned non-OK status", {
|
|
701
|
+
taskId: context.taskId,
|
|
702
|
+
status: response.status,
|
|
703
|
+
url: sanitizeUrl(url)
|
|
704
|
+
});
|
|
705
|
+
return err(
|
|
706
|
+
HaiSchedulerError.API_EXECUTION_FAILED,
|
|
707
|
+
schedulerM("scheduler_apiExecutionFailed", {
|
|
708
|
+
params: { error: `HTTP ${response.status}: ${responseText.slice(0, 200)}` }
|
|
709
|
+
})
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
return ok(responseText || null);
|
|
713
|
+
} catch (error) {
|
|
714
|
+
clearTimeout(timer);
|
|
715
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
716
|
+
logger3.error("API task execution failed", { taskId: context.taskId, error: message, url: sanitizeUrl(url) });
|
|
717
|
+
return err(
|
|
718
|
+
HaiSchedulerError.API_EXECUTION_FAILED,
|
|
719
|
+
schedulerM("scheduler_apiExecutionFailed", { params: { error: message } }),
|
|
720
|
+
error
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async function executeHookTask(task, context, hooks, startedAt) {
|
|
725
|
+
if (!hooks.onTaskExecute) {
|
|
726
|
+
return err(
|
|
727
|
+
HaiSchedulerError.EXECUTION_FAILED,
|
|
728
|
+
schedulerM("scheduler_taskHandlerMissing", { params: { taskId: task.id } })
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
const executeEvent = {
|
|
732
|
+
task,
|
|
733
|
+
trigger: context.trigger,
|
|
734
|
+
startedAt,
|
|
735
|
+
context
|
|
736
|
+
};
|
|
737
|
+
try {
|
|
738
|
+
const result = await hooks.onTaskExecute(executeEvent);
|
|
739
|
+
return ok(result !== void 0 ? JSON.stringify(result) : null);
|
|
740
|
+
} catch (error) {
|
|
741
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
742
|
+
logger3.error("Task execute hook failed", { taskId: task.id, error: message });
|
|
743
|
+
return err(
|
|
744
|
+
HaiSchedulerError.HOOK_EXECUTION_FAILED,
|
|
745
|
+
schedulerM("scheduler_hookExecutionFailed", { params: { error: message } }),
|
|
746
|
+
error
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
async function executeTask(task, trigger, hooks) {
|
|
751
|
+
const startedAt = Date.now();
|
|
752
|
+
const taskType = resolveTaskType(task, hooks);
|
|
753
|
+
const context = createTaskContext(task, trigger);
|
|
754
|
+
await notifyTaskStart(hooks, { task, trigger, startedAt });
|
|
755
|
+
let executionResult;
|
|
756
|
+
let shouldNotifyInterrupted = false;
|
|
757
|
+
if (!task.handler) {
|
|
758
|
+
executionResult = await executeHookTask(task, context, hooks, startedAt);
|
|
759
|
+
shouldNotifyInterrupted = !executionResult.success;
|
|
760
|
+
} else if (task.handler.kind === "api") {
|
|
761
|
+
executionResult = await executeApiTask(task, context);
|
|
762
|
+
} else {
|
|
763
|
+
executionResult = await executeJsTask(task, context);
|
|
764
|
+
}
|
|
765
|
+
const finishedAt = Date.now();
|
|
766
|
+
const status = executionResult.success ? "success" : shouldNotifyInterrupted ? "interrupted" : "failed";
|
|
767
|
+
const log = await saveExecutionLog(
|
|
768
|
+
task,
|
|
769
|
+
trigger,
|
|
770
|
+
taskType,
|
|
771
|
+
status,
|
|
772
|
+
startedAt,
|
|
773
|
+
finishedAt,
|
|
774
|
+
executionResult.success ? executionResult.data : null,
|
|
775
|
+
executionResult.success ? null : executionResult.error.message
|
|
776
|
+
);
|
|
777
|
+
if (!executionResult.success && shouldNotifyInterrupted) {
|
|
778
|
+
await notifyTaskInterrupted(hooks, {
|
|
779
|
+
task,
|
|
780
|
+
trigger,
|
|
781
|
+
startedAt,
|
|
782
|
+
interruptedAt: finishedAt,
|
|
783
|
+
reason: executionResult.error.message
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
if (status !== "interrupted")
|
|
787
|
+
await notifyTaskFinish(hooks, { task, trigger, startedAt, finishedAt, log });
|
|
788
|
+
return log;
|
|
789
|
+
}
|
|
790
|
+
function parseCronExpression(expression) {
|
|
791
|
+
try {
|
|
792
|
+
const cron = new Cron(expression, { legacyMode: false });
|
|
793
|
+
return ok(cron);
|
|
794
|
+
} catch {
|
|
795
|
+
return err(
|
|
796
|
+
HaiSchedulerError.INVALID_CRON,
|
|
797
|
+
schedulerM("scheduler_invalidCron", { params: { expression } })
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// src/scheduler-functions.ts
|
|
803
|
+
var logger4 = core.logger.child({ module: "scheduler", scope: "functions" });
|
|
804
|
+
var taskRegistry = /* @__PURE__ */ new Map();
|
|
805
|
+
var cronCache = /* @__PURE__ */ new Map();
|
|
806
|
+
var currentHooks = {};
|
|
807
|
+
function getTaskRegistry() {
|
|
808
|
+
return taskRegistry;
|
|
809
|
+
}
|
|
810
|
+
function getTask(taskId) {
|
|
811
|
+
return taskRegistry.get(taskId);
|
|
812
|
+
}
|
|
813
|
+
function hasTask(taskId) {
|
|
814
|
+
return taskRegistry.has(taskId);
|
|
815
|
+
}
|
|
816
|
+
function setTask(taskId, task) {
|
|
817
|
+
taskRegistry.set(taskId, task);
|
|
818
|
+
}
|
|
819
|
+
function deleteTask(taskId) {
|
|
820
|
+
taskRegistry.delete(taskId);
|
|
821
|
+
}
|
|
822
|
+
function getCron(taskId) {
|
|
823
|
+
return cronCache.get(taskId);
|
|
824
|
+
}
|
|
825
|
+
function setCron(taskId, cron) {
|
|
826
|
+
cronCache.set(taskId, cron);
|
|
827
|
+
}
|
|
828
|
+
function deleteCron(taskId) {
|
|
829
|
+
cronCache.delete(taskId);
|
|
830
|
+
}
|
|
831
|
+
function setHooks(hooks) {
|
|
832
|
+
currentHooks = { ...hooks };
|
|
833
|
+
}
|
|
834
|
+
function clearHooks() {
|
|
835
|
+
currentHooks = {};
|
|
836
|
+
}
|
|
837
|
+
function getHooks() {
|
|
838
|
+
return currentHooks;
|
|
839
|
+
}
|
|
840
|
+
function resetTaskState() {
|
|
841
|
+
taskRegistry.clear();
|
|
842
|
+
cronCache.clear();
|
|
843
|
+
currentHooks = {};
|
|
844
|
+
}
|
|
845
|
+
function normalizeTaskDescription(description) {
|
|
846
|
+
if (description === void 0)
|
|
847
|
+
return void 0;
|
|
848
|
+
const trimmed = description.trim();
|
|
849
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
850
|
+
}
|
|
851
|
+
function validateRetryPolicy(taskId, retry) {
|
|
852
|
+
if (!Number.isInteger(retry.maxAttempts) || retry.maxAttempts < 1) {
|
|
853
|
+
return err(
|
|
854
|
+
HaiSchedulerError.EXECUTION_FAILED,
|
|
855
|
+
schedulerM("scheduler_invalidRetryConfig", { params: { taskId } })
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
if (retry.backoffMs !== void 0) {
|
|
859
|
+
const hasInvalidBackoff = retry.backoffMs.some((ms) => !Number.isInteger(ms) || ms < 0);
|
|
860
|
+
if (hasInvalidBackoff) {
|
|
861
|
+
return err(
|
|
862
|
+
HaiSchedulerError.EXECUTION_FAILED,
|
|
863
|
+
schedulerM("scheduler_invalidRetryConfig", { params: { taskId } })
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return ok({
|
|
868
|
+
maxAttempts: retry.maxAttempts,
|
|
869
|
+
...retry.backoffMs ? { backoffMs: [...retry.backoffMs] } : {}
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
async function loadPersistedTasks(taskRepo2) {
|
|
873
|
+
if (!taskRepo2)
|
|
874
|
+
return;
|
|
875
|
+
const loadResult = await taskRepo2.loadTasks();
|
|
876
|
+
if (!loadResult.success) {
|
|
877
|
+
logger4.warn("Failed to load persisted tasks", { error: loadResult.error.message });
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
let loadedCount = 0;
|
|
881
|
+
for (const task of loadResult.data) {
|
|
882
|
+
const registerResult = await registerTask(task);
|
|
883
|
+
if (registerResult.success) {
|
|
884
|
+
loadedCount++;
|
|
885
|
+
logger4.debug("Loaded persisted task", { taskId: task.id, taskName: task.name });
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
logger4.warn("Failed to load persisted task", { taskId: task.id, error: registerResult.error.message });
|
|
889
|
+
}
|
|
890
|
+
if (loadedCount > 0)
|
|
891
|
+
logger4.info("Loaded persisted tasks", { count: loadedCount });
|
|
892
|
+
}
|
|
893
|
+
async function loadConfigTasks(tasks) {
|
|
894
|
+
let loadedCount = 0;
|
|
895
|
+
for (const task of tasks) {
|
|
896
|
+
if (!task.id) {
|
|
897
|
+
logger4.warn("Skipping config task with empty id");
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (hasTask(task.id)) {
|
|
901
|
+
logger4.debug("Skipping config task, already registered from DB", { taskId: task.id });
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
const registerResult = await registerTask(task);
|
|
905
|
+
if (registerResult.success) {
|
|
906
|
+
loadedCount++;
|
|
907
|
+
logger4.debug("Loaded config task", { taskId: task.id, taskName: task.name });
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
logger4.warn("Failed to load config task", { taskId: task.id, error: registerResult.error.message });
|
|
911
|
+
}
|
|
912
|
+
if (loadedCount > 0)
|
|
913
|
+
logger4.info("Loaded config tasks", { count: loadedCount });
|
|
914
|
+
}
|
|
915
|
+
async function registerTask(task, taskRepo2) {
|
|
916
|
+
if (!task.id) {
|
|
917
|
+
return err(
|
|
918
|
+
HaiSchedulerError.EXECUTION_FAILED,
|
|
919
|
+
schedulerM("scheduler_taskIdEmpty")
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
if (hasTask(task.id)) {
|
|
923
|
+
return err(
|
|
924
|
+
HaiSchedulerError.TASK_ALREADY_EXISTS,
|
|
925
|
+
schedulerM("scheduler_taskAlreadyExists", { params: { taskId: task.id } })
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
const cronResult = parseCronExpression(task.cron);
|
|
929
|
+
if (!cronResult.success)
|
|
930
|
+
return cronResult;
|
|
931
|
+
const retryResult = task.retry ? validateRetryPolicy(task.id, task.retry) : ok(void 0);
|
|
932
|
+
if (!retryResult.success)
|
|
933
|
+
return retryResult;
|
|
934
|
+
const normalizedTask = {
|
|
935
|
+
...task,
|
|
936
|
+
description: normalizeTaskDescription(task.description),
|
|
937
|
+
enabled: task.enabled !== false,
|
|
938
|
+
deleteAfterRun: task.deleteAfterRun === true,
|
|
939
|
+
retry: retryResult.data,
|
|
940
|
+
params: task.params ?? {}
|
|
941
|
+
};
|
|
942
|
+
setTask(task.id, normalizedTask);
|
|
943
|
+
setCron(task.id, cronResult.data);
|
|
944
|
+
if (taskRepo2) {
|
|
945
|
+
const saveResult = await taskRepo2.saveTask(normalizedTask);
|
|
946
|
+
if (!saveResult.success) {
|
|
947
|
+
logger4.warn("Failed to persist task definition, rolling back in-memory registration", { taskId: task.id, error: saveResult.error.message });
|
|
948
|
+
deleteTask(task.id);
|
|
949
|
+
deleteCron(task.id);
|
|
950
|
+
return err(
|
|
951
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
952
|
+
schedulerM("scheduler_dbSaveFailed", { params: { error: saveResult.error.message } }),
|
|
953
|
+
saveResult.error
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return ok(void 0);
|
|
958
|
+
}
|
|
959
|
+
async function unregisterTask(taskId, taskRepo2) {
|
|
960
|
+
if (!hasTask(taskId)) {
|
|
961
|
+
return err(
|
|
962
|
+
HaiSchedulerError.TASK_NOT_FOUND,
|
|
963
|
+
schedulerM("scheduler_taskNotFound", { params: { taskId } })
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
deleteTask(taskId);
|
|
967
|
+
deleteCron(taskId);
|
|
968
|
+
if (taskRepo2) {
|
|
969
|
+
const deleteResult = await taskRepo2.deleteTask(taskId);
|
|
970
|
+
if (!deleteResult.success) {
|
|
971
|
+
logger4.warn("Failed to delete persisted task definition", { taskId, error: deleteResult.error.message });
|
|
972
|
+
return err(
|
|
973
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
974
|
+
schedulerM("scheduler_dbSaveFailed", { params: { error: deleteResult.error.message } }),
|
|
975
|
+
deleteResult.error
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return ok(void 0);
|
|
980
|
+
}
|
|
981
|
+
async function updateRegisteredTask(taskId, updates, taskRepo2) {
|
|
982
|
+
const existingTask = getTask(taskId);
|
|
983
|
+
if (!existingTask) {
|
|
984
|
+
return err(
|
|
985
|
+
HaiSchedulerError.TASK_NOT_FOUND,
|
|
986
|
+
schedulerM("scheduler_taskNotFound", { params: { taskId } })
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
const existingCron = getCron(taskId);
|
|
990
|
+
if (updates.cron !== void 0) {
|
|
991
|
+
const cronResult = parseCronExpression(updates.cron);
|
|
992
|
+
if (!cronResult.success)
|
|
993
|
+
return cronResult;
|
|
994
|
+
setCron(taskId, cronResult.data);
|
|
995
|
+
}
|
|
996
|
+
let normalizedRetry;
|
|
997
|
+
if (updates.retry !== void 0) {
|
|
998
|
+
if (updates.retry === null) {
|
|
999
|
+
normalizedRetry = null;
|
|
1000
|
+
} else {
|
|
1001
|
+
const retryResult = validateRetryPolicy(taskId, updates.retry);
|
|
1002
|
+
if (!retryResult.success)
|
|
1003
|
+
return retryResult;
|
|
1004
|
+
normalizedRetry = retryResult.data;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
const normalizedDescription = updates.description !== void 0 ? normalizeTaskDescription(updates.description) : void 0;
|
|
1008
|
+
const normalizedUpdates = {
|
|
1009
|
+
...updates,
|
|
1010
|
+
...updates.description !== void 0 ? { description: normalizedDescription } : {},
|
|
1011
|
+
...updates.retry !== void 0 ? { retry: normalizedRetry } : {}
|
|
1012
|
+
};
|
|
1013
|
+
const updatedTask = {
|
|
1014
|
+
...existingTask,
|
|
1015
|
+
...normalizedUpdates.name !== void 0 ? { name: normalizedUpdates.name } : {},
|
|
1016
|
+
...normalizedUpdates.description !== void 0 ? { description: normalizedUpdates.description } : {},
|
|
1017
|
+
...normalizedUpdates.cron !== void 0 ? { cron: normalizedUpdates.cron } : {},
|
|
1018
|
+
...normalizedUpdates.enabled !== void 0 ? { enabled: normalizedUpdates.enabled } : {},
|
|
1019
|
+
...normalizedUpdates.deleteAfterRun !== void 0 ? { deleteAfterRun: normalizedUpdates.deleteAfterRun } : {},
|
|
1020
|
+
...normalizedUpdates.retry !== void 0 ? { retry: normalizedUpdates.retry ?? void 0 } : {},
|
|
1021
|
+
...normalizedUpdates.params !== void 0 ? { params: normalizedUpdates.params } : {},
|
|
1022
|
+
...normalizedUpdates.handler !== void 0 ? { handler: normalizedUpdates.handler ?? void 0 } : {}
|
|
1023
|
+
};
|
|
1024
|
+
setTask(taskId, updatedTask);
|
|
1025
|
+
if (taskRepo2) {
|
|
1026
|
+
const updateResult = await taskRepo2.updateTask(taskId, normalizedUpdates);
|
|
1027
|
+
if (!updateResult.success) {
|
|
1028
|
+
logger4.warn("Failed to update persisted task definition, rolling back in-memory update", { taskId, error: updateResult.error.message });
|
|
1029
|
+
setTask(taskId, existingTask);
|
|
1030
|
+
if (updates.cron !== void 0 && existingCron)
|
|
1031
|
+
setCron(taskId, existingCron);
|
|
1032
|
+
return err(
|
|
1033
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
1034
|
+
schedulerM("scheduler_dbSaveFailed", { params: { error: updateResult.error.message } }),
|
|
1035
|
+
updateResult.error
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return ok(void 0);
|
|
1040
|
+
}
|
|
1041
|
+
async function queryTaskLogs(logRepo2, options) {
|
|
1042
|
+
if (!logRepo2) {
|
|
1043
|
+
return err(
|
|
1044
|
+
HaiSchedulerError.DB_SAVE_FAILED,
|
|
1045
|
+
schedulerM("scheduler_dbNotInitialized")
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
return logRepo2.queryLogs(options);
|
|
1049
|
+
}
|
|
1050
|
+
var logger5 = core.logger.child({ module: "scheduler", scope: "runner" });
|
|
1051
|
+
var runningTasks = /* @__PURE__ */ new Set();
|
|
1052
|
+
var tickTimer = null;
|
|
1053
|
+
var lastTickMinute = -1;
|
|
1054
|
+
var currentNodeId = "";
|
|
1055
|
+
var currentLockTtlSec = 300;
|
|
1056
|
+
var currentTaskRepo = null;
|
|
1057
|
+
function configureLock(nodeId, lockTtlSec) {
|
|
1058
|
+
currentNodeId = nodeId;
|
|
1059
|
+
currentLockTtlSec = lockTtlSec;
|
|
1060
|
+
}
|
|
1061
|
+
function setTaskRepository(repo) {
|
|
1062
|
+
currentTaskRepo = repo;
|
|
1063
|
+
}
|
|
1064
|
+
function isTaskRunning(taskId) {
|
|
1065
|
+
return runningTasks.has(taskId);
|
|
1066
|
+
}
|
|
1067
|
+
function isTimerRunning() {
|
|
1068
|
+
return tickTimer !== null;
|
|
1069
|
+
}
|
|
1070
|
+
function resetRunner() {
|
|
1071
|
+
runningTasks.clear();
|
|
1072
|
+
if (tickTimer) {
|
|
1073
|
+
clearInterval(tickTimer);
|
|
1074
|
+
tickTimer = null;
|
|
1075
|
+
}
|
|
1076
|
+
lastTickMinute = -1;
|
|
1077
|
+
currentNodeId = "";
|
|
1078
|
+
currentLockTtlSec = 300;
|
|
1079
|
+
currentTaskRepo = null;
|
|
1080
|
+
}
|
|
1081
|
+
async function sleep(ms) {
|
|
1082
|
+
if (ms <= 0)
|
|
1083
|
+
return;
|
|
1084
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1085
|
+
}
|
|
1086
|
+
function resolveRetryBackoffMs(task, attempt) {
|
|
1087
|
+
const backoff = task.retry?.backoffMs;
|
|
1088
|
+
if (!backoff || backoff.length === 0)
|
|
1089
|
+
return 0;
|
|
1090
|
+
const index = Math.min(Math.max(attempt - 2, 0), backoff.length - 1);
|
|
1091
|
+
return backoff[index] ?? 0;
|
|
1092
|
+
}
|
|
1093
|
+
function tick() {
|
|
1094
|
+
const now = /* @__PURE__ */ new Date();
|
|
1095
|
+
const currentMinute = Math.floor(now.getTime() / 6e4);
|
|
1096
|
+
if (currentMinute === lastTickMinute)
|
|
1097
|
+
return;
|
|
1098
|
+
lastTickMinute = currentMinute;
|
|
1099
|
+
for (const [taskId, task] of getTaskRegistry()) {
|
|
1100
|
+
if (task.enabled === false)
|
|
1101
|
+
continue;
|
|
1102
|
+
const cron = getCron(taskId);
|
|
1103
|
+
if (!cron || !cron.match(now))
|
|
1104
|
+
continue;
|
|
1105
|
+
if (runningTasks.has(taskId)) {
|
|
1106
|
+
logger5.debug("Skipping task already running", { taskId, taskName: task.name });
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
void runTask(task, currentMinute, { type: "scheduled", source: null }).catch((error) => {
|
|
1110
|
+
logger5.error("Unhandled task execution error", { taskId, error });
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
async function runTask(task, minuteTimestamp, trigger = { type: "manual", source: null }) {
|
|
1115
|
+
const lockKey = minuteTimestamp !== void 0 ? `hai:scheduler:${task.id}:${minuteTimestamp}` : void 0;
|
|
1116
|
+
if (cache.isInitialized && lockKey) {
|
|
1117
|
+
const lockResult = await cache.lock.acquire(lockKey, { ttl: currentLockTtlSec, owner: currentNodeId });
|
|
1118
|
+
if (!lockResult.success) {
|
|
1119
|
+
logger5.warn("Failed to acquire distributed lock, interrupting task", { taskId: task.id, error: lockResult.error.message });
|
|
1120
|
+
return interruptTask(
|
|
1121
|
+
task,
|
|
1122
|
+
trigger,
|
|
1123
|
+
schedulerM("scheduler_lockAcquireFailed", { params: { taskId: task.id } }),
|
|
1124
|
+
getHooks()
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
if (!lockResult.data) {
|
|
1128
|
+
logger5.debug("Skipping task, another node holds the lock", { taskId: task.id, minuteTimestamp });
|
|
1129
|
+
return interruptTask(
|
|
1130
|
+
task,
|
|
1131
|
+
trigger,
|
|
1132
|
+
schedulerM("scheduler_lockAcquireFailed", { params: { taskId: task.id } }),
|
|
1133
|
+
getHooks()
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
if (runningTasks.has(task.id)) {
|
|
1138
|
+
return interruptTask(
|
|
1139
|
+
task,
|
|
1140
|
+
trigger,
|
|
1141
|
+
schedulerM("scheduler_taskRunning", { params: { taskId: task.id } }),
|
|
1142
|
+
getHooks()
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
runningTasks.add(task.id);
|
|
1146
|
+
try {
|
|
1147
|
+
const maxAttempts = Math.max(1, task.retry?.maxAttempts ?? 1);
|
|
1148
|
+
let log = null;
|
|
1149
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1150
|
+
log = await executeTask(task, trigger, getHooks());
|
|
1151
|
+
if (log.status !== "failed")
|
|
1152
|
+
break;
|
|
1153
|
+
if (attempt >= maxAttempts)
|
|
1154
|
+
break;
|
|
1155
|
+
const backoffMs = resolveRetryBackoffMs(task, attempt + 1);
|
|
1156
|
+
logger5.warn("Retrying failed task execution", {
|
|
1157
|
+
taskId: task.id,
|
|
1158
|
+
attempt,
|
|
1159
|
+
maxAttempts,
|
|
1160
|
+
backoffMs
|
|
1161
|
+
});
|
|
1162
|
+
await sleep(backoffMs);
|
|
1163
|
+
}
|
|
1164
|
+
if (!log) {
|
|
1165
|
+
return interruptTask(
|
|
1166
|
+
task,
|
|
1167
|
+
trigger,
|
|
1168
|
+
schedulerM("scheduler_executionFailed", { params: { error: "Task execution produced no log" } }),
|
|
1169
|
+
getHooks()
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
if (log.status === "failed")
|
|
1173
|
+
logger5.warn("Task execution failed", { taskId: task.id, error: log.error });
|
|
1174
|
+
else if (log.status === "interrupted")
|
|
1175
|
+
logger5.debug("Task execution interrupted", { taskId: task.id, error: log.error });
|
|
1176
|
+
else
|
|
1177
|
+
logger5.debug("Task execution succeeded", { taskId: task.id, duration: log.duration });
|
|
1178
|
+
if (task.deleteAfterRun === true) {
|
|
1179
|
+
const unregisterResult = await unregisterTask(task.id, currentTaskRepo);
|
|
1180
|
+
if (!unregisterResult.success) {
|
|
1181
|
+
logger5.warn("Failed to auto-delete one-time task after run", {
|
|
1182
|
+
taskId: task.id,
|
|
1183
|
+
error: unregisterResult.error.message
|
|
1184
|
+
});
|
|
1185
|
+
} else {
|
|
1186
|
+
logger5.info("Auto-deleted one-time task after run", { taskId: task.id });
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return log;
|
|
1190
|
+
} finally {
|
|
1191
|
+
runningTasks.delete(task.id);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
function startTimer(tickInterval) {
|
|
1195
|
+
lastTickMinute = -1;
|
|
1196
|
+
tickTimer = setInterval(() => tick(), tickInterval);
|
|
1197
|
+
}
|
|
1198
|
+
function stopTimer() {
|
|
1199
|
+
if (!tickTimer)
|
|
1200
|
+
return;
|
|
1201
|
+
clearInterval(tickTimer);
|
|
1202
|
+
tickTimer = null;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// src/scheduler-main.ts
|
|
1206
|
+
var logger6 = core.logger.child({ module: "scheduler", scope: "main" });
|
|
1207
|
+
var currentConfig = null;
|
|
1208
|
+
var initInProgress = false;
|
|
1209
|
+
var taskRepo = null;
|
|
1210
|
+
var logRepo = null;
|
|
1211
|
+
var notInitialized = core.module.createNotInitializedKit(
|
|
1212
|
+
HaiSchedulerError.NOT_INITIALIZED,
|
|
1213
|
+
() => schedulerM("scheduler_notInitialized")
|
|
1214
|
+
);
|
|
1215
|
+
async function initDatabase(parsed) {
|
|
1216
|
+
if (!parsed.enableDb)
|
|
1217
|
+
return parsed;
|
|
1218
|
+
if (!reldb.isInitialized) {
|
|
1219
|
+
logger6.error("enableDb is true but @h-ai/reldb is not initialized");
|
|
1220
|
+
throw new Error(schedulerM("scheduler_reldbNotInitialized"));
|
|
1221
|
+
}
|
|
1222
|
+
taskRepo = new SchedulerTaskRepository(reldb);
|
|
1223
|
+
logRepo = new SchedulerLogRepository(reldb);
|
|
1224
|
+
setTaskRepository(taskRepo);
|
|
1225
|
+
setLogRepository(logRepo, {
|
|
1226
|
+
maxLogs: parsed.maxLogs,
|
|
1227
|
+
retentionDays: parsed.retentionDays
|
|
1228
|
+
});
|
|
1229
|
+
const nodeId = parsed.nodeId ?? crypto.randomUUID();
|
|
1230
|
+
const lockTtlSec = Math.ceil(parsed.lockExpireMs / 1e3);
|
|
1231
|
+
configureLock(nodeId, lockTtlSec);
|
|
1232
|
+
if (cache.isInitialized)
|
|
1233
|
+
logger6.info("Distributed lock configured (cache-based)", { nodeId, lockTtlSec });
|
|
1234
|
+
else
|
|
1235
|
+
logger6.warn("Cache module not initialized, distributed lock will be enabled when cache becomes available");
|
|
1236
|
+
return { ...parsed, nodeId };
|
|
1237
|
+
}
|
|
1238
|
+
function normalizeTriggerInput(options) {
|
|
1239
|
+
return {
|
|
1240
|
+
type: "manual",
|
|
1241
|
+
source: options?.source ?? null
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
var scheduler = {
|
|
1245
|
+
async init(config) {
|
|
1246
|
+
if (initInProgress) {
|
|
1247
|
+
logger6.warn("Scheduler init already in progress, skipping concurrent call");
|
|
1248
|
+
return err(
|
|
1249
|
+
HaiSchedulerError.INIT_FAILED,
|
|
1250
|
+
schedulerM("scheduler_initFailed", { params: { error: "Concurrent initialization detected" } })
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
initInProgress = true;
|
|
1254
|
+
try {
|
|
1255
|
+
if (currentConfig) {
|
|
1256
|
+
logger6.warn("Scheduler module is already initialized, reinitializing");
|
|
1257
|
+
await scheduler.close();
|
|
1258
|
+
}
|
|
1259
|
+
logger6.info("Initializing scheduler module");
|
|
1260
|
+
const { tasks = [], hooks = {}, ...configOptions } = config ?? {};
|
|
1261
|
+
const parseResult = SchedulerConfigSchema.safeParse(configOptions);
|
|
1262
|
+
if (!parseResult.success) {
|
|
1263
|
+
logger6.error("Scheduler config validation failed", { error: parseResult.error.message });
|
|
1264
|
+
return err(
|
|
1265
|
+
HaiSchedulerError.CONFIG_ERROR,
|
|
1266
|
+
schedulerM("scheduler_configError", { params: { error: parseResult.error.message } }),
|
|
1267
|
+
parseResult.error
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
try {
|
|
1271
|
+
currentConfig = await initDatabase(parseResult.data);
|
|
1272
|
+
setHooks(hooks);
|
|
1273
|
+
await loadPersistedTasks(taskRepo);
|
|
1274
|
+
if (tasks.length > 0)
|
|
1275
|
+
await loadConfigTasks(tasks);
|
|
1276
|
+
logger6.info("Scheduler module initialized", { enableDb: currentConfig.enableDb, taskCount: getTaskRegistry().size });
|
|
1277
|
+
return ok(void 0);
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
logger6.error("Scheduler initialization failed", { error });
|
|
1280
|
+
await scheduler.close();
|
|
1281
|
+
return err(
|
|
1282
|
+
HaiSchedulerError.INIT_FAILED,
|
|
1283
|
+
schedulerM("scheduler_initFailed", {
|
|
1284
|
+
params: { error: error instanceof Error ? error.message : String(error) }
|
|
1285
|
+
}),
|
|
1286
|
+
error
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
} finally {
|
|
1290
|
+
initInProgress = false;
|
|
1291
|
+
}
|
|
1292
|
+
},
|
|
1293
|
+
async register(task) {
|
|
1294
|
+
if (!currentConfig)
|
|
1295
|
+
return notInitialized.result();
|
|
1296
|
+
const registerResult = await registerTask(task, taskRepo);
|
|
1297
|
+
if (registerResult.success) {
|
|
1298
|
+
logger6.info("Task registered", { taskId: task.id, taskName: task.name, cron: task.cron });
|
|
1299
|
+
}
|
|
1300
|
+
return registerResult;
|
|
1301
|
+
},
|
|
1302
|
+
async unregister(taskId) {
|
|
1303
|
+
if (!currentConfig)
|
|
1304
|
+
return notInitialized.result();
|
|
1305
|
+
const unregisterResult = await unregisterTask(taskId, taskRepo);
|
|
1306
|
+
if (unregisterResult.success)
|
|
1307
|
+
logger6.info("Task unregistered", { taskId });
|
|
1308
|
+
return unregisterResult;
|
|
1309
|
+
},
|
|
1310
|
+
async updateTask(taskId, updates) {
|
|
1311
|
+
if (!currentConfig)
|
|
1312
|
+
return notInitialized.result();
|
|
1313
|
+
const updateResult = await updateRegisteredTask(taskId, updates, taskRepo);
|
|
1314
|
+
if (updateResult.success)
|
|
1315
|
+
logger6.info("Task updated", { taskId, updates: Object.keys(updates) });
|
|
1316
|
+
return updateResult;
|
|
1317
|
+
},
|
|
1318
|
+
start() {
|
|
1319
|
+
if (!currentConfig)
|
|
1320
|
+
return notInitialized.result();
|
|
1321
|
+
if (isTimerRunning()) {
|
|
1322
|
+
return err(
|
|
1323
|
+
HaiSchedulerError.ALREADY_RUNNING,
|
|
1324
|
+
schedulerM("scheduler_alreadyRunning")
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
startTimer(currentConfig.tickInterval);
|
|
1328
|
+
logger6.info("Scheduler started", { tickInterval: currentConfig.tickInterval });
|
|
1329
|
+
return ok(void 0);
|
|
1330
|
+
},
|
|
1331
|
+
stop() {
|
|
1332
|
+
if (!currentConfig)
|
|
1333
|
+
return notInitialized.result();
|
|
1334
|
+
if (!isTimerRunning()) {
|
|
1335
|
+
return err(
|
|
1336
|
+
HaiSchedulerError.NOT_RUNNING,
|
|
1337
|
+
schedulerM("scheduler_notRunning")
|
|
1338
|
+
);
|
|
1339
|
+
}
|
|
1340
|
+
stopTimer();
|
|
1341
|
+
logger6.info("Scheduler stopped");
|
|
1342
|
+
return ok(void 0);
|
|
1343
|
+
},
|
|
1344
|
+
async trigger(taskId, options) {
|
|
1345
|
+
if (!currentConfig)
|
|
1346
|
+
return notInitialized.result();
|
|
1347
|
+
const task = getTask(taskId);
|
|
1348
|
+
if (!task) {
|
|
1349
|
+
return err(
|
|
1350
|
+
HaiSchedulerError.TASK_NOT_FOUND,
|
|
1351
|
+
schedulerM("scheduler_taskNotFound", { params: { taskId } })
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
if (isTaskRunning(taskId)) {
|
|
1355
|
+
return err(
|
|
1356
|
+
HaiSchedulerError.EXECUTION_FAILED,
|
|
1357
|
+
schedulerM("scheduler_taskRunning", { params: { taskId } })
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
const log = await runTask(task, void 0, normalizeTriggerInput(options));
|
|
1361
|
+
return ok(log);
|
|
1362
|
+
},
|
|
1363
|
+
async getLogs(options) {
|
|
1364
|
+
if (!currentConfig)
|
|
1365
|
+
return notInitialized.result();
|
|
1366
|
+
return queryTaskLogs(logRepo, options);
|
|
1367
|
+
},
|
|
1368
|
+
setHooks(hooks) {
|
|
1369
|
+
if (!currentConfig)
|
|
1370
|
+
return notInitialized.result();
|
|
1371
|
+
setHooks(hooks);
|
|
1372
|
+
return ok(void 0);
|
|
1373
|
+
},
|
|
1374
|
+
clearHooks() {
|
|
1375
|
+
if (!currentConfig)
|
|
1376
|
+
return notInitialized.result();
|
|
1377
|
+
clearHooks();
|
|
1378
|
+
return ok(void 0);
|
|
1379
|
+
},
|
|
1380
|
+
get tasks() {
|
|
1381
|
+
return getTaskRegistry();
|
|
1382
|
+
},
|
|
1383
|
+
get config() {
|
|
1384
|
+
return currentConfig;
|
|
1385
|
+
},
|
|
1386
|
+
get hooks() {
|
|
1387
|
+
return getHooks();
|
|
1388
|
+
},
|
|
1389
|
+
get isInitialized() {
|
|
1390
|
+
return currentConfig !== null;
|
|
1391
|
+
},
|
|
1392
|
+
get isRunning() {
|
|
1393
|
+
return isTimerRunning();
|
|
1394
|
+
},
|
|
1395
|
+
async close() {
|
|
1396
|
+
if (!currentConfig) {
|
|
1397
|
+
logger6.info("Scheduler module already closed, skipping");
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
logger6.info("Closing scheduler module");
|
|
1401
|
+
stopTimer();
|
|
1402
|
+
clearJsTaskHandlerCache();
|
|
1403
|
+
resetTaskState();
|
|
1404
|
+
resetRunner();
|
|
1405
|
+
setLogRepository(null);
|
|
1406
|
+
setTaskRepository(null);
|
|
1407
|
+
currentConfig = null;
|
|
1408
|
+
taskRepo = null;
|
|
1409
|
+
logRepo = null;
|
|
1410
|
+
logger6.info("Scheduler module closed");
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
export { HaiSchedulerError, SchedulerConfigSchema, scheduler };
|
|
1415
|
+
//# sourceMappingURL=index.js.map
|
|
1416
|
+
//# sourceMappingURL=index.js.map
|