feat(ping): add ping caching functionality with configurable duration in UI settings
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 2m42s
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 2m42s
This commit is contained in:
@@ -3,8 +3,9 @@
|
|||||||
* Использует RouterOS REST API — требует RouterOS 7.1+ с www-ssl (443) или www (80)
|
* Использует RouterOS REST API — требует RouterOS 7.1+ с www-ssl (443) или www (80)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
const { GetObjectCommand } = require('@aws-sdk/client-s3');
|
const { GetObjectCommand } = require('@aws-sdk/client-s3');
|
||||||
const { s3, BUCKET_NAME, streamToString, readS3TextObject } = require('../services/s3Service');
|
const { s3, BUCKET_NAME, streamToString, readS3TextObject, writeS3JsonObject } = require('../services/s3Service');
|
||||||
const { sendError, sendOk } = require('../middleware/errorHandler');
|
const { sendError, sendOk } = require('../middleware/errorHandler');
|
||||||
const { decrypt } = require('../utils/encryption');
|
const { decrypt } = require('../utils/encryption');
|
||||||
const { readServersFromS3 } = require('./serversRoutes');
|
const { readServersFromS3 } = require('./serversRoutes');
|
||||||
@@ -19,8 +20,9 @@ const { createRosClient, applyBlock } = require('../services/mikrotikApplyServic
|
|||||||
const IPSEC_PASSWORDS_KEY = 'network-config/ipsec-passwords.json';
|
const IPSEC_PASSWORDS_KEY = 'network-config/ipsec-passwords.json';
|
||||||
const NETWORK_CONFIG_KEY = 'network-config.json';
|
const NETWORK_CONFIG_KEY = 'network-config.json';
|
||||||
const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json';
|
const UI_SETTINGS_KEY = 'bgp_data/rt_ui_settings.json';
|
||||||
|
const PING_CACHE_PREFIX = 'ping-cache/';
|
||||||
|
|
||||||
/** Загрузить UI-настройки из S3 (для pingDomain и др.) */
|
/** Загрузить UI-настройки из S3 (для pingDomain, pingCacheMinutes и др.) */
|
||||||
async function loadUiSettings() {
|
async function loadUiSettings() {
|
||||||
try {
|
try {
|
||||||
const data = await readS3TextObject(UI_SETTINGS_KEY).catch(() => ({ body: '{}' }));
|
const data = await readS3TextObject(UI_SETTINGS_KEY).catch(() => ({ body: '{}' }));
|
||||||
@@ -31,6 +33,12 @@ async function loadUiSettings() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ключ кеша пинга по serverId, gatewayIp, target */
|
||||||
|
function pingCacheKey(serverId, gatewayIp, target) {
|
||||||
|
const payload = `${serverId}|${gatewayIp || ''}|${target || ''}`;
|
||||||
|
return PING_CACHE_PREFIX + crypto.createHash('sha256').update(payload, 'utf8').digest('hex') + '.json';
|
||||||
|
}
|
||||||
|
|
||||||
/** Загрузить network-config из S3 */
|
/** Загрузить network-config из S3 */
|
||||||
async function loadNetworkConfig() {
|
async function loadNetworkConfig() {
|
||||||
try {
|
try {
|
||||||
@@ -311,15 +319,41 @@ async function pingViaInterface(req, res) {
|
|||||||
return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST');
|
return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Цель пинга: из тела запроса, иначе из настроек интерфейса (Домен для пинга), иначе по умолчанию
|
const uiSettings = await loadUiSettings();
|
||||||
|
|
||||||
|
// Цель пинга: из тела запроса, иначе из настроек (Домен для пинга), иначе по умолчанию
|
||||||
if (target == null || String(target).trim() === '') {
|
if (target == null || String(target).trim() === '') {
|
||||||
const uiSettings = await loadUiSettings();
|
|
||||||
const pingDomain = (uiSettings.pingDomain && String(uiSettings.pingDomain).trim()) || '';
|
const pingDomain = (uiSettings.pingDomain && String(uiSettings.pingDomain).trim()) || '';
|
||||||
target = pingDomain || 'www.gstatic.com';
|
target = pingDomain || 'www.gstatic.com';
|
||||||
} else {
|
} else {
|
||||||
target = String(target).trim();
|
target = String(target).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pingCacheMinutes = Math.max(0, parseInt(uiSettings.pingCacheMinutes, 10) || 0);
|
||||||
|
const cacheKey = pingCacheMinutes > 0 ? pingCacheKey(serverId, gatewayIp, target) : null;
|
||||||
|
|
||||||
|
if (cacheKey) {
|
||||||
|
try {
|
||||||
|
const data = await readS3TextObject(cacheKey).catch(() => null);
|
||||||
|
if (data?.body) {
|
||||||
|
const cached = JSON.parse(data.body);
|
||||||
|
const cachedAt = typeof cached.cachedAt === 'number' ? cached.cachedAt : 0;
|
||||||
|
const ttlMs = pingCacheMinutes * 60 * 1000;
|
||||||
|
if (cachedAt && Date.now() - cachedAt < ttlMs) {
|
||||||
|
return res.json({
|
||||||
|
ok: true,
|
||||||
|
avgMs: cached.avgMs,
|
||||||
|
minMs: cached.minMs,
|
||||||
|
maxMs: cached.maxMs,
|
||||||
|
loss: cached.loss,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// кеш протух или невалиден — выполняем реальный пинг
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const servers = await readServersFromS3();
|
const servers = await readServersFromS3();
|
||||||
const server = servers.find((s) => (s.id || s.dns || s.ip) === serverId);
|
const server = servers.find((s) => (s.id || s.dns || s.ip) === serverId);
|
||||||
if (!server || server.type !== 'jumphost') {
|
if (!server || server.type !== 'jumphost') {
|
||||||
@@ -486,15 +520,19 @@ async function pingViaInterface(req, res) {
|
|||||||
? Number(String(summary['packet-loss']).replace('%', ''))
|
? Number(String(summary['packet-loss']).replace('%', ''))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// ВАЖНО: возвращаем данные напрямую, без sendOk,
|
const result = { ok: true, avgMs, minMs, maxMs, loss };
|
||||||
// чтобы фронтенд (EasySwitch) мог читать поля avgMs/minMs/maxMs/loss
|
|
||||||
return res.json({
|
if (cacheKey) {
|
||||||
ok: true,
|
writeS3JsonObject(cacheKey, {
|
||||||
avgMs,
|
avgMs,
|
||||||
minMs,
|
minMs,
|
||||||
maxMs,
|
maxMs,
|
||||||
loss,
|
loss,
|
||||||
});
|
cachedAt: Date.now(),
|
||||||
|
}).catch((err) => console.warn('[mikrotik][pingViaInterface] cache write failed:', err?.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = error.response?.data?.message || error.message || 'Ping failed';
|
const msg = error.response?.data?.message || error.message || 'Ping failed';
|
||||||
const status = error.response?.status;
|
const status = error.response?.status;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export default function SettingsModal({ open, onClose }) {
|
|||||||
const [wsUrl, setWsUrl] = useState('');
|
const [wsUrl, setWsUrl] = useState('');
|
||||||
const [baseAS, setBaseAS] = useState('65001');
|
const [baseAS, setBaseAS] = useState('65001');
|
||||||
const [pingDomain, setPingDomain] = useState('');
|
const [pingDomain, setPingDomain] = useState('');
|
||||||
|
const [pingCacheMinutes, setPingCacheMinutes] = useState('');
|
||||||
const [ptrZoneReplaceFrom, setPtrZoneReplaceFrom] = useState('');
|
const [ptrZoneReplaceFrom, setPtrZoneReplaceFrom] = useState('');
|
||||||
const [ptrZoneReplaceTo, setPtrZoneReplaceTo] = useState('');
|
const [ptrZoneReplaceTo, setPtrZoneReplaceTo] = useState('');
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ export default function SettingsModal({ open, onClose }) {
|
|||||||
setWsUrl(String(data?.wsUpdateUrl || ''));
|
setWsUrl(String(data?.wsUpdateUrl || ''));
|
||||||
setBaseAS(String(data?.baseAS || '65001'));
|
setBaseAS(String(data?.baseAS || '65001'));
|
||||||
setPingDomain(String(data?.pingDomain || '').trim());
|
setPingDomain(String(data?.pingDomain || '').trim());
|
||||||
|
setPingCacheMinutes(data?.pingCacheMinutes != null ? String(data.pingCacheMinutes) : '');
|
||||||
setPtrZoneReplaceFrom(String(data?.ptrZoneReplaceFrom || ''));
|
setPtrZoneReplaceFrom(String(data?.ptrZoneReplaceFrom || ''));
|
||||||
setPtrZoneReplaceTo(String(data?.ptrZoneReplaceTo || ''));
|
setPtrZoneReplaceTo(String(data?.ptrZoneReplaceTo || ''));
|
||||||
const e = res?.headers?.etag || res?.headers?.ETag || '';
|
const e = res?.headers?.etag || res?.headers?.ETag || '';
|
||||||
@@ -100,6 +102,7 @@ export default function SettingsModal({ open, onClose }) {
|
|||||||
wsUpdateUrl: String(wsUrl || '').trim(),
|
wsUpdateUrl: String(wsUrl || '').trim(),
|
||||||
baseAS: String(baseAS || '65001').trim(),
|
baseAS: String(baseAS || '65001').trim(),
|
||||||
pingDomain: String(pingDomain || '').trim(),
|
pingDomain: String(pingDomain || '').trim(),
|
||||||
|
pingCacheMinutes: Math.max(0, parseInt(pingCacheMinutes, 10) || 0),
|
||||||
ptrZoneReplaceFrom: String(ptrZoneReplaceFrom || '').trim(),
|
ptrZoneReplaceFrom: String(ptrZoneReplaceFrom || '').trim(),
|
||||||
ptrZoneReplaceTo: String(ptrZoneReplaceTo || '').trim(),
|
ptrZoneReplaceTo: String(ptrZoneReplaceTo || '').trim(),
|
||||||
};
|
};
|
||||||
@@ -212,6 +215,18 @@ export default function SettingsModal({ open, onClose }) {
|
|||||||
disabled={loading || saving}
|
disabled={loading || saving}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label="Срок кеша пинга (мин)"
|
||||||
|
name="pingCacheMinutes"
|
||||||
|
type="number"
|
||||||
|
value={pingCacheMinutes}
|
||||||
|
onChange={setPingCacheMinutes}
|
||||||
|
placeholder="0"
|
||||||
|
helpText="0 — без кеша. При значении больше 0 результаты пинга кешируются в S3 на указанное количество минут."
|
||||||
|
disabled={loading || saving}
|
||||||
|
min={0}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="mt-4 pt-3 border-top">
|
<div className="mt-4 pt-3 border-top">
|
||||||
<h6 className="mb-3">Настройка PTR зоны</h6>
|
<h6 className="mb-3">Настройка PTR зоны</h6>
|
||||||
<div className="row g-3">
|
<div className="row g-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user