From ac5b512a72464cb649ec5dc093da076ce739cd0b Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 1 Jun 2026 14:07:13 +0800 Subject: [PATCH] Reduce terminal chart update timeouts --- frontend/app/api/cities/detail-batch/route.ts | 2 +- .../__tests__/ssePatchArchitecture.test.ts | 2 +- .../scan-terminal/temperature-chart-logic.ts | 2 +- .../ops/__tests__/apiPerformanceTiming.test.ts | 5 +++++ tests/test_web_observability.py | 18 ++++++++++++++---- web/services/city_api.py | 18 +++++++++++++----- 6 files changed, 35 insertions(+), 12 deletions(-) diff --git a/frontend/app/api/cities/detail-batch/route.ts b/frontend/app/api/cities/detail-batch/route.ts index 8ad216f5..80e1409f 100644 --- a/frontend/app/api/cities/detail-batch/route.ts +++ b/frontend/app/api/cities/detail-batch/route.ts @@ -8,7 +8,7 @@ import { const API_BASE = process.env.POLYWEATHER_API_BASE_URL; const DETAIL_BATCH_PROXY_TIMEOUT_MS = Number( - process.env.POLYWEATHER_CITY_DETAIL_BATCH_PROXY_TIMEOUT_MS || "12000", + process.env.POLYWEATHER_CITY_DETAIL_BATCH_PROXY_TIMEOUT_MS || "15000", ); export async function GET(req: NextRequest) { diff --git a/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts index 716717ad..157049c5 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/ssePatchArchitecture.test.ts @@ -258,7 +258,7 @@ export function runTests() { "single-runway charts must not show the runway-detail toggle because aggregate and individual views are visually redundant", ); assert( - chartLogic.includes("HOURLY_DETAIL_REQUEST_TIMEOUT_MS = 12_000") && + chartLogic.includes("HOURLY_DETAIL_REQUEST_TIMEOUT_MS = 16_000") && chartLogic.includes("fetchCityDetailBatchWithTimeout") && chartLogic.includes("signal: controller.signal") && chartLogic.includes("controller.abort()"), diff --git a/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts b/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts index 6ce12965..a7b1ee2f 100644 --- a/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts +++ b/frontend/components/dashboard/scan-terminal/temperature-chart-logic.ts @@ -357,7 +357,7 @@ const HOURLY_FORCE_REFRESH_DEDUP_MS = 60_000; const _hourlyCache = new Map(); const _hourlyRequestCache = new Map>(); const MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS = 3; -const HOURLY_DETAIL_REQUEST_TIMEOUT_MS = 12_000; +const HOURLY_DETAIL_REQUEST_TIMEOUT_MS = 16_000; let _hourlyActiveDetailRequests = 0; const _hourlyDetailRequestQueue: Array<() => void> = []; const RUNWAY_LINE_COLORS = ["#00897b", "#d97706", "#7c3aed", "#0891b2", "#ea580c", "#64748b"]; diff --git a/frontend/components/ops/__tests__/apiPerformanceTiming.test.ts b/frontend/components/ops/__tests__/apiPerformanceTiming.test.ts index 23ac7683..150c1a69 100644 --- a/frontend/components/ops/__tests__/apiPerformanceTiming.test.ts +++ b/frontend/components/ops/__tests__/apiPerformanceTiming.test.ts @@ -41,6 +41,11 @@ export function runTests() { const detailBatchProxy = readFrontend("app", "api", "cities", "detail-batch", "route.ts"); assert.match(detailBatchProxy, /createProxyTimer\(req,\s*"city_detail_batch"\)/); assert.match(detailBatchProxy, /timing:\s*timer/); + assert.match( + detailBatchProxy, + /POLYWEATHER_CITY_DETAIL_BATCH_PROXY_TIMEOUT_MS\s*\|\|\s*"15000"/, + "city detail batch proxy should leave room for backend partial responses instead of aborting at the chart fetch deadline", + ); assert.match( detailBatchProxy, /fetchCache:\s*"no-store"/, diff --git a/tests/test_web_observability.py b/tests/test_web_observability.py index f91ba962..28bfaecd 100644 --- a/tests/test_web_observability.py +++ b/tests/test_web_observability.py @@ -853,13 +853,17 @@ def test_city_detail_batch_chart_scope_returns_only_chart_fields(monkeypatch): assert "ai_analysis" not in detail -def test_chart_detail_payload_avoids_threadpool_queue_and_reuses_short_cache(monkeypatch): +def test_chart_detail_payload_uses_threadpool_and_reuses_short_cache(monkeypatch): import asyncio build_calls = 0 + threadpool_calls = 0 - async def fail_threadpool(*_args, **_kwargs): - raise AssertionError("chart payload should not wait behind the shared threadpool") + async def fake_run_in_threadpool(fn, *args, **kwargs): + nonlocal threadpool_calls + threadpool_calls += 1 + await asyncio.sleep(0) + return fn(*args, **kwargs) def build_chart_detail(data, resolution): nonlocal build_calls @@ -873,7 +877,7 @@ def test_chart_detail_payload_avoids_threadpool_queue_and_reuses_short_cache(mon city_api._CITY_CHART_DETAIL_PAYLOAD_CACHE.clear() city_api._CITY_CHART_DETAIL_PAYLOAD_CACHE_TS.clear() monkeypatch.setenv("POLYWEATHER_CITY_DETAIL_PAYLOAD_CACHE_TTL_SEC", "20") - monkeypatch.setattr(city_api, "run_in_threadpool", fail_threadpool) + monkeypatch.setattr(city_api, "run_in_threadpool", fake_run_in_threadpool) monkeypatch.setattr(city_api.legacy_routes, "_build_city_chart_detail_payload", build_chart_detail) data = { @@ -888,6 +892,12 @@ def test_chart_detail_payload_avoids_threadpool_queue_and_reuses_short_cache(mon assert first == second assert first["resolution"] == "10m" assert build_calls == 1 + assert threadpool_calls == 1 + + +def test_city_detail_batch_partial_timeout_default_stays_below_proxy_budget(monkeypatch): + monkeypatch.delenv("POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS", raising=False) + assert city_api._city_detail_batch_partial_timeout_seconds() == 6.0 def test_city_detail_batch_endpoint_limits_backend_concurrency(monkeypatch): diff --git a/web/services/city_api.py b/web/services/city_api.py index 230ea265..3f93cf61 100644 --- a/web/services/city_api.py +++ b/web/services/city_api.py @@ -282,7 +282,11 @@ async def _build_city_chart_detail_payload( ) -> Dict[str, Any]: ttl = _city_detail_payload_cache_ttl() if ttl <= 0: - return legacy_routes._build_city_chart_detail_payload(data, resolution) + return await run_in_threadpool( + legacy_routes._build_city_chart_detail_payload, + data, + resolution, + ) key = _city_chart_detail_payload_cache_key(data, resolution) now_ts = time.time() @@ -292,7 +296,11 @@ async def _build_city_chart_detail_payload( if cached is not None and now_ts - cached_ts < ttl: return cached - payload = legacy_routes._build_city_chart_detail_payload(data, resolution) + payload = await run_in_threadpool( + legacy_routes._build_city_chart_detail_payload, + data, + resolution, + ) async with _CITY_CHART_DETAIL_PAYLOAD_LOCK: _CITY_CHART_DETAIL_PAYLOAD_CACHE[key] = payload @@ -709,11 +717,11 @@ def _city_detail_batch_concurrency() -> int: def _city_detail_batch_partial_timeout_seconds() -> Optional[float]: try: timeout_ms = int( - os.getenv("POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS", "8500") - or "8500" + os.getenv("POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS", "6000") + or "6000" ) except ValueError: - timeout_ms = 8500 + timeout_ms = 6000 if timeout_ms <= 0: return None return max(0.001, min(60.0, timeout_ms / 1000.0))