批量运行任务添加并行数限制

This commit is contained in:
whyour 2021-05-16 17:24:53 +08:00
parent fefdcb88fd
commit c183201e6f
9 changed files with 146 additions and 94 deletions

View File

@ -20,11 +20,16 @@ const dbPath = path.join(rootPath, 'db/');
const manualLogPath = path.join(rootPath, 'manual_log/'); const manualLogPath = path.join(rootPath, 'manual_log/');
const cronDbFile = path.join(rootPath, 'db/crontab.db'); const cronDbFile = path.join(rootPath, 'db/crontab.db');
const cookieDbFile = path.join(rootPath, 'db/cookie.db'); const cookieDbFile = path.join(rootPath, 'db/cookie.db');
const configFound = dotenv.config({ path: confFile });
if (envFound.error) { if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️"); throw new Error("⚠️ Couldn't find .env file ⚠️");
} }
if (configFound.error) {
throw new Error("⚠️ Couldn't find config.sh file ⚠️");
}
export default { export default {
port: parseInt(process.env.PORT as string, 10), port: parseInt(process.env.PORT as string, 10),
cronPort: parseInt(process.env.CRON_PORT as string, 10), cronPort: parseInt(process.env.CRON_PORT as string, 10),

View File

@ -9,6 +9,7 @@ export class Crontab {
status?: CrontabStatus; status?: CrontabStatus;
isSystem?: 1 | 0; isSystem?: 1 | 0;
pid?: number; pid?: number;
isDisabled?: 1 | 0;
constructor(options: Crontab) { constructor(options: Crontab) {
this.name = options.name; this.name = options.name;
@ -21,6 +22,7 @@ export class Crontab {
this.timestamp = new Date().toString(); this.timestamp = new Date().toString();
this.isSystem = options.isSystem || 0; this.isSystem = options.isSystem || 0;
this.pid = options.pid; this.pid = options.pid;
this.isDisabled = options.isDisabled || 0;
} }
} }
@ -28,4 +30,5 @@ export enum CrontabStatus {
'running', 'running',
'idle', 'idle',
'disabled', 'disabled',
'queued',
} }

View File

@ -74,6 +74,25 @@ export default async () => {
} }
}); });
// patch 禁用状态字段改变
cronDb
.find({
status: CrontabStatus.disabled,
})
.exec((err, docs) => {
if (docs.length > 0) {
const ids = docs.map((x) => x._id);
cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.idle, isDisabled: 1 } },
{ multi: true },
(err) => {
cronService.autosave_crontab();
},
);
}
});
// 初始化保存一次ck和定时任务数据 // 初始化保存一次ck和定时任务数据
await cronService.autosave_crontab(); await cronService.autosave_crontab();
await cookieService.set_cookies(); await cookieService.set_cookies();

View File

@ -29,7 +29,8 @@ const run = async () => {
if ( if (
_schedule && _schedule &&
_schedule.length > 5 && _schedule.length > 5 &&
task.status !== CrontabStatus.disabled task.status !== CrontabStatus.disabled &&
!task.isDisabled
) { ) {
schedule.scheduleJob(task.schedule, function () { schedule.scheduleJob(task.schedule, function () {
let command = task.command as string; let command = task.command as string;

View File

@ -7,11 +7,16 @@ import { exec, execSync, spawn } from 'child_process';
import fs from 'fs'; import fs from 'fs';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { getFileContentByName } from '../config/util'; import { getFileContentByName } from '../config/util';
import PQueue from 'p-queue';
@Service() @Service()
export default class CronService { export default class CronService {
private cronDb = new DataStore({ filename: config.cronDbFile }); private cronDb = new DataStore({ filename: config.cronDbFile });
private queue = new PQueue({
concurrency: parseInt(process.env.MaxConcurrentNum) || 5,
});
constructor(@Inject('logger') private logger: winston.Logger) { constructor(@Inject('logger') private logger: winston.Logger) {
this.cronDb.loadDatabase((err) => { this.cronDb.loadDatabase((err) => {
if (err) throw err; if (err) throw err;
@ -124,97 +129,102 @@ export default class CronService {
} }
public async run(ids: string[]) { public async run(ids: string[]) {
this.cronDb.find({ _id: { $in: ids } }).exec((err, docs: Crontab[]) => { this.cronDb.update(
for (let i = 0; i < docs.length; i++) { { _id: { $in: ids } },
const doc = docs[i]; { $set: { status: CrontabStatus.queued } },
this.runSingle(doc); { multi: true },
} );
}); for (let i = 0; i < ids.length; i++) {
const id = ids[i];
this.queue.add(() => this.runSingle(id));
}
} }
public async stop(ids: string[]) { public async stop(ids: string[]) {
this.cronDb.find({ _id: { $in: ids } }).exec((err, docs: Crontab[]) => { this.cronDb.find({ _id: { $in: ids } }).exec((err, docs: Crontab[]) => {
for (let i = 0; i < docs.length; i++) { this.cronDb.update(
const doc = docs[i]; { _id: { $in: ids } },
if (doc.pid) { { $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
exec(`kill -9 ${doc.pid}`, (err, stdout, stderr) => { );
this.cronDb.update( const pids = docs
{ _id: doc._id }, .map((x) => x.pid)
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } }, .filter((x) => !!x)
); .join('\n');
}); console.log(pids);
} exec(`echo - e "${pids}" | xargs kill - 9`);
}
}); });
} }
private async runSingle(cron: Crontab) { private async runSingle(id: string): Promise<number> {
let { _id, command } = cron; return new Promise(async (resolve) => {
const cron = await this.get(id);
if (cron.status !== CrontabStatus.queued) {
resolve(0);
return;
}
this.logger.silly('Running job'); let { _id, command } = cron;
this.logger.silly('ID: ' + _id);
this.logger.silly('Original command: ' + command);
let logFile = `${config.manualLogPath}${_id}.log`; this.logger.silly('Running job');
fs.writeFileSync(logFile, `开始执行...\n\n${new Date().toString()}\n`); this.logger.silly('ID: ' + _id);
this.logger.silly('Original command: ' + command);
let cmdStr = command; let logFile = `${config.manualLogPath}${_id}.log`;
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) { fs.writeFileSync(logFile, `开始执行...\n${new Date().toString()}\n`);
cmdStr = `task ${cmdStr}`;
}
if (cmdStr.endsWith('.js')) {
cmdStr = `${cmdStr} now`;
}
const cmd = spawn(cmdStr, { shell: true });
this.cronDb.update( let cmdStr = command;
{ _id }, if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
{ $set: { status: CrontabStatus.running, pid: cmd.pid } }, cmdStr = `task ${cmdStr}`;
); }
if (cmdStr.endsWith('.js')) {
cmdStr = `${cmdStr} now`;
}
const cmd = spawn(cmdStr, { shell: true });
cmd.stdout.on('data', (data) => {
this.logger.info(`stdout: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.stderr.on('data', (data) => {
this.logger.info(`stderr: ${data}`);
fs.appendFileSync(logFile, data);
});
cmd.on('close', (code) => {
this.logger.info(`child process exited with code ${code}`);
this.cronDb.update( this.cronDb.update(
{ _id }, { _id },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } }, { $set: { status: CrontabStatus.running, pid: cmd.pid } },
); );
});
cmd.on('error', (err) => { cmd.stdout.on('data', (data) => {
this.logger.info(err); this.logger.info(`stdout: ${data}`);
fs.appendFileSync(logFile, err.stack); fs.appendFileSync(logFile, data);
}); });
cmd.on('exit', (code: number, signal: any) => { cmd.stderr.on('data', (data) => {
this.logger.info(`cmd exit ${code}`); this.logger.info(`stderr: ${data}`);
this.cronDb.update( fs.appendFileSync(logFile, data);
{ _id }, });
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
);
fs.appendFileSync(logFile, `\n\n执行结束...`);
});
cmd.on('disconnect', () => { cmd.on('close', (code) => {
this.logger.info(`cmd disconnect`); this.logger.info(`child process exited with code ${code}`);
this.cronDb.update({ _id }, { $set: { status: CrontabStatus.idle } }); this.cronDb.update(
fs.appendFileSync(logFile, `\n\n连接断开...`); { _id },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
);
});
cmd.on('error', (err) => {
this.logger.info(err);
fs.appendFileSync(logFile, err.stack);
});
cmd.on('exit', (code: number, signal: any) => {
this.logger.info(`cmd exit ${code}`);
this.cronDb.update(
{ _id },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
);
fs.appendFileSync(logFile, `\n执行结束...`);
resolve(code);
});
}); });
} }
public async disabled(ids: string[]) { public async disabled(ids: string[]) {
this.cronDb.update( this.cronDb.update(
{ _id: { $in: ids } }, { _id: { $in: ids } },
{ $set: { status: CrontabStatus.disabled } }, { $set: { isDisabled: 1 } },
{ multi: true }, { multi: true },
); );
await this.set_crontab(true); await this.set_crontab(true);
@ -223,7 +233,7 @@ export default class CronService {
public async enabled(ids: string[]) { public async enabled(ids: string[]) {
this.cronDb.update( this.cronDb.update(
{ _id: { $in: ids } }, { _id: { $in: ids } },
{ $set: { status: CrontabStatus.idle } }, { $set: { isDisabled: 0 } },
{ multi: true }, { multi: true },
); );
await this.set_crontab(true); await this.set_crontab(true);
@ -244,7 +254,7 @@ export default class CronService {
var crontab_string = ''; var crontab_string = '';
tabs.forEach((tab) => { tabs.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(' '); const _schedule = tab.schedule && tab.schedule.split(' ');
if (tab.status === CrontabStatus.disabled || _schedule.length !== 5) { if (tab.isDisabled === 1 || _schedule.length !== 5) {
crontab_string += '# '; crontab_string += '# ';
crontab_string += tab.schedule; crontab_string += tab.schedule;
crontab_string += ' '; crontab_string += ' ';

View File

@ -36,6 +36,7 @@
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"node-schedule": "^2.0.0", "node-schedule": "^2.0.0",
"p-queue": "6.6.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"typedi": "^0.8.0", "typedi": "^0.8.0",
"winston": "^3.3.3" "winston": "^3.3.3"

View File

@ -13,6 +13,9 @@ AutoAddCron="true"
## 设置定时任务执行的超时时间默认1h后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天 ## 设置定时任务执行的超时时间默认1h后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime="1h" CommandTimeoutTime="1h"
## 设置批量执行任务时的并发数默认同时执行5个任务
MaxConcurrentNum="5"
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间 ## 在运行 task 命令时,随机延迟启动任务的最大延迟时间
## 如果任务不是必须准点运行的任务,那么给它增加一个随机延迟,由你定义最大延迟时间,单位为秒,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行 ## 如果任务不是必须准点运行的任务,那么给它增加一个随机延迟,由你定义最大延迟时间,单位为秒,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行
## 在crontab.list中在每小时第0-2分、第30-31分、第59分这几个时间内启动的任务均算作必须准点运行的任务在启动这些任务时即使你定义了RandomDelay也将准点运行不启用随机延迟 ## 在crontab.list中在每小时第0-2分、第30-31分、第59分这几个时间内启动的任务均算作必须准点运行的任务在启动这些任务时即使你定义了RandomDelay也将准点运行不启用随机延迟

View File

@ -24,6 +24,7 @@ import {
StopOutlined, StopOutlined,
DeleteOutlined, DeleteOutlined,
PauseCircleOutlined, PauseCircleOutlined,
SendOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
@ -38,6 +39,7 @@ enum CrontabStatus {
'running', 'running',
'idle', 'idle',
'disabled', 'disabled',
'queued',
} }
enum OperationName { enum OperationName {
@ -99,17 +101,29 @@ const Crontab = () => {
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => ( render: (text: string, record: any) => (
<> <>
{record.status === CrontabStatus.idle && ( {!record.isDisabled && (
<Tag icon={<ClockCircleOutlined />} color="default"> <>
{record.status === CrontabStatus.idle && (
</Tag> <Tag icon={<ClockCircleOutlined />} color="default">
</Tag>
)}
{record.status === CrontabStatus.running && (
<Tag
icon={<Loading3QuartersOutlined spin />}
color="processing"
>
</Tag>
)}
{record.status === CrontabStatus.queued && (
<Tag icon={<SendOutlined />} color="default">
</Tag>
)}
</>
)} )}
{record.status === CrontabStatus.running && ( {record.isDisabled === 1 && (
<Tag icon={<Loading3QuartersOutlined spin />} color="processing">
</Tag>
)}
{record.status === CrontabStatus.disabled && (
<Tag icon={<CloseCircleOutlined />} color="error"> <Tag icon={<CloseCircleOutlined />} color="error">
</Tag> </Tag>
@ -123,7 +137,7 @@ const Crontab = () => {
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any, index: number) => ( render: (text: string, record: any, index: number) => (
<Space size="middle"> <Space size="middle">
{record.status !== CrontabStatus.running && ( {record.status === CrontabStatus.idle && (
<Tooltip title="运行"> <Tooltip title="运行">
<a <a
onClick={() => { onClick={() => {
@ -134,7 +148,7 @@ const Crontab = () => {
</a> </a>
</Tooltip> </Tooltip>
)} )}
{record.status === CrontabStatus.running && ( {record.status !== CrontabStatus.idle && (
<Tooltip title="停止"> <Tooltip title="停止">
<a <a
onClick={() => { onClick={() => {
@ -303,12 +317,10 @@ const Crontab = () => {
const enabledOrDisabledCron = (record: any, index: number) => { const enabledOrDisabledCron = (record: any, index: number) => {
Modal.confirm({ Modal.confirm({
title: `确认${ title: `确认${record.isDisabled === 1 ? '启用' : '禁用'}`,
record.status === CrontabStatus.disabled ? '启用' : '禁用'
}`,
content: ( content: (
<> <>
{record.status === CrontabStatus.disabled ? '启用' : '禁用'} {record.isDisabled === 1 ? '启用' : '禁用'}
{' '} {' '}
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name} {record.name}
@ -320,7 +332,7 @@ const Crontab = () => {
request request
.put( .put(
`${config.apiPrefix}crons/${ `${config.apiPrefix}crons/${
record.status === CrontabStatus.disabled ? 'enable' : 'disable' record.isDisabled === 1 ? 'enable' : 'disable'
}`, }`,
{ {
data: [record._id], data: [record._id],
@ -328,14 +340,11 @@ const Crontab = () => {
) )
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
const newStatus = const newStatus = record.isDisabled === 1 ? 0 : 1;
record.status === CrontabStatus.disabled
? CrontabStatus.idle
: CrontabStatus.disabled;
const result = [...value]; const result = [...value];
result.splice(index, 1, { result.splice(index, 1, {
...record, ...record,
status: newStatus, isDisabled: newStatus,
}); });
setValue(result); setValue(result);
} else { } else {
@ -366,14 +375,14 @@ const Crontab = () => {
<Menu.Item <Menu.Item
key="enableordisable" key="enableordisable"
icon={ icon={
record.status === CrontabStatus.disabled ? ( record.isDisabled === 1 ? (
<CheckCircleOutlined /> <CheckCircleOutlined />
) : ( ) : (
<StopOutlined /> <StopOutlined />
) )
} }
> >
{record.status === CrontabStatus.disabled ? '启用' : '禁用'} {record.isDisabled === 1 ? '启用' : '禁用'}
</Menu.Item> </Menu.Item>
{record.isSystem !== 1 && ( {record.isSystem !== 1 && (
<Menu.Item key="delete" icon={<DeleteOutlined />}> <Menu.Item key="delete" icon={<DeleteOutlined />}>

View File

@ -11,6 +11,7 @@ enum CrontabStatus {
'running', 'running',
'idle', 'idle',
'disabled', 'disabled',
'queued',
} }
const CronLogModal = ({ const CronLogModal = ({