Reduce terminal chart update timeouts
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
|||||||
|
|
||||||
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
const API_BASE = process.env.POLYWEATHER_API_BASE_URL;
|
||||||
const DETAIL_BATCH_PROXY_TIMEOUT_MS = Number(
|
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) {
|
export async function GET(req: NextRequest) {
|
||||||
|
|||||||
@@ -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",
|
"single-runway charts must not show the runway-detail toggle because aggregate and individual views are visually redundant",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
chartLogic.includes("HOURLY_DETAIL_REQUEST_TIMEOUT_MS = 12_000") &&
|
chartLogic.includes("HOURLY_DETAIL_REQUEST_TIMEOUT_MS = 16_000") &&
|
||||||
chartLogic.includes("fetchCityDetailBatchWithTimeout") &&
|
chartLogic.includes("fetchCityDetailBatchWithTimeout") &&
|
||||||
chartLogic.includes("signal: controller.signal") &&
|
chartLogic.includes("signal: controller.signal") &&
|
||||||
chartLogic.includes("controller.abort()"),
|
chartLogic.includes("controller.abort()"),
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ const HOURLY_FORCE_REFRESH_DEDUP_MS = 60_000;
|
|||||||
const _hourlyCache = new Map<string, { ts: number; data: HourlyForecast }>();
|
const _hourlyCache = new Map<string, { ts: number; data: HourlyForecast }>();
|
||||||
const _hourlyRequestCache = new Map<string, Promise<HourlyForecast>>();
|
const _hourlyRequestCache = new Map<string, Promise<HourlyForecast>>();
|
||||||
const MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS = 3;
|
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;
|
let _hourlyActiveDetailRequests = 0;
|
||||||
const _hourlyDetailRequestQueue: Array<() => void> = [];
|
const _hourlyDetailRequestQueue: Array<() => void> = [];
|
||||||
const RUNWAY_LINE_COLORS = ["#00897b", "#d97706", "#7c3aed", "#0891b2", "#ea580c", "#64748b"];
|
const RUNWAY_LINE_COLORS = ["#00897b", "#d97706", "#7c3aed", "#0891b2", "#ea580c", "#64748b"];
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ export function runTests() {
|
|||||||
const detailBatchProxy = readFrontend("app", "api", "cities", "detail-batch", "route.ts");
|
const detailBatchProxy = readFrontend("app", "api", "cities", "detail-batch", "route.ts");
|
||||||
assert.match(detailBatchProxy, /createProxyTimer\(req,\s*"city_detail_batch"\)/);
|
assert.match(detailBatchProxy, /createProxyTimer\(req,\s*"city_detail_batch"\)/);
|
||||||
assert.match(detailBatchProxy, /timing:\s*timer/);
|
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(
|
assert.match(
|
||||||
detailBatchProxy,
|
detailBatchProxy,
|
||||||
/fetchCache:\s*"no-store"/,
|
/fetchCache:\s*"no-store"/,
|
||||||
|
|||||||
@@ -853,13 +853,17 @@ def test_city_detail_batch_chart_scope_returns_only_chart_fields(monkeypatch):
|
|||||||
assert "ai_analysis" not in detail
|
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
|
import asyncio
|
||||||
|
|
||||||
build_calls = 0
|
build_calls = 0
|
||||||
|
threadpool_calls = 0
|
||||||
|
|
||||||
async def fail_threadpool(*_args, **_kwargs):
|
async def fake_run_in_threadpool(fn, *args, **kwargs):
|
||||||
raise AssertionError("chart payload should not wait behind the shared threadpool")
|
nonlocal threadpool_calls
|
||||||
|
threadpool_calls += 1
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
return fn(*args, **kwargs)
|
||||||
|
|
||||||
def build_chart_detail(data, resolution):
|
def build_chart_detail(data, resolution):
|
||||||
nonlocal build_calls
|
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.clear()
|
||||||
city_api._CITY_CHART_DETAIL_PAYLOAD_CACHE_TS.clear()
|
city_api._CITY_CHART_DETAIL_PAYLOAD_CACHE_TS.clear()
|
||||||
monkeypatch.setenv("POLYWEATHER_CITY_DETAIL_PAYLOAD_CACHE_TTL_SEC", "20")
|
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)
|
monkeypatch.setattr(city_api.legacy_routes, "_build_city_chart_detail_payload", build_chart_detail)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
@@ -888,6 +892,12 @@ def test_chart_detail_payload_avoids_threadpool_queue_and_reuses_short_cache(mon
|
|||||||
assert first == second
|
assert first == second
|
||||||
assert first["resolution"] == "10m"
|
assert first["resolution"] == "10m"
|
||||||
assert build_calls == 1
|
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):
|
def test_city_detail_batch_endpoint_limits_backend_concurrency(monkeypatch):
|
||||||
|
|||||||
@@ -282,7 +282,11 @@ async def _build_city_chart_detail_payload(
|
|||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
ttl = _city_detail_payload_cache_ttl()
|
ttl = _city_detail_payload_cache_ttl()
|
||||||
if ttl <= 0:
|
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)
|
key = _city_chart_detail_payload_cache_key(data, resolution)
|
||||||
now_ts = time.time()
|
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:
|
if cached is not None and now_ts - cached_ts < ttl:
|
||||||
return cached
|
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:
|
async with _CITY_CHART_DETAIL_PAYLOAD_LOCK:
|
||||||
_CITY_CHART_DETAIL_PAYLOAD_CACHE[key] = payload
|
_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]:
|
def _city_detail_batch_partial_timeout_seconds() -> Optional[float]:
|
||||||
try:
|
try:
|
||||||
timeout_ms = int(
|
timeout_ms = int(
|
||||||
os.getenv("POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS", "8500")
|
os.getenv("POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS", "6000")
|
||||||
or "8500"
|
or "6000"
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
timeout_ms = 8500
|
timeout_ms = 6000
|
||||||
if timeout_ms <= 0:
|
if timeout_ms <= 0:
|
||||||
return None
|
return None
|
||||||
return max(0.001, min(60.0, timeout_ms / 1000.0))
|
return max(0.001, min(60.0, timeout_ms / 1000.0))
|
||||||
|
|||||||
Reference in New Issue
Block a user