From 3840a91ba5dd88df0058539013c7447ac2897516 Mon Sep 17 00:00:00 2001 From: shats Date: Tue, 24 Feb 2026 12:46:08 +0700 Subject: [PATCH] feat(ping): implement scoped request handling and cancellation for ping requests to improve performance and resource management --- .cursor/debug-378b5f.log | 2 +- frontend/src/EasySwitchManager.jsx | 17 +++++++++-- frontend/src/contexts/PingContext.jsx | 41 +++++++++++++++++++++++++-- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/.cursor/debug-378b5f.log b/.cursor/debug-378b5f.log index 43e7d57..7fe9fcb 100644 --- a/.cursor/debug-378b5f.log +++ b/.cursor/debug-378b5f.log @@ -1 +1 @@ -{"sessionId":"378b5f","runId":"baseline","hypothesisId":"H1_H2","location":"frontend/src/lib/api.js:request","message":"API request started","data":{"method":"GET","url":"/alerts"},"timestamp":1771911695277} +{"sessionId":"378b5f","runId":"baseline","hypothesisId":"H1_H2","location":"frontend/src/lib/api.js:request","message":"API request started","data":{"method":"GET","url":"/alerts"},"timestamp":1771911955281} diff --git a/frontend/src/EasySwitchManager.jsx b/frontend/src/EasySwitchManager.jsx index 437fdeb..285fe6f 100644 --- a/frontend/src/EasySwitchManager.jsx +++ b/frontend/src/EasySwitchManager.jsx @@ -53,7 +53,7 @@ function EasySwitchManager() { const [expandedServers, setExpandedServers] = useState(new Set()); // Развернутые серверы const [groupByTags, setGroupByTags] = useState(true); // Группировка по тегам // Единый источник пингов (кеш + API) — общий с страницей фильтров - const { pingMap, requestPings } = usePing(); + const { pingMap, requestPings, cancelScope } = usePing(); const location = useLocation(); useEffect(() => { @@ -115,9 +115,20 @@ function EasySwitchManager() { if (gw.ip) pingTasks.push({ routerId, gatewayIp: gw.ip }); }); }); - requestPings(pingTasks); + requestPings(pingTasks, { scope: 'easy-switch' }); }, [location.pathname, servers, inventoryServers, requestPings]); + useEffect(() => { + if (location.pathname === '/easy-switch') return; + cancelScope('easy-switch'); + }, [location.pathname, cancelScope]); + + useEffect(() => { + return () => { + cancelScope('easy-switch'); + }; + }, [cancelScope]); + const loadDataWithParams = async (communities, inventory, ncGatewaysParam, signal) => { setLoading(true); setError(''); @@ -258,7 +269,7 @@ function EasySwitchManager() { if (gw.ip) pingTasks.push({ routerId, gatewayIp: gw.ip }); }); }); - requestPings(pingTasks); + requestPings(pingTasks, { scope: 'easy-switch' }); // Отладочная информация try { diff --git a/frontend/src/contexts/PingContext.jsx b/frontend/src/contexts/PingContext.jsx index 3ba02f7..93558af 100644 --- a/frontend/src/contexts/PingContext.jsx +++ b/frontend/src/contexts/PingContext.jsx @@ -16,6 +16,8 @@ const PING_CACHE_TTL_MS = 2 * 60 * 1000; // 2 минуты export function PingProvider({ children }) { const [pingMap, setPingMap] = useState({}); const inFlightRef = useRef(new Set()); + const controllersRef = useRef(new Map()); // key -> AbortController + const keyScopesRef = useRef(new Map()); // key -> scope const pingMapRef = useRef(pingMap); const timestampsRef = useRef({}); // key → Date.now() когда запись установлена pingMapRef.current = pingMap; @@ -44,8 +46,10 @@ export function PingProvider({ children }) { * Пропускаются ключи с валидным кешем (не истёк TTL) и уже запрашиваемые. * @param tasks Array<{ routerId, gatewayIp }> */ - const requestPings = useCallback((tasks) => { + const requestPings = useCallback((tasks, options = {}) => { if (!Array.isArray(tasks) || tasks.length === 0) return; + const scope = String(options?.scope || 'default'); + const force = Boolean(options?.force); const toRequest = []; tasks.forEach(({ routerId, gatewayIp }) => { @@ -53,40 +57,71 @@ export function PingProvider({ children }) { if (!routerId || !ip) return; const key = getKey(routerId, ip); if (!key) return; - if (isCacheValid(key)) return; + if (!force && isCacheValid(key)) return; if (inFlightRef.current.has(key)) return; inFlightRef.current.add(key); toRequest.push({ routerId, ip, key }); }); + // #region agent log + fetch('http://192.168.10.2:7343/ingest/aa002dd6-6968-4a35-b09d-f05302d83266',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'378b5f'},body:JSON.stringify({sessionId:'378b5f',runId:'post-fix',hypothesisId:'H6',location:'frontend/src/contexts/PingContext.jsx:requestPings',message:'Ping batch scheduled',data:{scope,count:toRequest.length,force},timestamp:Date.now()})}).catch(()=>{}); + // #endregion + toRequest.forEach(({ routerId, ip, key }) => { + const controller = new AbortController(); + controllersRef.current.set(key, controller); + keyScopesRef.current.set(key, scope); api .post('/mikrotik/ping', { serverId: routerId, gatewayIp: ip, count: PING_COUNT, - }) + }, { signal: controller.signal }) .then(({ data }) => { const value = typeof data?.avgMs === 'number' ? Math.round(data.avgMs) : null; timestampsRef.current[key] = Date.now(); setPingMap((prev) => ({ ...prev, [key]: value })); }) .catch((e) => { + if (e?.code === 'ERR_CANCELED' || e?.name === 'CanceledError' || e?.name === 'AbortError') { + // #region agent log + fetch('http://192.168.10.2:7343/ingest/aa002dd6-6968-4a35-b09d-f05302d83266',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'378b5f'},body:JSON.stringify({sessionId:'378b5f',runId:'post-fix',hypothesisId:'H6',location:'frontend/src/contexts/PingContext.jsx:ping-canceled',message:'Ping request canceled',data:{routerId:String(routerId||''),gatewayIp:String(ip||''),scope:String(keyScopesRef.current.get(key)||'')},timestamp:Date.now()})}).catch(()=>{}); + // #endregion + return; + } console.warn('PingContext: failed to fetch ping for', routerId, ip, e?.message || e); timestampsRef.current[key] = Date.now(); setPingMap((prev) => ({ ...prev, [key]: null })); }) .finally(() => { inFlightRef.current.delete(key); + controllersRef.current.delete(key); + keyScopesRef.current.delete(key); }); }); }, [isCacheValid]); + const cancelScope = useCallback((scope) => { + const scopeName = String(scope || ''); + if (!scopeName) return; + let canceled = 0; + controllersRef.current.forEach((controller, key) => { + if (keyScopesRef.current.get(key) === scopeName) { + controller.abort(); + canceled += 1; + } + }); + // #region agent log + fetch('http://192.168.10.2:7343/ingest/aa002dd6-6968-4a35-b09d-f05302d83266',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'378b5f'},body:JSON.stringify({sessionId:'378b5f',runId:'post-fix',hypothesisId:'H6',location:'frontend/src/contexts/PingContext.jsx:cancelScope',message:'Canceled ping scope',data:{scope:scopeName,canceled},timestamp:Date.now()})}).catch(()=>{}); + // #endregion + }, []); + const value = { pingMap, getPing, requestPings, getKey, + cancelScope, }; return (