fix(hub): prevent monitor board stuck on slow aggregate polling
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -56,9 +56,10 @@ HUB_TRUST_LAN=true
|
|||||||
# 四实例网页登录(直链反代/IP:端口 访问时输入;中控点「打开实例」免输)
|
# 四实例网页登录(直链反代/IP:端口 访问时输入;中控点「打开实例」免输)
|
||||||
# 各 crypto_monitor_*/.env 统一:APP_USERNAME=... APP_PASSWORD=...
|
# 各 crypto_monitor_*/.env 统一:APP_USERNAME=... APP_PASSWORD=...
|
||||||
|
|
||||||
# 监控区 /api/monitor/board 聚合超时(秒,默认 agent 8 / flask 10)
|
# 监控区 /api/monitor/board 聚合超时(秒,默认 agent 8 / flask 10 / board 45)
|
||||||
# HUB_AGENT_TIMEOUT=8
|
# HUB_AGENT_TIMEOUT=8
|
||||||
# HUB_FLASK_TIMEOUT=10
|
# HUB_FLASK_TIMEOUT=10
|
||||||
|
# HUB_BOARD_TIMEOUT=45
|
||||||
# 为 false 时不拉各实例 /api/price_snapshot(关键位门控简化为「-」,首屏明显更快)
|
# 为 false 时不拉各实例 /api/price_snapshot(关键位门控简化为「-」,首屏明显更快)
|
||||||
# HUB_BOARD_KEY_PRICES=true
|
# HUB_BOARD_KEY_PRICES=true
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ DIR = Path(__file__).resolve().parent
|
|||||||
HUB_BUILD = "20260528-hub-market"
|
HUB_BUILD = "20260528-hub-market"
|
||||||
HUB_AGENT_TIMEOUT = float(os.getenv("HUB_AGENT_TIMEOUT", "8"))
|
HUB_AGENT_TIMEOUT = float(os.getenv("HUB_AGENT_TIMEOUT", "8"))
|
||||||
HUB_FLASK_TIMEOUT = float(os.getenv("HUB_FLASK_TIMEOUT", "10"))
|
HUB_FLASK_TIMEOUT = float(os.getenv("HUB_FLASK_TIMEOUT", "10"))
|
||||||
|
HUB_BOARD_TIMEOUT = float(os.getenv("HUB_BOARD_TIMEOUT", "45"))
|
||||||
_board_key_prices_raw = (os.getenv("HUB_BOARD_KEY_PRICES", "true") or "").strip().lower()
|
_board_key_prices_raw = (os.getenv("HUB_BOARD_KEY_PRICES", "true") or "").strip().lower()
|
||||||
HUB_BOARD_KEY_PRICES = _board_key_prices_raw in ("1", "true", "yes", "on")
|
HUB_BOARD_KEY_PRICES = _board_key_prices_raw in ("1", "true", "yes", "on")
|
||||||
|
|
||||||
@@ -748,8 +749,7 @@ async def _assemble_board_row(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/monitor/board")
|
async def _build_monitor_board_payload() -> dict:
|
||||||
async def api_monitor_board():
|
|
||||||
exchanges = enabled_exchanges()
|
exchanges = enabled_exchanges()
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
agent_rows = await asyncio.gather(
|
agent_rows = await asyncio.gather(
|
||||||
@@ -767,6 +767,28 @@ async def api_monitor_board():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/monitor/board")
|
||||||
|
async def api_monitor_board():
|
||||||
|
try:
|
||||||
|
return await asyncio.wait_for(_build_monitor_board_payload(), timeout=HUB_BOARD_TIMEOUT)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return JSONResponse(
|
||||||
|
{
|
||||||
|
"ok": False,
|
||||||
|
"rows": [],
|
||||||
|
"error": "board_timeout",
|
||||||
|
"msg": (
|
||||||
|
f"监控聚合超过 {int(HUB_BOARD_TIMEOUT)} 秒。"
|
||||||
|
"请检查子代理/Flask,或设 HUB_BOARD_KEY_PRICES=false、缩短 HUB_FLASK_TIMEOUT"
|
||||||
|
),
|
||||||
|
"updated_at": __import__("datetime").datetime.now().isoformat(
|
||||||
|
timespec="seconds"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
status_code=504,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _require_hub_logged_in(request: Request) -> None:
|
def _require_hub_logged_in(request: Request) -> None:
|
||||||
if password_required() and not validate_session_token(request.cookies.get(SESSION_COOKIE)):
|
if password_required() and not validate_session_token(request.cookies.get(SESSION_COOKIE)):
|
||||||
raise HTTPException(status_code=401, detail="未登录中控")
|
raise HTTPException(status_code=401, detail="未登录中控")
|
||||||
|
|||||||
@@ -1408,6 +1408,14 @@ button.btn-sm {
|
|||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.board-loading-sub {
|
||||||
|
margin: 12px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--muted);
|
||||||
|
max-width: 36rem;
|
||||||
|
}
|
||||||
|
|
||||||
.board-loading {
|
.board-loading {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -8,8 +8,10 @@
|
|||||||
let expandedExchangeId = sessionStorage.getItem("hub_expanded_ex") || "";
|
let expandedExchangeId = sessionStorage.getItem("hub_expanded_ex") || "";
|
||||||
const HUB_MONITOR_BOARD_CACHE_KEY = "hub_monitor_board_v1";
|
const HUB_MONITOR_BOARD_CACHE_KEY = "hub_monitor_board_v1";
|
||||||
const HUB_MONITOR_CACHE_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
const HUB_MONITOR_CACHE_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
||||||
let monitorBoardFetchSeq = 0;
|
const HUB_MONITOR_FETCH_TIMEOUT_MS = 55000;
|
||||||
let lastMonitorBoardUpdatedAt = "";
|
let lastMonitorBoardUpdatedAt = "";
|
||||||
|
let monitorBoardInFlight = false;
|
||||||
|
let monitorBoardSlowHintTimer = null;
|
||||||
|
|
||||||
async function apiFetch(url, opts) {
|
async function apiFetch(url, opts) {
|
||||||
const r = await fetch(url, opts);
|
const r = await fetch(url, opts);
|
||||||
@@ -344,10 +346,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stopMonitorPoll() {
|
function stopMonitorPoll() {
|
||||||
|
clearTimeout(monitorTimer);
|
||||||
clearInterval(monitorTimer);
|
clearInterval(monitorTimer);
|
||||||
monitorTimer = null;
|
monitorTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearMonitorBoardSlowHint() {
|
||||||
|
if (monitorBoardSlowHintTimer) {
|
||||||
|
clearTimeout(monitorBoardSlowHintTimer);
|
||||||
|
monitorBoardSlowHintTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleMonitorBoardSlowHint(box) {
|
||||||
|
clearMonitorBoardSlowHint();
|
||||||
|
if (!box) return;
|
||||||
|
monitorBoardSlowHintTimer = setTimeout(() => {
|
||||||
|
if (lastMonitorRows.length) return;
|
||||||
|
const el = box.querySelector(".board-loading");
|
||||||
|
if (!el) return;
|
||||||
|
const sub = el.querySelector(".board-loading-sub");
|
||||||
|
if (sub) {
|
||||||
|
sub.textContent =
|
||||||
|
"聚合较慢(四所子代理 + Flask)。可检查 PM2、或设 HUB_BOARD_KEY_PRICES=false 加速;下方超时后会提示错误。";
|
||||||
|
}
|
||||||
|
}, 12000);
|
||||||
|
}
|
||||||
|
|
||||||
function saveMonitorBoardCache(rows, updatedAt) {
|
function saveMonitorBoardCache(rows, updatedAt) {
|
||||||
try {
|
try {
|
||||||
sessionStorage.setItem(
|
sessionStorage.setItem(
|
||||||
@@ -414,13 +439,22 @@
|
|||||||
renderMonitorGrid(rows || []);
|
renderMonitorGrid(rows || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function scheduleNextMonitorPoll() {
|
||||||
|
stopMonitorPoll();
|
||||||
|
if (!document.getElementById("auto-monitor")?.checked) return;
|
||||||
|
if (currentPage() !== "monitor") return;
|
||||||
|
monitorTimer = setTimeout(async () => {
|
||||||
|
await loadMonitorBoard({ background: true });
|
||||||
|
scheduleNextMonitorPoll();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
function startMonitorPoll() {
|
function startMonitorPoll() {
|
||||||
stopMonitorPoll();
|
stopMonitorPoll();
|
||||||
const hadCache = restoreMonitorBoardFromCache();
|
const hadCache = restoreMonitorBoardFromCache();
|
||||||
loadMonitorBoard({ background: hadCache });
|
void loadMonitorBoard({ background: hadCache }).finally(() => {
|
||||||
if (document.getElementById("auto-monitor").checked) {
|
scheduleNextMonitorPoll();
|
||||||
monitorTimer = setInterval(() => loadMonitorBoard({ background: true }), 5000);
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
@@ -576,30 +610,44 @@
|
|||||||
async function loadMonitorBoard(opts) {
|
async function loadMonitorBoard(opts) {
|
||||||
const options = opts || {};
|
const options = opts || {};
|
||||||
const background = !!options.background;
|
const background = !!options.background;
|
||||||
|
const force = !!options.force;
|
||||||
|
if (monitorBoardInFlight && background && !force) return;
|
||||||
const box = document.getElementById("monitor-grid");
|
const box = document.getElementById("monitor-grid");
|
||||||
const seq = ++monitorBoardFetchSeq;
|
|
||||||
const showLoading = !background && !lastMonitorRows.length;
|
const showLoading = !background && !lastMonitorRows.length;
|
||||||
if (showLoading && box) {
|
if (showLoading && box) {
|
||||||
box.innerHTML =
|
box.innerHTML =
|
||||||
'<div class="board-loading"><span class="board-loading-spin" aria-hidden="true"></span>正在聚合四所数据…</div>';
|
'<div class="board-loading"><span class="board-loading-spin" aria-hidden="true"></span>正在聚合四所数据…<p class="board-loading-sub"></p></div>';
|
||||||
|
scheduleMonitorBoardSlowHint(box);
|
||||||
} else if (background && lastMonitorRows.length) {
|
} else if (background && lastMonitorRows.length) {
|
||||||
applyMonitorBoardUi(lastMonitorRows, null, { stale: true });
|
applyMonitorBoardUi(lastMonitorRows, null, { stale: true });
|
||||||
}
|
}
|
||||||
|
monitorBoardInFlight = true;
|
||||||
|
const ctrl = new AbortController();
|
||||||
|
const fetchTimer = setTimeout(() => ctrl.abort(), HUB_MONITOR_FETCH_TIMEOUT_MS);
|
||||||
try {
|
try {
|
||||||
const r = await apiFetch("/api/monitor/board");
|
const r = await apiFetch("/api/monitor/board", { signal: ctrl.signal });
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
if (seq !== monitorBoardFetchSeq) return;
|
if (!r.ok) {
|
||||||
|
throw new Error(data.msg || data.detail || `HTTP ${r.status}`);
|
||||||
|
}
|
||||||
lastMonitorRows = data.rows || [];
|
lastMonitorRows = data.rows || [];
|
||||||
saveMonitorBoardCache(lastMonitorRows, data.updated_at);
|
saveMonitorBoardCache(lastMonitorRows, data.updated_at);
|
||||||
applyMonitorBoardUi(lastMonitorRows, data.updated_at, { stale: false });
|
applyMonitorBoardUi(lastMonitorRows, data.updated_at, { stale: false });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (seq !== monitorBoardFetchSeq) return;
|
const msg =
|
||||||
|
e && e.name === "AbortError"
|
||||||
|
? "聚合超时(约 55 秒)。请检查子代理/Flask 是否运行,或关闭 HUB_BOARD_KEY_PRICES 加速"
|
||||||
|
: String(e);
|
||||||
if (background && lastMonitorRows.length) {
|
if (background && lastMonitorRows.length) {
|
||||||
showToast("监控数据刷新失败,仍显示上次缓存", true);
|
showToast("监控数据刷新失败,仍显示上次缓存", true);
|
||||||
applyMonitorBoardUi(lastMonitorRows, null, { stale: false });
|
applyMonitorBoardUi(lastMonitorRows, null, { stale: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (box) box.innerHTML = `<div class="err">${esc(e)}</div>`;
|
if (box) box.innerHTML = `<div class="err">${esc(msg)}</div>`;
|
||||||
|
} finally {
|
||||||
|
clearTimeout(fetchTimer);
|
||||||
|
clearMonitorBoardSlowHint();
|
||||||
|
monitorBoardInFlight = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1870,7 +1918,8 @@
|
|||||||
location.href = "/login";
|
location.href = "/login";
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById("btn-monitor-refresh").onclick = loadMonitorBoard;
|
document.getElementById("btn-monitor-refresh").onclick = () =>
|
||||||
|
loadMonitorBoard({ force: true, background: !!lastMonitorRows.length });
|
||||||
document.getElementById("auto-monitor").onchange = startMonitorPoll;
|
document.getElementById("auto-monitor").onchange = startMonitorPoll;
|
||||||
document.getElementById("btn-close-all").onclick = closeAll;
|
document.getElementById("btn-close-all").onclick = closeAll;
|
||||||
document.getElementById("btn-settings-save").onclick = saveSettings;
|
document.getElementById("btn-settings-save").onclick = saveSettings;
|
||||||
|
|||||||
@@ -245,6 +245,6 @@
|
|||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
<script src="/assets/chart.js?v=20260528-hub-no-30m"></script>
|
<script src="/assets/chart.js?v=20260528-hub-no-30m"></script>
|
||||||
<script src="/assets/app.js?v=20260603-hub-monitor-cache"></script>
|
<script src="/assets/app.js?v=20260603-hub-board-poll-fix"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user