mirror of
https://github.com/whyour/qinglong.git
synced 2025-05-23 14:56:07 +08:00
增加任务重复运行提醒
This commit is contained in:
parent
f4cb3eacf8
commit
8b8eae211b
|
@ -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);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
: []),
|
: []),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue
Block a user