UNPKG

multi-lane-manager

Version:

Nacos 泳道管理与请求路由组件

377 lines (375 loc) 16.4 kB
import { getConfig, getGlobalState, updateConfigPort } from "./chunk-WTX544FP.mjs"; import { logger } from "./chunk-XVCPZ7FX.mjs"; // src/utils/nacos.ts import axios from "axios"; var nacosInstanceCache = /* @__PURE__ */ new Map(); function getCacheKey(serviceName, targetLaneId) { return `${serviceName}:${targetLaneId}`; } function isCacheValid(cacheItem, ttl) { return Date.now() - cacheItem.timestamp < ttl; } function cleanExpiredCache(ttl) { const now = Date.now(); for (const [key, item] of nacosInstanceCache.entries()) { if (now - item.timestamp >= ttl) { nacosInstanceCache.delete(key); logger.debug(`\u{1F5D1}\uFE0F \u6E05\u7406\u8FC7\u671F\u7F13\u5B58: ${key}`); } } } async function registerServiceInstance(port) { const config = getConfig(); if (!config.isLaneEnabled) { logger.debug("\u{1F6AB} \u6CF3\u9053\u529F\u80FD\u672A\u542F\u7528\uFF0C\u8DF3\u8FC7\u670D\u52A1\u6CE8\u518C"); return true; } try { logger.debug(`\u{1F680} \u5F00\u59CB\u670D\u52A1\u6CE8\u518C\u6D41\u7A0B\uFF0C\u65F6\u95F4\u6233: ${(/* @__PURE__ */ new Date()).toISOString()}`); updateConfigPort(port); const registrationUrl = `${config.nacosUrl}/nacos/v1/ns/instance`; const params = new URLSearchParams(); params.append("serviceName", config.serviceName); params.append("ip", config.host); params.append("port", port.toString()); params.append("weight", "1.0"); params.append("healthy", "true"); params.append("metadata", JSON.stringify(config.metadata)); params.append("clusterName", config.currentLaneId || "DEFAULT"); params.append("ephemeral", "true"); params.append("ipDeleteTimeout", config.instanceTtl.toString()); params.append("instanceHeartBeatInterval", config.heartbeatInterval.toString()); params.append("instanceHeartBeatTimeOut", (config.heartbeatInterval * 2).toString()); logger.debug(`\u{1F4E4} \u53D1\u9001\u6CE8\u518C\u8BF7\u6C42\u5230 Nacos: ${registrationUrl}`); const response = await axios.post(registrationUrl, params, { headers: { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": "Nuxt3-LaneManager/1.0" }, timeout: config.registrationTimeout }); logger.debug(`\u{1F4E5} \u6CE8\u518C\u54CD\u5E94\u72B6\u6001\u7801: ${response.status}, \u6570\u636E: ${JSON.stringify(response.data)}`); if (response.status === 200 && response.data === "ok") { logger.success(`\u2705 \u670D\u52A1\u6CE8\u518C\u6210\u529F: ${config.serviceName}@${config.host}:${port} (\u6CF3\u9053: ${config.currentLaneId})`); const globalState = getGlobalState(); globalState._laneMgrRegistered = true; globalState._laneMgrPort = port; startHeartbeat(port); return true; } else { logger.error(`\u274C \u670D\u52A1\u6CE8\u518C\u5931\u8D25: ${response.data}`); return false; } } catch (error) { logger.error(`\u274C \u670D\u52A1\u6CE8\u518C\u9519\u8BEF: ${error instanceof Error ? error.message : String(error)}`); if (axios.isAxiosError(error) && error.response) { logger.error(`\u{1F534} HTTP\u72B6\u6001\u7801: ${error.response.status}, \u54CD\u5E94: ${JSON.stringify(error.response.data)}`); } return false; } } async function deregisterServiceInstance(port) { const config = getConfig(); if (!config.isLaneEnabled) { logger.debug("\u{1F6AB} \u6CF3\u9053\u529F\u80FD\u672A\u542F\u7528\uFF0C\u8DF3\u8FC7\u670D\u52A1\u6CE8\u9500"); return true; } try { stopHeartbeat(); const deregistrationUrl = `${config.nacosUrl}/nacos/v1/ns/instance`; const params = new URLSearchParams(); params.append("serviceName", config.serviceName); params.append("ip", config.host); params.append("port", port.toString()); params.append("ephemeral", "true"); logger.debug(`\u{1F4E4} \u53D1\u9001\u6CE8\u9500\u8BF7\u6C42\u5230 Nacos: ${deregistrationUrl}`); const response = await axios.delete(`${deregistrationUrl}?${params.toString()}`, { timeout: config.registrationTimeout }); logger.debug(`\u{1F4E5} \u6CE8\u9500\u54CD\u5E94\u72B6\u6001\u7801: ${response.status}, \u6570\u636E: ${JSON.stringify(response.data)}`); if (response.status === 200 && response.data === "ok") { logger.success(`\u2705 \u670D\u52A1\u6CE8\u9500\u6210\u529F: ${config.serviceName}@${config.host}:${port}`); const globalState = getGlobalState(); globalState._laneMgrRegistered = false; globalState._laneMgrPort = void 0; return true; } else { logger.error(`\u274C \u670D\u52A1\u6CE8\u9500\u5931\u8D25: ${response.data}`); return false; } } catch (error) { logger.error(`\u274C \u670D\u52A1\u6CE8\u9500\u9519\u8BEF: ${error instanceof Error ? error.message : String(error)}`); return false; } } async function sendHeartbeat(port, logDetails = false) { const config = getConfig(); if (!config.isLaneEnabled) { if (logDetails) { logger.debug("\u{1F6AB} \u6CF3\u9053\u529F\u80FD\u672A\u542F\u7528\uFF0C\u8DF3\u8FC7\u5FC3\u8DF3"); } return true; } try { const heartbeatUrl = `${config.nacosUrl}/nacos/v1/ns/instance/beat`; const params = new URLSearchParams(); params.append("serviceName", config.serviceName); params.append("ip", config.host); params.append("port", port.toString()); const now = /* @__PURE__ */ new Date(); const beat = { serviceName: config.serviceName, ip: config.host, port, weight: 1, metadata: { ...config.metadata, lastHeartbeat: now.toISOString(), // 添加心跳时间戳 heartbeatTimestamp: now.getTime().toString() // 添加时间戳(毫秒) }, cluster: config.currentLaneId, scheduled: false }; params.append("beat", JSON.stringify(beat)); if (logDetails) { logger.debug(`\u{1F493} \u53D1\u9001\u5FC3\u8DF3\u5230 Nacos: ${heartbeatUrl}`); } const response = await axios.put(`${heartbeatUrl}?${params.toString()}`, null, { headers: { "User-Agent": "Nuxt3-LaneManager/1.0" }, timeout: config.registrationTimeout / 2 // 心跳超时时间应该比注册短 }); if (logDetails) { logger.debug(`\u{1F4E5} \u5FC3\u8DF3\u54CD\u5E94\u72B6\u6001\u7801: ${response.status}, \u6570\u636E: ${JSON.stringify(response.data)}`); } if (response.status === 200) { if (logDetails) { logger.debug(`\u2705 \u5FC3\u8DF3\u6210\u529F: ${config.serviceName}@${config.host}:${port}`); } return true; } else { logger.warn(`\u26A0\uFE0F \u5FC3\u8DF3\u5931\u8D25: ${response.data}`); return false; } } catch (error) { logger.error(`\u274C \u5FC3\u8DF3\u9519\u8BEF: ${error instanceof Error ? error.message : String(error)}`); return false; } } function startHeartbeat(port) { const config = getConfig(); const globalState = getGlobalState(); stopHeartbeat(); globalState._laneMgrHeartbeatCount = 0; logger.info(`\u23F1\uFE0F \u542F\u52A8\u5FC3\u8DF3\u5B9A\u65F6\u5668\uFF0C\u95F4\u9694: ${config.heartbeatInterval}\u6BEB\u79D2\uFF0C\u6BCF30\u6B21\u5FC3\u8DF3\u6253\u5370\u4E00\u6B21\u65E5\u5FD7`); globalState._laneMgrHeartbeatTimer = setInterval(async () => { try { if (globalState._laneMgrHeartbeatCount !== void 0) { globalState._laneMgrHeartbeatCount++; } else { globalState._laneMgrHeartbeatCount = 1; } const shouldLogDetails = globalState._laneMgrHeartbeatCount % 30 === 0; await sendHeartbeat(port, shouldLogDetails); if (shouldLogDetails) { logger.info(`\u{1F493} \u5FC3\u8DF3\u8BA1\u6570: ${globalState._laneMgrHeartbeatCount}\uFF0C\u7EE7\u7EED\u4FDD\u6301\u8FDE\u63A5...`); } } catch (error) { logger.error(`\u274C \u5FC3\u8DF3\u5B9A\u65F6\u5668\u9519\u8BEF: ${error instanceof Error ? error.message : String(error)}`); } }, config.heartbeatInterval); } function stopHeartbeat() { const globalState = getGlobalState(); if (globalState._laneMgrHeartbeatTimer) { logger.info("\u23F9\uFE0F \u505C\u6B62\u5FC3\u8DF3\u5B9A\u65F6\u5668"); clearInterval(globalState._laneMgrHeartbeatTimer); globalState._laneMgrHeartbeatTimer = void 0; globalState._laneMgrHeartbeatCount = 0; } } async function fetchNacosLaneInstancesFromServer(serviceName, targetLaneId, sortByHeartbeat = true) { const config = getConfig(); logger.debug(`\u{1F4E1} \u76F4\u63A5\u4ECE Nacos \u670D\u52A1\u5668\u67E5\u8BE2\u5B9E\u4F8B\uFF0C\u670D\u52A1\u540D: ${serviceName}, \u76EE\u6807\u6CF3\u9053: ${targetLaneId}`); try { const nacosListUrl = `${config.nacosUrl}/nacos/v1/ns/instance/list`; const params = new URLSearchParams({ serviceName, healthyOnly: "true", // 只获取健康实例 groupName: config.nacosGroupName }); if (config.nacosNamespace) { params.append("namespaceId", config.nacosNamespace); } logger.debug(`\u{1F4E4} Nacos \u5B9E\u4F8B\u67E5\u8BE2 URL: ${nacosListUrl}?${params.toString()}`); const response = await axios.get(`${nacosListUrl}?${params.toString()}`, { timeout: config.registrationTimeout }); logger.debug(`\u{1F4E5} Nacos \u5B9E\u4F8B\u67E5\u8BE2\u54CD\u5E94\u72B6\u6001: ${response.status}`); if (response.data && response.data.hosts) { const filteredInstances = response.data.hosts.filter( (instance) => instance.healthy && instance.enabled && instance.metadata && instance.metadata.laneId === targetLaneId ).map( (instance) => { let lastHeartbeat = Date.now(); if (instance.metadata?.heartbeatTimestamp) { try { const timestamp = parseInt(instance.metadata.heartbeatTimestamp, 10); if (!isNaN(timestamp) && timestamp > 0) { lastHeartbeat = timestamp; } } catch (error) { logger.debug(`\u26A0\uFE0F \u89E3\u6790\u5B9E\u4F8B ${instance.ip}:${instance.port} \u6570\u5B57\u65F6\u95F4\u6233\u5931\u8D25: ${error}`); } } else if (instance.metadata?.lastHeartbeat) { try { const heartbeatTime = new Date(instance.metadata.lastHeartbeat).getTime(); if (!isNaN(heartbeatTime)) { lastHeartbeat = heartbeatTime; } } catch (error) { logger.debug(`\u26A0\uFE0F \u89E3\u6790\u5B9E\u4F8B ${instance.ip}:${instance.port} ISO\u65F6\u95F4\u6233\u5931\u8D25: ${error}`); } } if (instance.instanceHeartBeatInterval && !instance.metadata?.lastHeartbeat) { lastHeartbeat = Date.now() - instance.instanceHeartBeatInterval / 2; } return { ip: instance.ip, port: instance.port, serviceName: instance.serviceName, clusterName: instance.clusterName, ephemeral: instance.ephemeral, metadata: instance.metadata, status: "UP", lastHeartbeat, version: instance.metadata?.version || "unknown" }; } ); if (sortByHeartbeat) { filteredInstances.sort((a, b) => { const timeA = typeof a.lastHeartbeat === "number" ? a.lastHeartbeat : new Date(a.lastHeartbeat).getTime(); const timeB = typeof b.lastHeartbeat === "number" ? b.lastHeartbeat : new Date(b.lastHeartbeat).getTime(); return timeB - timeA; }); logger.debug(`\u{1F504} \u5DF2\u6309\u5FC3\u8DF3\u65F6\u95F4\u6392\u5E8F ${filteredInstances.length} \u4E2A\u5B9E\u4F8B`); if (filteredInstances.length > 0) { logger.debug(`\u{1F4CA} \u5B9E\u4F8B\u5FC3\u8DF3\u65F6\u95F4\u6392\u5E8F\u7ED3\u679C:`); filteredInstances.forEach((instance, index) => { const heartbeatTime = typeof instance.lastHeartbeat === "number" ? new Date(instance.lastHeartbeat).toISOString() : instance.lastHeartbeat; logger.debug(` ${index + 1}. ${instance.ip}:${instance.port} - \u5FC3\u8DF3\u65F6\u95F4: ${heartbeatTime}`); }); } } logger.info(`\u2705 \u4ECE\u670D\u52A1\u5668\u67E5\u8BE2\u5230 ${filteredInstances.length} \u4E2A\u5C5E\u4E8E\u6CF3\u9053 ${targetLaneId} \u7684\u5065\u5EB7\u5B9E\u4F8B${sortByHeartbeat ? "\uFF08\u5DF2\u6309\u5FC3\u8DF3\u65F6\u95F4\u6392\u5E8F\uFF09" : ""}`); return filteredInstances; } else { logger.warn(`\u26A0\uFE0F Nacos \u5B9E\u4F8B\u67E5\u8BE2\u54CD\u5E94\u683C\u5F0F\u4E0D\u6B63\u786E\u6216\u65E0\u5B9E\u4F8B\u5217\u8868:`, response.data); return []; } } catch (error) { logger.error(`\u274C Nacos \u5B9E\u4F8B\u67E5\u8BE2\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`); if (axios.isAxiosError(error) && error.response) { logger.error(`\u{1F534} HTTP\u72B6\u6001\u7801: ${error.response.status}, \u54CD\u5E94: ${JSON.stringify(error.response.data)}`); } return []; } } async function getNacosLaneInstances(serviceName, targetLaneId, sortByHeartbeat = true) { const config = getConfig(); const cacheKey = getCacheKey(serviceName, targetLaneId); const cacheTtl = config.nacosCacheTtl; console.log(`[multi-lane-manager] \u{1F50D} \u83B7\u53D6 Nacos \u5B9E\u4F8B\uFF0C\u670D\u52A1\u540D: ${serviceName}, \u76EE\u6807\u6CF3\u9053: ${targetLaneId}`); logger.debug(`\u{1F50D} \u83B7\u53D6 Nacos \u5B9E\u4F8B\uFF0C\u670D\u52A1\u540D: ${serviceName}, \u76EE\u6807\u6CF3\u9053: ${targetLaneId}, \u7F13\u5B58TTL: ${cacheTtl}ms`); cleanExpiredCache(cacheTtl); const cachedItem = nacosInstanceCache.get(cacheKey); if (cachedItem) { if (cachedItem.promise) { logger.debug(`\u23F3 \u53D1\u73B0\u6B63\u5728\u8FDB\u884C\u7684\u8BF7\u6C42\uFF0C\u7B49\u5F85\u5B8C\u6210: ${cacheKey}`); try { return await cachedItem.promise; } catch (error) { logger.warn(`\u26A0\uFE0F \u6B63\u5728\u8FDB\u884C\u7684\u8BF7\u6C42\u5931\u8D25\uFF0C\u79FB\u9664\u7F13\u5B58: ${cacheKey}, \u9519\u8BEF: ${error}`); nacosInstanceCache.delete(cacheKey); } } else if (isCacheValid(cachedItem, cacheTtl)) { logger.debug(`\u{1F4BE} \u4F7F\u7528\u7F13\u5B58\u6570\u636E: ${cacheKey}, \u7F13\u5B58\u65F6\u95F4: ${new Date(cachedItem.timestamp).toISOString()}`); return cachedItem.data; } else { logger.debug(`\u23F0 \u7F13\u5B58\u5DF2\u8FC7\u671F\uFF0C\u79FB\u9664: ${cacheKey}`); nacosInstanceCache.delete(cacheKey); } } logger.debug(`\u{1F310} \u521B\u5EFA\u65B0\u7684 Nacos \u67E5\u8BE2\u8BF7\u6C42: ${cacheKey}`); const requestPromise = fetchNacosLaneInstancesFromServer(serviceName, targetLaneId, sortByHeartbeat); nacosInstanceCache.set(cacheKey, { data: [], // 临时空数据 timestamp: Date.now(), promise: requestPromise }); try { const result = await requestPromise; nacosInstanceCache.set(cacheKey, { data: result, timestamp: Date.now() }); logger.debug(`\u2705 \u8BF7\u6C42\u5B8C\u6210\uFF0C\u5DF2\u7F13\u5B58\u7ED3\u679C: ${cacheKey}, \u5B9E\u4F8B\u6570\u91CF: ${result.length}`); return result; } catch (error) { nacosInstanceCache.delete(cacheKey); logger.error(`\u274C Nacos \u67E5\u8BE2\u8BF7\u6C42\u5931\u8D25\uFF0C\u79FB\u9664\u7F13\u5B58: ${cacheKey}, \u9519\u8BEF: ${error}`); throw error; } } function clearNacosInstanceCache() { const cacheSize = nacosInstanceCache.size; nacosInstanceCache.clear(); logger.info(`\u{1F5D1}\uFE0F \u5DF2\u6E05\u7406\u6240\u6709 Nacos \u5B9E\u4F8B\u7F13\u5B58\uFF0C\u6E05\u7406\u4E86 ${cacheSize} \u4E2A\u7F13\u5B58\u9879`); } function getNacosCacheStats() { const config = getConfig(); const cacheTtl = config.nacosCacheTtl; const now = Date.now(); let validItems = 0; let expiredItems = 0; let pendingRequests = 0; for (const item of nacosInstanceCache.values()) { if (item.promise) { pendingRequests++; } else if (now - item.timestamp < cacheTtl) { validItems++; } else { expiredItems++; } } return { totalItems: nacosInstanceCache.size, validItems, expiredItems, pendingRequests }; } export { registerServiceInstance, deregisterServiceInstance, sendHeartbeat, startHeartbeat, stopHeartbeat, getNacosLaneInstances, clearNacosInstanceCache, getNacosCacheStats }; //# sourceMappingURL=chunk-OFHMEPQ6.mjs.map