mirror of
https://github.com/whyour/qinglong.git
synced 2026-07-17 09:34:31 +08:00
修复删除定时任务后 crontab.list 残留导致订阅更新无法重新注册 (#3021)
* 修复删除定时任务后 crontab.list 未同步导致订阅更新无法重新注册脚本 crontab.list 作为数据库的文件镜像,其同步 setCrontab() 此前串行在尽力而为 的 gRPC addCron/delCron 调用之后:当调度 worker 重启等导致 gRPC 短暂不可用、 addCron/delCron reject 时,setCrontab() 会被跳过,crontab.list 与数据库脱节。 而 shell/update.sh 的 gen_list_repo() 将 crontab.list 作为"已有任务"真源, 于是订阅更新误判脚本已存在、跳过新增注册(前端再也看不到这些脚本)。 将 create/update/remove/disabled/enabled/autosave_crontab 中的 gRPC 调用改为 尽力而为(try/catch + 告警日志),保证 setCrontab() 总是执行;autosave_crontab 额外将 setCrontab 提前到 addCron 之前,确保启动/调度器重启时文件总是同步, 并避免 gRPC 失败导致应用启动崩溃。 调度器内存中的任务注册与 crontab.list/系统 crontab 同步是两个相互独立的关注点: gRPC 失败时内存调度可能在调度器重启后由 autosave_crontab 重新注册,但文件同步 不应因此被阻断。修复后既有的 crontab.list 残留会在下次重启或任意增删改时自愈。 可能与 #2422(订阅更新重复添加任务)同根因。 * 过滤 delCron 中 scheduleStacks 的 null job nodeSchedule.scheduleJob() 对无效 cron 表达式(如 "0 * /6 * * *", 空格分开了 * /6)会返回 null,原代码原样存入 scheduleStacks 数组。 delCron 取消时对 null 调 cancel() 抛出 UNKNOWN 错误,导致 gRPC handler 中断、HTTP 端 cronService.remove() 跳过 setCrontab(), crontab.list 残留已删任务的记录,导致订阅更新时 gen_list_repo() 误判脚本已存在、不再重新注册。 过滤 null 并对每个 cancel 调用加 try/catch 容错。
This commit is contained in:
@@ -13,7 +13,20 @@ const delCron = (
|
|||||||
'[schedule][取消定时任务] 任务ID: %s',
|
'[schedule][取消定时任务] 任务ID: %s',
|
||||||
id,
|
id,
|
||||||
);
|
);
|
||||||
scheduleStacks.get(id)?.forEach(x => x.cancel());
|
// 过滤掉 nodeSchedule.scheduleJob() 对无效表达式返回的 null,
|
||||||
|
// 否则对 null 调 cancel() 会让整个取消流程抛出 UNKNOWN 错误,
|
||||||
|
// 进而导致 HTTP 端的 remove() 跳过 setCrontab(),造成 crontab.list 残留。
|
||||||
|
scheduleStacks.get(id)?.filter((x) => x != null).forEach((x) => {
|
||||||
|
try {
|
||||||
|
x.cancel();
|
||||||
|
} catch (error: any) {
|
||||||
|
Logger.warn(
|
||||||
|
'[schedule][取消任务失败] 任务ID: %s, 错误: %s',
|
||||||
|
id,
|
||||||
|
error?.message || error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
scheduleStacks.delete(id);
|
scheduleStacks.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-24
@@ -106,15 +106,24 @@ export default class CronService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.shouldUseCronClient(doc)) {
|
if (this.shouldUseCronClient(doc)) {
|
||||||
await cronClient.addCron([
|
try {
|
||||||
{
|
await cronClient.addCron([
|
||||||
name: doc.name || '',
|
{
|
||||||
id: String(doc.id),
|
name: doc.name || '',
|
||||||
schedule: doc.schedule!,
|
id: String(doc.id),
|
||||||
command: this.makeCommand(doc),
|
schedule: doc.schedule!,
|
||||||
extra_schedules: doc.extra_schedules || [],
|
command: this.makeCommand(doc),
|
||||||
},
|
extra_schedules: doc.extra_schedules || [],
|
||||||
]);
|
},
|
||||||
|
]);
|
||||||
|
} catch (error: any) {
|
||||||
|
// 调度器注册为尽力而为,失败时不阻断 crontab.list 与系统 crontab 的同步,
|
||||||
|
// 调度器重启后会重新注册(见 autosave_crontab)
|
||||||
|
this.logger.warn(
|
||||||
|
'[crontab] Failed to register cron job in scheduler:',
|
||||||
|
error?.message || error,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.setCrontab();
|
await this.setCrontab();
|
||||||
@@ -136,18 +145,32 @@ export default class CronService {
|
|||||||
return newDoc;
|
return newDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
await cronClient.delCron([String(newDoc.id)]);
|
try {
|
||||||
|
await cronClient.delCron([String(newDoc.id)]);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(
|
||||||
|
'[crontab] Failed to unregister cron job in scheduler:',
|
||||||
|
error?.message || error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.shouldUseCronClient(newDoc)) {
|
if (this.shouldUseCronClient(newDoc)) {
|
||||||
await cronClient.addCron([
|
try {
|
||||||
{
|
await cronClient.addCron([
|
||||||
name: doc.name || '',
|
{
|
||||||
id: String(newDoc.id),
|
name: doc.name || '',
|
||||||
schedule: newDoc.schedule!,
|
id: String(newDoc.id),
|
||||||
command: this.makeCommand(newDoc),
|
schedule: newDoc.schedule!,
|
||||||
extra_schedules: newDoc.extra_schedules || [],
|
command: this.makeCommand(newDoc),
|
||||||
},
|
extra_schedules: newDoc.extra_schedules || [],
|
||||||
]);
|
},
|
||||||
|
]);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(
|
||||||
|
'[crontab] Failed to register cron job in scheduler:',
|
||||||
|
error?.message || error,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.setCrontab();
|
await this.setCrontab();
|
||||||
@@ -240,7 +263,14 @@ export default class CronService {
|
|||||||
|
|
||||||
public async remove(ids: number[]) {
|
public async remove(ids: number[]) {
|
||||||
await CrontabModel.destroy({ where: { id: ids } });
|
await CrontabModel.destroy({ where: { id: ids } });
|
||||||
await cronClient.delCron(ids.map(String));
|
try {
|
||||||
|
await cronClient.delCron(ids.map(String));
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(
|
||||||
|
'[crontab] Failed to unregister cron job in scheduler:',
|
||||||
|
error?.message || error,
|
||||||
|
);
|
||||||
|
}
|
||||||
await this.setCrontab();
|
await this.setCrontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,7 +695,14 @@ export default class CronService {
|
|||||||
|
|
||||||
public async disabled(ids: number[]) {
|
public async disabled(ids: number[]) {
|
||||||
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
|
||||||
await cronClient.delCron(ids.map(String));
|
try {
|
||||||
|
await cronClient.delCron(ids.map(String));
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(
|
||||||
|
'[crontab] Failed to unregister cron job in scheduler:',
|
||||||
|
error?.message || error,
|
||||||
|
);
|
||||||
|
}
|
||||||
await this.setCrontab();
|
await this.setCrontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,7 +723,14 @@ export default class CronService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await cronClient.addCron(crons);
|
try {
|
||||||
|
await cronClient.addCron(crons);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(
|
||||||
|
'[crontab] Failed to register cron job in scheduler:',
|
||||||
|
error?.message || error,
|
||||||
|
);
|
||||||
|
}
|
||||||
await this.setCrontab();
|
await this.setCrontab();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -864,8 +908,19 @@ export default class CronService {
|
|||||||
await writeFileWithLock(config.crontabFile, '');
|
await writeFileWithLock(config.crontabFile, '');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await cronClient.addCron(regularCrons);
|
|
||||||
this.setCrontab(tabs);
|
// 先同步 crontab.list 与系统 crontab,确保其始终反映数据库真实状态。
|
||||||
|
// gRPC 调度注册为尽力而为:失败时不阻断文件同步,调度器重启后会重新注册。
|
||||||
|
// 这避免了因调度器短暂不可用导致 crontab.list 与数据库脱节(订阅更新误判任务已存在)。
|
||||||
|
await this.setCrontab(tabs);
|
||||||
|
try {
|
||||||
|
await cronClient.addCron(regularCrons);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(
|
||||||
|
'[crontab] Failed to register cron job in scheduler:',
|
||||||
|
error?.message || error,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async bootTask() {
|
public async bootTask() {
|
||||||
|
|||||||
Reference in New Issue
Block a user