From fc355bc86fe66b4a45eb996ec3f9acfff22d85e0 Mon Sep 17 00:00:00 2001 From: Martha Ramirez <59023744+YNight-FZQ@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:34:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=A0=E9=99=A4=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E4=BB=BB=E5=8A=A1=E5=90=8E=20crontab.list=20=E6=9C=AA?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E5=AF=BC=E8=87=B4=E8=AE=A2=E9=98=85=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=97=A0=E6=B3=95=E9=87=8D=E6=96=B0=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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(订阅更新重复添加任务)同根因。 --- back/services/cron.ts | 103 ++++++++++++++++++++++++++++++++---------- 1 file changed, 79 insertions(+), 24 deletions(-) diff --git a/back/services/cron.ts b/back/services/cron.ts index 16e24693..518fa364 100644 --- a/back/services/cron.ts +++ b/back/services/cron.ts @@ -106,15 +106,24 @@ export default class CronService { } if (this.shouldUseCronClient(doc)) { - await cronClient.addCron([ - { - name: doc.name || '', - id: String(doc.id), - schedule: doc.schedule!, - command: this.makeCommand(doc), - extra_schedules: doc.extra_schedules || [], - }, - ]); + try { + await cronClient.addCron([ + { + name: doc.name || '', + id: String(doc.id), + schedule: doc.schedule!, + 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(); @@ -136,18 +145,32 @@ export default class CronService { 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)) { - await cronClient.addCron([ - { - name: doc.name || '', - id: String(newDoc.id), - schedule: newDoc.schedule!, - command: this.makeCommand(newDoc), - extra_schedules: newDoc.extra_schedules || [], - }, - ]); + try { + await cronClient.addCron([ + { + name: doc.name || '', + id: String(newDoc.id), + schedule: newDoc.schedule!, + 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(); @@ -233,7 +256,14 @@ export default class CronService { public async remove(ids: number[]) { 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(); } @@ -687,7 +717,14 @@ export default class CronService { public async disabled(ids: number[]) { 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(); } @@ -708,7 +745,14 @@ export default class CronService { 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(); } @@ -886,8 +930,19 @@ export default class CronService { await writeFileWithLock(config.crontabFile, ''); 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() {