增加任务重复运行提醒

This commit is contained in:
whyour 2024-08-23 09:37:26 +08:00
parent f4cb3eacf8
commit 8b8eae211b
7 changed files with 88 additions and 17 deletions

View File

@ -24,7 +24,7 @@ const addCron = (
); );
if (extraSchedules?.length) { if (extraSchedules?.length) {
extraSchedules.forEach(x => { extraSchedules.forEach((x) => {
Logger.info( Logger.info(
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s', '[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
id, id,
@ -32,21 +32,21 @@ const addCron = (
x.schedule, x.schedule,
command, command,
); );
}) });
} }
scheduleStacks.set(id, [ scheduleStacks.set(id, [
nodeSchedule.scheduleJob(id, schedule, async () => { nodeSchedule.scheduleJob(id, schedule, async () => {
Logger.info(`[schedule][准备运行任务] 命令: ${command}`); Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
runCron(command, { name, schedule, extraSchedules }); runCron(command, item);
}), }),
...(extraSchedules?.length ...(extraSchedules?.length
? extraSchedules.map((x) => ? extraSchedules.map((x) =>
nodeSchedule.scheduleJob(id, x.schedule, async () => { nodeSchedule.scheduleJob(id, x.schedule, async () => {
Logger.info(`[schedule][准备运行任务] 命令: ${command}`); Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
runCron(command, { name, schedule, extraSchedules }); runCron(command, item);
}), }),
) )
: []), : []),
]); ]);
} }

View File

@ -42,7 +42,7 @@ export default class ScheduleService {
private maxBuffer = 200 * 1024 * 1024; private maxBuffer = 200 * 1024 * 1024;
constructor(@Inject('logger') private logger: winston.Logger) { } constructor(@Inject('logger') private logger: winston.Logger) {}
async runTask( async runTask(
command: string, command: string,
@ -51,12 +51,19 @@ export default class ScheduleService {
schedule?: string; schedule?: string;
name?: string; name?: string;
command?: string; command?: string;
id: string;
}, },
completionTime: 'start' | 'end' = 'end', completionTime: 'start' | 'end' = 'end',
) { ) {
return taskLimit.runWithCronLimit(() => { return taskLimit.runWithCronLimit(params, () => {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify({ ...params, command })}`); taskLimit.removeQueuedCron(params.id);
this.logger.info(
`[panel][开始执行任务] 参数 ${JSON.stringify({
...params,
command,
})}`,
);
try { try {
const startTime = dayjs(); const startTime = dayjs();
@ -131,6 +138,7 @@ export default class ScheduleService {
name, name,
schedule, schedule,
command, command,
id: _id,
}); });
}), }),
); );
@ -140,6 +148,7 @@ export default class ScheduleService {
name, name,
schedule, schedule,
command, command,
id: _id,
}); });
} }
} }
@ -148,6 +157,7 @@ export default class ScheduleService {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info('[panel][取消定时任务], 任务名: %s', name); this.logger.info('[panel][取消定时任务], 任务名: %s', name);
if (this.scheduleStacks.has(_id)) { if (this.scheduleStacks.has(_id)) {
taskLimit.removeQueuedCron(_id);
this.scheduleStacks.get(_id)?.cancel(); this.scheduleStacks.get(_id)?.cancel();
this.scheduleStacks.delete(_id); this.scheduleStacks.delete(_id);
} }
@ -172,6 +182,7 @@ export default class ScheduleService {
this.runTask(command, callbacks, { this.runTask(command, callbacks, {
name, name,
command, command,
id: _id,
}); });
}, },
(err) => { (err) => {
@ -195,6 +206,7 @@ export default class ScheduleService {
this.runTask(command, callbacks, { this.runTask(command, callbacks, {
name, name,
command, command,
id: _id,
}); });
} }
} }
@ -202,6 +214,7 @@ export default class ScheduleService {
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) { async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info('[取消interval任务], 任务ID: %s, 任务名: %s', _id, name); this.logger.info('[取消interval任务], 任务ID: %s, 任务名: %s', _id, name);
taskLimit.removeQueuedCron(_id);
this.intervalSchedule.removeById(_id); this.intervalSchedule.removeById(_id);
} }

View File

@ -7,6 +7,7 @@ import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config'; import config from '../config';
import { TASK_COMMAND } from '../config/const'; import { TASK_COMMAND } from '../config/const';
import { getFileContentByName, getPid, killTask, rmPath } from '../config/util'; import { getFileContentByName, getPid, killTask, rmPath } from '../config/util';
import taskLimit from '../shared/pLimit';
@Service() @Service()
export default class ScriptService { export default class ScriptService {
@ -43,7 +44,7 @@ export default class ScriptService {
const pid = await this.scheduleService.runTask( const pid = await this.scheduleService.runTask(
`real_time=true ${command}`, `real_time=true ${command}`,
this.taskCallbacks(filePath), this.taskCallbacks(filePath),
{ command }, { command, id: relativePath.replace(/ /g, '-') },
'start', 'start',
); );
@ -53,6 +54,7 @@ export default class ScriptService {
public async stopScript(filePath: string, pid: number) { public async stopScript(filePath: string, pid: number) {
if (!pid) { if (!pid) {
const relativePath = path.relative(config.scriptPath, filePath); const relativePath = path.relative(config.scriptPath, filePath);
taskLimit.removeQueuedCron(relativePath.replace(/ /g, '-'));
pid = (await getPid(`${TASK_COMMAND} ${relativePath} now`)) as number; pid = (await getPid(`${TASK_COMMAND} ${relativePath} now`)) as number;
} }
try { try {

View File

@ -29,6 +29,7 @@ import { LOG_END_SYMBOL } from '../config/const';
import { formatCommand, formatUrl } from '../config/subscription'; import { formatCommand, formatUrl } from '../config/subscription';
import { CrontabModel } from '../data/cron'; import { CrontabModel } from '../data/cron';
import CrontabService from './cron'; import CrontabService from './cron';
import taskLimit from '../shared/pLimit';
@Service() @Service()
export default class SubscriptionService { export default class SubscriptionService {
@ -301,6 +302,7 @@ export default class SubscriptionService {
for (const doc of docs) { for (const doc of docs) {
if (doc.pid) { if (doc.pid) {
try { try {
taskLimit.removeQueuedCron(String(doc.id));
await killTask(doc.pid); await killTask(doc.pid);
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);
@ -326,6 +328,7 @@ export default class SubscriptionService {
name: subscription.name, name: subscription.name,
schedule: subscription.schedule, schedule: subscription.schedule,
command, command,
id: String(subscription.id),
}); });
} }

View File

@ -178,6 +178,7 @@ export default class SystemService {
}, },
{ {
command, command,
id: 'update-node-mirror',
}, },
); );
} }
@ -252,6 +253,7 @@ export default class SystemService {
}, },
{ {
command, command,
id: 'update-linux-mirror',
}, },
); );
} }
@ -363,6 +365,7 @@ export default class SystemService {
} }
this.scheduleService.runTask(`real_time=true ${command}`, callback, { this.scheduleService.runTask(`real_time=true ${command}`, callback, {
command, command,
id: command.replace(/ /g, '-'),
}); });
} }
@ -371,6 +374,7 @@ export default class SystemService {
return { code: 400, message: '参数错误' }; return { code: 400, message: '参数错误' };
} }
taskLimit.removeQueuedCron(command.replace(/ /g, '-'));
if (pid) { if (pid) {
await killTask(pid); await killTask(pid);
return { code: 200 }; return { code: 200 };

View File

@ -3,14 +3,29 @@ import os from 'os';
import { AuthDataType, SystemModel } from '../data/system'; import { AuthDataType, SystemModel } from '../data/system';
import Logger from '../loaders/logger'; import Logger from '../loaders/logger';
import { Dependence } from '../data/dependence'; import { Dependence } from '../data/dependence';
import { ICron } from '../protos/cron';
import NotificationService from '../services/notify';
import { Inject } from 'typedi';
export type Override<
T,
K extends Partial<{ [P in keyof T]: any }> | string,
> = K extends string
? Omit<T, K> & { [P in keyof T]: T[P] | unknown }
: Omit<T, keyof K> & K;
type TCron = Override<Partial<ICron>, { id: string }>;
interface IDependencyFn<T> { interface IDependencyFn<T> {
(): Promise<T>; (): Promise<T>;
dependency?: Dependence; dependency?: Dependence;
} }
interface ICronFn<T> {
(): Promise<T>;
cron?: TCron;
}
class TaskLimit { class TaskLimit {
private dependenyLimit = new PQueue({ concurrency: 1 }); private dependenyLimit = new PQueue({ concurrency: 1 });
private queuedDependencyIds = new Set<number>([]); private queuedDependencyIds = new Set<number>([]);
private queuedCrons = new Map<string, TCron[]>();
private updateLogLimit = new PQueue({ concurrency: 1 }); private updateLogLimit = new PQueue({ concurrency: 1 });
private cronLimit = new PQueue({ private cronLimit = new PQueue({
concurrency: Math.max(os.cpus().length, 4), concurrency: Math.max(os.cpus().length, 4),
@ -18,6 +33,8 @@ class TaskLimit {
private manualCronoLimit = new PQueue({ private manualCronoLimit = new PQueue({
concurrency: Math.max(os.cpus().length, 4), concurrency: Math.max(os.cpus().length, 4),
}); });
@Inject((type) => NotificationService)
private notificationService!: NotificationService;
get cronLimitActiveCount() { get cronLimitActiveCount() {
return this.cronLimit.pending; return this.cronLimit.pending;
@ -71,6 +88,16 @@ class TaskLimit {
} }
} }
public removeQueuedCron(id: string) {
if (this.queuedCrons.has(id)) {
const runs = this.queuedCrons.get(id);
if (runs && runs.length > 0) {
runs.pop();
this.queuedCrons.set(id, runs);
}
}
}
public async setCustomLimit(limit?: number) { public async setCustomLimit(limit?: number) {
if (limit) { if (limit) {
this.cronLimit.concurrency = limit; this.cronLimit.concurrency = limit;
@ -88,9 +115,24 @@ class TaskLimit {
} }
public async runWithCronLimit<T>( public async runWithCronLimit<T>(
fn: () => Promise<T>, cron: TCron,
fn: ICronFn<T>,
options?: Partial<QueueAddOptions>, options?: Partial<QueueAddOptions>,
): Promise<T | void> { ): Promise<T | void> {
let runs = this.queuedCrons.get(cron.id);
if (!runs?.length) {
runs = [];
}
runs.push(cron);
if (runs.length >= 5) {
this.notificationService.notify(
'任务重复运行',
`任务 ${cron.name} ${cron.command} 处于运行中的已达 5 个,请检查系统日志`,
);
return;
}
this.queuedCrons.set(cron.id, runs);
fn.cron = cron;
return this.cronLimit.add(fn, options); return this.cronLimit.add(fn, options);
} }

View File

@ -1,11 +1,18 @@
import { spawn } from 'cross-spawn'; import { spawn } from 'cross-spawn';
import taskLimit from './pLimit'; import taskLimit from './pLimit';
import Logger from '../loaders/logger'; import Logger from '../loaders/logger';
import { ICron } from '../protos/cron';
export function runCron(cmd: string, options?: { schedule: string; extraSchedules: Array<{ schedule: string }>; name: string }): Promise<number | void> { export function runCron(cmd: string, cron: ICron): Promise<number | void> {
return taskLimit.runWithCronLimit(() => { return taskLimit.runWithCronLimit(cron, () => {
return new Promise(async (resolve: any) => { return new Promise(async (resolve: any) => {
Logger.info(`[schedule][开始执行任务] 参数 ${JSON.stringify({ ...options, command: cmd })}`); taskLimit.removeQueuedCron(cron.id);
Logger.info(
`[schedule][开始执行任务] 参数 ${JSON.stringify({
...cron,
command: cmd,
})}`,
);
const cp = spawn(cmd, { shell: '/bin/bash' }); const cp = spawn(cmd, { shell: '/bin/bash' });
cp.stderr.on('data', (data) => { cp.stderr.on('data', (data) => {
@ -24,7 +31,7 @@ export function runCron(cmd: string, options?: { schedule: string; extraSchedule
}); });
cp.on('exit', async (code) => { cp.on('exit', async (code) => {
resolve({ ...options, command: cmd, pid: cp.pid, code }); resolve({ ...cron, command: cmd, pid: cp.pid, code });
}); });
}); });
}); });