修改并发逻辑,系统设置增加定时任务并发设置

This commit is contained in:
whyour 2023-07-01 15:26:20 +08:00
parent db227e56bf
commit 702c3160ec
18 changed files with 163 additions and 88 deletions

View File

@ -70,12 +70,12 @@ export default (app: Router) => {
}); });
route.get( route.get(
'/log/remove', '/config',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
const data = await systemService.getLogRemoveFrequency(); const data = await systemService.getSystemConfig();
res.send({ code: 200, data }); res.send({ code: 200, data });
} catch (e) { } catch (e) {
return next(e); return next(e);
@ -84,18 +84,19 @@ export default (app: Router) => {
); );
route.put( route.put(
'/log/remove', '/config',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
frequency: Joi.number().required(), logRemoveFrequency: Joi.number().optional().allow(null),
cronConcurrency: Joi.number().optional().allow(null),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
const result = await systemService.updateLogRemoveFrequency( const result = await systemService.updateSystemConfig(
req.body.frequency, req.body,
); );
res.send(result); res.send(result);
} catch (e) { } catch (e) {

View File

@ -1,10 +1,11 @@
import { sequelize } from '.'; import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize'; import { DataTypes, Model, ModelDefined } from 'sequelize';
import { NotificationInfo } from './notify';
export class AuthInfo { export class AuthInfo {
ip?: string; ip?: string;
type: AuthDataType; type: AuthDataType;
info?: any; info?: AuthModelInfo;
id?: number; id?: number;
constructor(options: AuthInfo) { constructor(options: AuthInfo) {
@ -25,9 +26,25 @@ export enum AuthDataType {
'authToken' = 'authToken', 'authToken' = 'authToken',
'notification' = 'notification', 'notification' = 'notification',
'removeLogFrequency' = 'removeLogFrequency', 'removeLogFrequency' = 'removeLogFrequency',
'systemConfig' = 'systemConfig',
} }
interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo {} export interface SystemConfigInfo {
logRemoveFrequency?: number;
cronConcurrency?: number;
}
export interface LoginLogInfo {
timestamp?: number;
address?: string;
ip?: string;
platform?: string;
status?: LoginStatus,
}
export type AuthModelInfo = SystemConfigInfo & Partial<NotificationInfo> & LoginLogInfo;
interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo { }
export const AuthModel = sequelize.define<AuthInstance>('Auth', { export const AuthModel = sequelize.define<AuthInstance>('Auth', {
ip: DataTypes.STRING, ip: DataTypes.STRING,
type: DataTypes.STRING, type: DataTypes.STRING,

View File

@ -44,9 +44,9 @@ export class Crontab {
export enum CrontabStatus { export enum CrontabStatus {
'running', 'running',
'queued',
'idle', 'idle',
'disabled', 'disabled',
'queued',
} }
interface CronInstance extends Model<Crontab, Crontab>, Crontab {} interface CronInstance extends Model<Crontab, Crontab>, Crontab {}

View File

@ -32,7 +32,7 @@ export default async () => {
// 初始化更新所有任务状态为空闲 // 初始化更新所有任务状态为空闲
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.idle }, { status: CrontabStatus.idle },
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } }, { where: { status: { [Op.ne]: CrontabStatus.disabled } } },
); );
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖 // 初始化时安装所有处于安装中,安装成功,安装失败的依赖

View File

@ -18,10 +18,13 @@ const confFile = path.join(configPath, 'config.sh');
const authConfigFile = path.join(configPath, 'auth.json'); const authConfigFile = path.join(configPath, 'auth.json');
const sampleConfigFile = path.join(samplePath, 'config.sample.sh'); const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
const sampleAuthFile = path.join(samplePath, 'auth.sample.json'); const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
const sampleNotifyJsFile = path.join(samplePath, 'notify.js'); const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
const sampleNotifyPyFile = path.join(samplePath, 'notify.py'); const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js'); const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py'); const scriptNotifyPyFile = path.join(scriptPath, 'notify.py');
const TaskBeforeFile = path.join(configPath, 'task_before.sh');
const TaskAfterFile = path.join(configPath, 'task_after.sh');
const homedir = os.homedir(); const homedir = os.homedir();
const sshPath = path.resolve(homedir, '.ssh'); const sshPath = path.resolve(homedir, '.ssh');
const sshdPath = path.join(dataPath, 'ssh.d'); const sshdPath = path.join(dataPath, 'ssh.d');
@ -39,6 +42,8 @@ export default async () => {
const tmpDirExist = await fileExist(tmpPath); const tmpDirExist = await fileExist(tmpPath);
const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile); const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile);
const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile); const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile);
const TaskBeforeFileExist = await fileExist(TaskBeforeFile);
const TaskAfterFileExist = await fileExist(TaskAfterFile);
if (!configDirExist) { if (!configDirExist) {
fs.mkdirSync(configPath); fs.mkdirSync(configPath);
@ -89,6 +94,14 @@ export default async () => {
fs.writeFileSync(scriptNotifyPyFile, fs.readFileSync(sampleNotifyPyFile)); fs.writeFileSync(scriptNotifyPyFile, fs.readFileSync(sampleNotifyPyFile));
} }
if (!TaskBeforeFileExist) {
fs.writeFileSync(TaskBeforeFile, fs.readFileSync(sampleTaskShellFile));
}
if (!TaskAfterFileExist) {
fs.writeFileSync(TaskAfterFile, fs.readFileSync(sampleTaskShellFile));
}
dotenv.config({ path: confFile }); dotenv.config({ path: confFile });
Logger.info('✌️ Init file down'); Logger.info('✌️ Init file down');

View File

@ -29,7 +29,7 @@ export default async () => {
}); });
// 运行删除日志任务 // 运行删除日志任务
const data = await systemService.getLogRemoveFrequency(); const data = await systemService.getSystemConfig();
if (data && data.info && data.info.frequency) { if (data && data.info && data.info.frequency) {
const rmlogCron = { const rmlogCron = {
id: data.id, id: data.id,

View File

@ -7,21 +7,21 @@ import fs from 'fs';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { import {
getFileContentByName, getFileContentByName,
concurrentRun,
fileExist, fileExist,
killTask, killTask,
} from '../config/util'; } from '../config/util';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { Op, where, col as colFn, FindOptions } from 'sequelize'; import { Op, where, col as colFn, FindOptions, fn } from 'sequelize';
import path from 'path'; import path from 'path';
import { TASK_PREFIX, QL_PREFIX } from '../config/const'; import { TASK_PREFIX, QL_PREFIX } from '../config/const';
import cronClient from '../schedule/client'; import cronClient from '../schedule/client';
import { runWithCpuLimit } from '../shared/pLimit'; import taskLimit from '../shared/pLimit';
import { spawn } from 'cross-spawn'; import { spawn } from 'cross-spawn';
import { Fn } from 'sequelize/types/utils';
@Service() @Service()
export default class CronService { export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) { }
private isSixCron(cron: Crontab) { private isSixCron(cron: Crontab) {
const { schedule } = cron; const { schedule } = cron;
@ -281,7 +281,7 @@ export default class CronService {
} }
} }
private formatViewSort(order: string[][], viewQuery: any) { private formatViewSort(order: (string | Fn)[][], viewQuery: any) {
if (viewQuery.sorts && viewQuery.sorts.length > 0) { if (viewQuery.sorts && viewQuery.sorts.length > 0) {
for (const { property, type } of viewQuery.sorts) { for (const { property, type } of viewQuery.sorts) {
order.unshift([property, type]); order.unshift([property, type]);
@ -387,7 +387,7 @@ export default class CronService {
} }
private async runSingle(cronId: number): Promise<number> { private async runSingle(cronId: number): Promise<number> {
return runWithCpuLimit(() => { return taskLimit.runWithCpuLimit(() => {
return new Promise(async (resolve: any) => { return new Promise(async (resolve: any) => {
const cron = await this.getDb({ id: cronId }); const cron = await this.getDb({ id: cronId });
if (cron.status !== CrontabStatus.queued) { if (cron.status !== CrontabStatus.queued) {

View File

@ -14,7 +14,7 @@ import SockService from './sock';
import { FindOptions, Op } from 'sequelize'; import { FindOptions, Op } from 'sequelize';
import { concurrentRun } from '../config/util'; import { concurrentRun } from '../config/util';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { runOneByOne, runWithCpuLimit } from '../shared/pLimit'; import taskLimit from '../shared/pLimit';
@Service() @Service()
export default class DependenceService { export default class DependenceService {
@ -147,7 +147,7 @@ export default class DependenceService {
isInstall: boolean = true, isInstall: boolean = true,
force: boolean = false, force: boolean = false,
) { ) {
return runOneByOne(() => { return taskLimit.runOneByOne(() => {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
const depIds = [dependency.id!]; const depIds = [dependency.id!];
const status = isInstall const status = isInstall

View File

@ -9,7 +9,7 @@ import {
Task, Task,
} from 'toad-scheduler'; } from 'toad-scheduler';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { runWithCpuLimit } from '../shared/pLimit'; import taskLimit from '../shared/pLimit';
import { spawn } from 'cross-spawn'; import { spawn } from 'cross-spawn';
interface ScheduleTaskType { interface ScheduleTaskType {
@ -49,7 +49,7 @@ export default class ScheduleService {
callbacks: TaskCallbacks = {}, callbacks: TaskCallbacks = {},
completionTime: 'start' | 'end' = 'end', completionTime: 'start' | 'end' = 'end',
) { ) {
return runWithCpuLimit(() => { return taskLimit.runWithCpuLimit(() => {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const startTime = dayjs(); const startTime = dayjs();

View File

@ -2,7 +2,7 @@ import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth'; import { AuthDataType, AuthInfo, AuthModel, AuthModelInfo } from '../data/auth';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
@ -16,6 +16,7 @@ import {
parseVersion, parseVersion,
} from '../config/util'; } from '../config/util';
import { TASK_COMMAND } from '../config/const'; import { TASK_COMMAND } from '../config/const';
import taskLimit from '../shared/pLimit'
@Service() @Service()
export default class SystemService { export default class SystemService {
@ -28,8 +29,8 @@ export default class SystemService {
private sockService: SockService, private sockService: SockService,
) {} ) {}
public async getLogRemoveFrequency() { public async getSystemConfig() {
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency }); const doc = await this.getDb({ type: AuthDataType.systemConfig });
return doc || {}; return doc || {};
} }
@ -62,25 +63,30 @@ export default class SystemService {
} }
} }
public async updateLogRemoveFrequency(frequency: number) { public async updateSystemConfig(info: AuthModelInfo) {
const oDoc = await this.getLogRemoveFrequency(); const oDoc = await this.getSystemConfig();
const result = await this.updateAuthDb({ const result = await this.updateAuthDb({
...oDoc, ...oDoc,
type: AuthDataType.removeLogFrequency, type: AuthDataType.systemConfig,
info: { frequency }, info,
}); });
const cron = { if (info.logRemoveFrequency) {
id: result.id, const cron = {
name: '删除日志', id: result.id,
command: `ql rmlog ${frequency}`, name: '删除日志',
}; command: `ql rmlog ${info.logRemoveFrequency}`,
await this.scheduleService.cancelIntervalTask(cron); };
if (frequency > 0) { await this.scheduleService.cancelIntervalTask(cron);
this.scheduleService.createIntervalTask(cron, { if (info.logRemoveFrequency > 0) {
days: frequency, this.scheduleService.createIntervalTask(cron, {
}); days: info.logRemoveFrequency,
});
}
} }
return { code: 200, data: { ...cron } }; if (info.cronConcurrency) {
await taskLimit.setCustomLimit(info.cronConcurrency);
}
return { code: 200, data: info };
} }
public async checkUpdate() { public async checkUpdate() {

View File

@ -10,7 +10,7 @@ import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default'; import { authenticator } from '@otplib/preset-default';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth'; import { AuthDataType, AuthInfo, AuthModel, AuthModelInfo, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
import { Request } from 'express'; import { Request } from 'express';
@ -27,7 +27,7 @@ export default class UserService {
@Inject('logger') private logger: winston.Logger, @Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService, private scheduleService: ScheduleService,
private sockService: SockService, private sockService: SockService,
) {} ) { }
public async login( public async login(
payloads: { payloads: {
@ -119,8 +119,7 @@ export default class UserService {
}); });
await this.notificationService.notify( await this.notificationService.notify(
'登录通知', '登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${ `你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${req.platform
req.platform
} ip地址 ${ip}`, } ip地址 ${ip}`,
); );
await this.getLoginLog(); await this.getLoginLog();
@ -148,8 +147,7 @@ export default class UserService {
}); });
await this.notificationService.notify( await this.notificationService.notify(
'登录通知', '登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${ `你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${req.platform
req.platform
} ip地址 ${ip}`, } ip地址 ${ip}`,
); );
await this.getLoginLog(); await this.getLoginLog();
@ -187,12 +185,12 @@ export default class UserService {
}); });
} }
public async getLoginLog(): Promise<AuthInfo[]> { public async getLoginLog(): Promise<Array<AuthModelInfo | undefined>> {
const docs = await AuthModel.findAll({ const docs = await AuthModel.findAll({
where: { type: AuthDataType.loginLog }, where: { type: AuthDataType.loginLog },
}); });
if (docs && docs.length > 0) { if (docs && docs.length > 0) {
const result = docs.sort((a, b) => b.info.timestamp - a.info.timestamp); const result = docs.sort((a, b) => b.info!.timestamp! - a.info!.timestamp!);
if (result.length > 100) { if (result.length > 100) {
await AuthModel.destroy({ await AuthModel.destroy({
where: { id: result[result.length - 1].id }, where: { id: result[result.length - 1].id },

View File

@ -1,17 +1,37 @@
import pLimit from "p-limit"; import pLimit from "p-limit";
import os from 'os'; import os from 'os';
import { AuthDataType, AuthModel } from "../data/auth";
const cpuLimit = pLimit(os.cpus().length); class TaskLimit {
const oneLimit = pLimit(1); private oneLimit = pLimit(1);
private cpuLimit = pLimit(Math.max(os.cpus().length, 4));
export function runWithCpuLimit<T>(fn: () => Promise<T>): Promise<T> { constructor() {
return cpuLimit(() => { this.setCustomLimit();
return fn(); }
});
public async setCustomLimit(limit?: number) {
if (limit) {
this.cpuLimit = pLimit(limit);
return;
}
const doc = await AuthModel.findOne({ where: { type: AuthDataType.systemConfig } });
if (doc?.info?.cronConcurrency) {
this.cpuLimit = pLimit(doc?.info?.cronConcurrency);
}
}
public runWithCpuLimit<T>(fn: () => Promise<T>): Promise<T> {
return this.cpuLimit(() => {
return fn();
});
}
public runOneByOne<T>(fn: () => Promise<T>): Promise<T> {
return this.oneLimit(() => {
return fn();
});
}
} }
export function runOneByOne<T>(fn: () => Promise<T>): Promise<T> { export default new TaskLimit();
return oneLimit(() => {
return fn();
});
}

View File

@ -1,9 +1,9 @@
import { spawn } from 'cross-spawn'; import { spawn } from 'cross-spawn';
import { runWithCpuLimit } from "./pLimit"; import taskLimit from "./pLimit";
import Logger from '../loaders/logger'; import Logger from '../loaders/logger';
export function runCron(cmd: string): Promise<number> { export function runCron(cmd: string): Promise<number> {
return runWithCpuLimit(() => { return taskLimit.runWithCpuLimit(() => {
return new Promise(async (resolve: any) => { return new Promise(async (resolve: any) => {
Logger.silly('运行命令: ' + cmd); Logger.silly('运行命令: ' + cmd);

View File

@ -91,6 +91,10 @@ export TG_API_HOST=""
export DD_BOT_TOKEN="" export DD_BOT_TOKEN=""
export DD_BOT_SECRET="" export DD_BOT_SECRET=""
## 企业微信反向代理地址
## (环境变量名 QYWX_ORIGIN)
export QYWX_ORIGIN=""
## 5. 企业微信机器人 ## 5. 企业微信机器人
## 官方说明文档https://work.weixin.qq.com/api/doc/90000/90136/91770 ## 官方说明文档https://work.weixin.qq.com/api/doc/90000/90136/91770
## 下方填写密钥,企业微信推送 webhook 后面的 key ## 下方填写密钥,企业微信推送 webhook 后面的 key

View File

@ -60,9 +60,9 @@ const { Search } = Input;
export enum CrontabStatus { export enum CrontabStatus {
'running', 'running',
'queued',
'idle', 'idle',
'disabled', 'disabled',
'queued',
} }
const CrontabSort: any = { 0: 0, 5: 1, 3: 2, 1: 3, 4: 4 }; const CrontabSort: any = { 0: 0, 5: 1, 3: 2, 1: 3, 4: 4 };

View File

@ -1,6 +1,5 @@
.error-wrapper { .error-wrapper {
display: flex; display: flex;
align-items: center;
justify-content: center; justify-content: center;
height: 100vh; height: 100vh;

View File

@ -81,7 +81,7 @@ const Error = () => {
</Typography.Paragraph> </Typography.Paragraph>
</div> </div>
) : ( ) : (
<PageLoading style={{ paddingTop: 0 }} tip="启动中,请稍后..." /> <PageLoading tip="启动中,请稍后..." />
)} )}
</div> </div>
); );

View File

@ -19,7 +19,10 @@ const Other = ({
reloadTheme, reloadTheme,
}: Pick<SharedContext, 'socketMessage' | 'reloadTheme' | 'systemInfo'>) => { }: Pick<SharedContext, 'socketMessage' | 'reloadTheme' | 'systemInfo'>) => {
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto'; const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
const [logRemoveFrequency, setLogRemoveFrequency] = useState<number | null>(); const [systemConfig, setSystemConfig] = useState<{
logRemoveFrequency?: number | null;
cronConcurrency?: number | null;
}>();
const [form] = Form.useForm(); const [form] = Form.useForm();
const { const {
@ -45,13 +48,12 @@ const Other = ({
reloadTheme(); reloadTheme();
}; };
const getLogRemoveFrequency = () => { const getSystemConfig = () => {
request request
.get(`${config.apiPrefix}system/log/remove`) .get(`${config.apiPrefix}system/config`)
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200 && data.info) { if (code === 200 && data.info) {
const { frequency } = data.info; setSystemConfig(data.info);
setLogRemoveFrequency(frequency);
} }
}) })
.catch((error: any) => { .catch((error: any) => {
@ -59,25 +61,23 @@ const Other = ({
}); });
}; };
const updateRemoveLogFrequency = () => { const updateSystemConfig = () => {
setTimeout(() => { request
request .put(`${config.apiPrefix}system/config`, {
.put(`${config.apiPrefix}system/log/remove`, { data: { ...systemConfig },
data: { frequency: logRemoveFrequency }, })
}) .then(({ code, data }) => {
.then(({ code, data }) => { if (code === 200) {
if (code === 200) { message.success('更新成功');
message.success('更新成功'); }
} })
}) .catch((error: any) => {
.catch((error: any) => { console.log(error);
console.log(error); });
});
});
}; };
useEffect(() => { useEffect(() => {
getLogRemoveFrequency(); getSystemConfig();
}, []); }, []);
return ( return (
@ -100,12 +100,29 @@ const Other = ({
<InputNumber <InputNumber
addonBefore="每" addonBefore="每"
addonAfter="天" addonAfter="天"
style={{ width: 150 }} style={{ width: 142 }}
min={0} min={0}
value={logRemoveFrequency} value={systemConfig?.logRemoveFrequency}
onChange={(value) => setLogRemoveFrequency(value)} onChange={(value) => {
setSystemConfig({ ...systemConfig, logRemoveFrequency: value });
}}
/> />
<Button type="primary" onClick={updateRemoveLogFrequency}> <Button type="primary" onClick={updateSystemConfig}>
</Button>
</Input.Group>
</Form.Item>
<Form.Item label="定时任务并发数" name="frequency">
<Input.Group compact>
<InputNumber
style={{ width: 142 }}
min={1}
value={systemConfig?.cronConcurrency}
onChange={(value) => {
setSystemConfig({ ...systemConfig, cronConcurrency: value });
}}
/>
<Button type="primary" onClick={updateSystemConfig}>
</Button> </Button>
</Input.Group> </Input.Group>