mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-01 05:13:50 +08:00
perf: bound runtime memory and storage usage
This commit is contained in:
@@ -366,6 +366,23 @@
|
||||
"添加标签成功": "Tags added successfully",
|
||||
"清空日志": "Clear Logs",
|
||||
"清除依赖缓存": "Clean dependency cache",
|
||||
"历史数据保留与手动清理": "History retention and manual cleanup",
|
||||
"保留天数为0时禁用对应清理,预览不会删除数据": "Set retention to 0 to disable that cleanup. Preview never deletes data.",
|
||||
"任务统计": "Task statistics",
|
||||
"清除 Node 依赖缓存": "Clean Node dependency cache",
|
||||
"清除 Python 依赖缓存": "Clean Python dependency cache",
|
||||
"清理后压缩数据库": "Compact database after cleanup",
|
||||
"保存设置": "Save settings",
|
||||
"预览清理": "Preview cleanup",
|
||||
"确认清理存储数据": "Confirm storage cleanup",
|
||||
"确认清理": "Clean now",
|
||||
"将删除历史运行实例": "Historical running instances to delete",
|
||||
"将删除任务统计": "Task statistics to delete",
|
||||
"将清除依赖缓存": "Dependency caches to clear",
|
||||
"清理依赖缓存后相关依赖需要重新安装": "Dependencies must be reinstalled after their cache is cleared.",
|
||||
"数据库压缩期间可能暂时阻塞请求": "Database compaction may briefly block requests.",
|
||||
"此操作不可恢复,请确认已完成必要备份": "This action cannot be undone. Confirm that required backups are complete.",
|
||||
"清理完成": "Cleanup complete",
|
||||
"清除成功": "Clean successful",
|
||||
"源文件": "Source File",
|
||||
"激活成功": "Activation successful",
|
||||
|
||||
@@ -366,6 +366,23 @@
|
||||
"添加标签成功": "添加标签成功",
|
||||
"清空日志": "清空日志",
|
||||
"清除依赖缓存": "清除依赖缓存",
|
||||
"历史数据保留与手动清理": "历史数据保留与手动清理",
|
||||
"保留天数为0时禁用对应清理,预览不会删除数据": "保留天数为0时禁用对应清理,预览不会删除数据",
|
||||
"任务统计": "任务统计",
|
||||
"清除 Node 依赖缓存": "清除 Node 依赖缓存",
|
||||
"清除 Python 依赖缓存": "清除 Python 依赖缓存",
|
||||
"清理后压缩数据库": "清理后压缩数据库",
|
||||
"保存设置": "保存设置",
|
||||
"预览清理": "预览清理",
|
||||
"确认清理存储数据": "确认清理存储数据",
|
||||
"确认清理": "确认清理",
|
||||
"将删除历史运行实例": "将删除历史运行实例",
|
||||
"将删除任务统计": "将删除任务统计",
|
||||
"将清除依赖缓存": "将清除依赖缓存",
|
||||
"清理依赖缓存后相关依赖需要重新安装": "清理依赖缓存后相关依赖需要重新安装",
|
||||
"数据库压缩期间可能暂时阻塞请求": "数据库压缩期间可能暂时阻塞请求",
|
||||
"此操作不可恢复,请确认已完成必要备份": "此操作不可恢复,请确认已完成必要备份",
|
||||
"清理完成": "清理完成",
|
||||
"清除成功": "清除成功",
|
||||
"源文件": "源文件",
|
||||
"激活成功": "激活成功",
|
||||
|
||||
@@ -16,11 +16,12 @@ import {
|
||||
CheckCircleOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { PageLoading } from "@ant-design/pro-layout";
|
||||
import { logEnded } from "@/utils";
|
||||
import { CrontabStatus } from "./type";
|
||||
import Ansi from "ansi-to-react";
|
||||
|
||||
const { Countdown } = Statistic;
|
||||
const LOG_CHUNK_BYTES = 256 * 1024;
|
||||
const MAX_LOG_VIEW_CHARS = 1024 * 1024;
|
||||
|
||||
const CronLogModal = ({
|
||||
cron,
|
||||
@@ -38,35 +39,49 @@ const CronLogModal = ({
|
||||
const [executing, setExecuting] = useState<any>(true);
|
||||
const [isPhone, setIsPhone] = useState(false);
|
||||
const scrollInfoRef = useRef({ value: 0, down: true });
|
||||
const logOffsetRef = useRef<number>();
|
||||
const valueRef = useRef(value);
|
||||
const pollTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const uniqPath = logUrl ? logUrl : String(cron?.id);
|
||||
|
||||
const getCronLog = (isFirst?: boolean) => {
|
||||
if (isFirst) {
|
||||
setLoading(true);
|
||||
}
|
||||
const baseUrl = logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`;
|
||||
const separator = baseUrl.includes("?") ? "&" : "?";
|
||||
const offset = isFirst ? undefined : logOffsetRef.current;
|
||||
const pagination = `${separator}limit=${LOG_CHUNK_BYTES}${
|
||||
isFirst ? "&tail=true" : offset !== undefined ? `&offset=${offset}` : ""
|
||||
}`;
|
||||
request
|
||||
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
|
||||
.then(({ code, data, logStatus }) => {
|
||||
if (
|
||||
code === 200 &&
|
||||
localStorage.getItem("logCron") === uniqPath &&
|
||||
data !== value
|
||||
) {
|
||||
const log = (data as string) || intl.get("暂无日志");
|
||||
setValue(log);
|
||||
const hasNext = logStatus === 'running';
|
||||
if (!hasNext && !logEnded(value) && value !== intl.get("启动中...")) {
|
||||
setTimeout(() => {
|
||||
autoScroll();
|
||||
});
|
||||
}
|
||||
setExecuting(hasNext);
|
||||
if (hasNext) {
|
||||
setTimeout(() => {
|
||||
autoScroll();
|
||||
getCronLog();
|
||||
}, 2000);
|
||||
}
|
||||
.get(`${baseUrl}${pagination}`)
|
||||
.then(({ code, data, logStatus, nextOffset }) => {
|
||||
if (code !== 200 || localStorage.getItem("logCron") !== uniqPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasNext = logStatus === "running";
|
||||
const chunk = (data as string) || "";
|
||||
let log = isFirst ? chunk : `${valueRef.current}${chunk}`;
|
||||
if (!log && !hasNext) {
|
||||
log = intl.get("暂无日志");
|
||||
}
|
||||
if (log.length > MAX_LOG_VIEW_CHARS) {
|
||||
log = log.slice(-MAX_LOG_VIEW_CHARS);
|
||||
}
|
||||
valueRef.current = log;
|
||||
setValue(log);
|
||||
if (typeof nextOffset === "number") {
|
||||
logOffsetRef.current = nextOffset;
|
||||
}
|
||||
setExecuting(hasNext);
|
||||
|
||||
if (chunk || !hasNext) {
|
||||
autoScroll();
|
||||
}
|
||||
if (hasNext) {
|
||||
pollTimerRef.current = setTimeout(() => getCronLog(), 2000);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -89,6 +104,9 @@ const CronLogModal = ({
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
if (pollTimerRef.current) {
|
||||
clearTimeout(pollTimerRef.current);
|
||||
}
|
||||
localStorage.removeItem("logCron");
|
||||
handleCancel();
|
||||
};
|
||||
@@ -117,12 +135,19 @@ const CronLogModal = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (cron && cron.id) {
|
||||
logOffsetRef.current = undefined;
|
||||
getCronLog(true);
|
||||
}
|
||||
return () => {
|
||||
if (pollTimerRef.current) {
|
||||
clearTimeout(pollTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, [cron]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
valueRef.current = data;
|
||||
setValue(data);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
@@ -59,7 +59,7 @@ const Log = () => {
|
||||
.get(
|
||||
`${config.apiPrefix}logs/detail?file=${node.title}&path=${
|
||||
node.parent || ''
|
||||
}`,
|
||||
}&tail=true&limit=${1024 * 1024}`,
|
||||
)
|
||||
.then(({ code, data }) => {
|
||||
if (code === 200) {
|
||||
|
||||
@@ -51,7 +51,6 @@ const Setting = () => {
|
||||
reloadTheme,
|
||||
systemInfo,
|
||||
} = useOutletContext<SharedContext>();
|
||||
console.log('user', user);
|
||||
const columns = [
|
||||
{
|
||||
title: intl.get('名称'),
|
||||
|
||||
@@ -62,6 +62,8 @@ const Other = ({
|
||||
cronConcurrency?: number | null;
|
||||
timezone?: string | null;
|
||||
globalSshKey?: string | null;
|
||||
runningInstanceRetentionDays?: number | null;
|
||||
cronStatRetentionDays?: number | null;
|
||||
}>();
|
||||
const [form] = Form.useForm();
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
@@ -69,6 +71,11 @@ const Other = ({
|
||||
const showDownloadProgress = useProgress(intl.get('下载'));
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [selectedModules, setSelectedModules] = useState<string[]>(['base']);
|
||||
const [cleanupLoading, setCleanupLoading] = useState(false);
|
||||
const [dependenceCacheTypes, setDependenceCacheTypes] = useState<string[]>(
|
||||
[],
|
||||
);
|
||||
const [compactDatabase, setCompactDatabase] = useState(false);
|
||||
|
||||
const {
|
||||
enable: enableDarkMode,
|
||||
@@ -136,6 +143,93 @@ const Other = ({
|
||||
});
|
||||
};
|
||||
|
||||
const retentionPayload = () => ({
|
||||
runningInstanceRetentionDays:
|
||||
systemConfig?.runningInstanceRetentionDays || 0,
|
||||
cronStatRetentionDays: systemConfig?.cronStatRetentionDays || 0,
|
||||
});
|
||||
|
||||
const saveRetentionPolicy = () => {
|
||||
request
|
||||
.put(
|
||||
`${config.apiPrefix}system/storage-retention/config`,
|
||||
retentionPayload(),
|
||||
)
|
||||
.then(({ code }) => {
|
||||
if (code === 200) {
|
||||
message.success(intl.get('更新成功'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
if (!bytes) return '0 B';
|
||||
const units = ['B', 'KiB', 'MiB', 'GiB'];
|
||||
const unit = Math.min(
|
||||
Math.floor(Math.log(bytes) / Math.log(1024)),
|
||||
units.length - 1,
|
||||
);
|
||||
return `${(bytes / 1024 ** unit).toFixed(unit ? 1 : 0)} ${units[unit]}`;
|
||||
};
|
||||
|
||||
const previewStorageCleanup = () => {
|
||||
setCleanupLoading(true);
|
||||
const payload = {
|
||||
...retentionPayload(),
|
||||
dependenceCacheTypes,
|
||||
compactDatabase,
|
||||
};
|
||||
request
|
||||
.post(`${config.apiPrefix}system/storage-retention/preview`, payload)
|
||||
.then(({ code, data }) => {
|
||||
if (code !== 200) return;
|
||||
Modal.confirm({
|
||||
width: 560,
|
||||
centered: true,
|
||||
title: intl.get('确认清理存储数据'),
|
||||
okText: intl.get('确认清理'),
|
||||
cancelText: intl.get('取消'),
|
||||
okButtonProps: { danger: true },
|
||||
content: (
|
||||
<div>
|
||||
<p>
|
||||
{intl.get('将删除历史运行实例')}: {data.runningInstances}
|
||||
</p>
|
||||
<p>
|
||||
{intl.get('将删除任务统计')}: {data.cronStats}
|
||||
</p>
|
||||
<p>
|
||||
{intl.get('将清除依赖缓存')}: {data.dependenceCaches.length} (
|
||||
{formatBytes(data.dependenceCacheBytes)})
|
||||
</p>
|
||||
{data.dependenceCaches.length > 0 && (
|
||||
<p>{intl.get('清理依赖缓存后相关依赖需要重新安装')}</p>
|
||||
)}
|
||||
{compactDatabase && (
|
||||
<p>{intl.get('数据库压缩期间可能暂时阻塞请求')}</p>
|
||||
)}
|
||||
<p>{intl.get('此操作不可恢复,请确认已完成必要备份')}</p>
|
||||
</div>
|
||||
),
|
||||
onOk: () => {
|
||||
setCleanupLoading(true);
|
||||
return request
|
||||
.post(`${config.apiPrefix}system/storage-retention/cleanup`, {
|
||||
...payload,
|
||||
confirmation: 'CLEAN',
|
||||
})
|
||||
.then(({ code }) => {
|
||||
if (code === 200) {
|
||||
message.success(intl.get('清理完成'));
|
||||
}
|
||||
})
|
||||
.finally(() => setCleanupLoading(false));
|
||||
},
|
||||
});
|
||||
})
|
||||
.finally(() => setCleanupLoading(false));
|
||||
};
|
||||
|
||||
const exportData = () => {
|
||||
setExportLoading(true);
|
||||
request
|
||||
@@ -302,6 +396,69 @@ const Other = ({
|
||||
</Button>
|
||||
</Input.Group>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={intl.get('历史数据保留与手动清理')}
|
||||
tooltip={intl.get('保留天数为0时禁用对应清理,预览不会删除数据')}
|
||||
>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<InputNumber
|
||||
addonBefore={intl.get('运行实例')}
|
||||
addonAfter={intl.get('天')}
|
||||
min={0}
|
||||
max={3650}
|
||||
value={systemConfig?.runningInstanceRetentionDays || 0}
|
||||
onChange={(value) => {
|
||||
setSystemConfig({
|
||||
...systemConfig,
|
||||
runningInstanceRetentionDays: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<InputNumber
|
||||
addonBefore={intl.get('任务统计')}
|
||||
addonAfter={intl.get('天')}
|
||||
min={0}
|
||||
max={3650}
|
||||
value={systemConfig?.cronStatRetentionDays || 0}
|
||||
onChange={(value) => {
|
||||
setSystemConfig({
|
||||
...systemConfig,
|
||||
cronStatRetentionDays: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Checkbox.Group
|
||||
value={dependenceCacheTypes}
|
||||
options={[
|
||||
{ label: intl.get('清除 Node 依赖缓存'), value: 'node' },
|
||||
{ label: intl.get('清除 Python 依赖缓存'), value: 'python3' },
|
||||
]}
|
||||
onChange={(value) => setDependenceCacheTypes(value as string[])}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Checkbox
|
||||
checked={compactDatabase}
|
||||
onChange={(event) => setCompactDatabase(event.target.checked)}
|
||||
>
|
||||
{intl.get('清理后压缩数据库')}
|
||||
</Checkbox>
|
||||
</div>
|
||||
<Button onClick={saveRetentionPolicy} style={{ marginRight: 8 }}>
|
||||
{intl.get('保存设置')}
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
loading={cleanupLoading}
|
||||
onClick={previewStorageCleanup}
|
||||
>
|
||||
{intl.get('预览清理')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item label={intl.get('定时任务并发数')} name="frequency">
|
||||
<Input.Group compact>
|
||||
<InputNumber
|
||||
|
||||
@@ -29,7 +29,9 @@ const SystemLog = ({ height, theme }: any) => {
|
||||
const { loading, refresh } = useRequest(
|
||||
() => {
|
||||
return request.get<Blob>(
|
||||
`${config.apiPrefix}system/log?startTime=${range[0]}&endTime=${range[1]}`,
|
||||
`${config.apiPrefix}system/log?startTime=${range[0]}&endTime=${
|
||||
range[1]
|
||||
}&limit=${1024 * 1024}`,
|
||||
{
|
||||
responseType: 'blob',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user