From 3a1b2dfd6de08695e80a766f8e9e0df70bb10e92 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Tue, 9 Jun 2026 22:47:23 +0800 Subject: [PATCH] Avoid caching failed scan snapshots --- docker-compose.yml | 2 +- frontend/app/api/scan/terminal/route.ts | 10 +++++++- .../__tests__/proxyCachePolicy.test.ts | 24 +++++++++++++++++++ frontend/lib/proxy-cache-policy.ts | 21 ++++++++++++++-- tests/test_deployment_runtime_config.py | 6 +---- 5 files changed, 54 insertions(+), 9 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3f04e6dd..c6b90153 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -112,7 +112,7 @@ services: POLYWEATHER_OBSERVATION_COLLECTOR_INITIAL_DELAY_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_INITIAL_DELAY_SEC:-5} POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC:-300} POLYWEATHER_OBSERVATION_COLLECTOR_TICK_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_TICK_SEC:-30} - POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: ${POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED:-true} + POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'true' POLYWEATHER_SERVICE_ROLE: web UVICORN_WORKERS: ${UVICORN_WORKERS:-2} healthcheck: diff --git a/frontend/app/api/scan/terminal/route.ts b/frontend/app/api/scan/terminal/route.ts index 956f6e30..34e17ae8 100644 --- a/frontend/app/api/scan/terminal/route.ts +++ b/frontend/app/api/scan/terminal/route.ts @@ -1,6 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { proxyBackendJsonGet } from "@/lib/api-proxy"; -import { buildForceRefreshProxyCachePolicy } from "@/lib/proxy-cache-policy"; +import { + buildForceRefreshProxyCachePolicy, + buildScanTerminalResponseCacheControl, +} from "@/lib/proxy-cache-policy"; import { DASHBOARD_REFRESH_POLICY_SEC } from "@/lib/refresh-policy"; import { createProxyTimer, @@ -64,6 +67,11 @@ export async function GET(req: NextRequest) { try { return await proxyBackendJsonGet(req, { cacheControl: cachePolicy.responseCacheControl, + cacheControlForData: (data) => + buildScanTerminalResponseCacheControl( + data, + cachePolicy.responseCacheControl, + ), fetchCache: cachePolicy.fetchMode === "no-store" ? "no-store" : undefined, publicMessage: "Failed to fetch scan terminal data", diff --git a/frontend/components/dashboard/scan-terminal/__tests__/proxyCachePolicy.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/proxyCachePolicy.test.ts index eeb2d520..79db674a 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/proxyCachePolicy.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/proxyCachePolicy.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; import { + buildScanTerminalResponseCacheControl, buildCityDetailProxyCachePolicy, buildForceRefreshProxyCachePolicy, isForceRefreshValue, @@ -26,6 +27,24 @@ export function runTests() { const scanForced = buildForceRefreshProxyCachePolicy("true", 10); assert.equal(scanForced.fetchMode, "no-store"); + const normalScanCache = "public, max-age=0, s-maxage=300, stale-while-revalidate=900"; + assert.equal( + buildScanTerminalResponseCacheControl({ status: "ready", stale: false }, normalScanCache), + normalScanCache, + ); + assert.equal( + buildScanTerminalResponseCacheControl({ status: "failed", stale: false }, normalScanCache), + "no-store, max-age=0", + ); + assert.equal( + buildScanTerminalResponseCacheControl({ status: "partial", stale: false }, normalScanCache), + "no-store, max-age=0", + ); + assert.equal( + buildScanTerminalResponseCacheControl({ status: "ready", stale: true }, normalScanCache), + "no-store, max-age=0", + ); + const scanTerminalProxySource = fs.readFileSync( path.join(process.cwd(), "app", "api", "scan", "terminal", "route.ts"), "utf8", @@ -40,6 +59,11 @@ export function runTests() { /buildForceRefreshProxyCachePolicy\(forceRefresh,\s*10\)/, "scan terminal proxy must not use the old 10 second edge cache because it over-drives the slow scan endpoint", ); + assert.match( + scanTerminalProxySource, + /cacheControlForData:\s*\(data\)\s*=>\s*buildScanTerminalResponseCacheControl/, + "scan terminal proxy must not CDN-cache failed, stale, or partial business payloads", + ); const scanTerminalClientSource = fs.readFileSync( path.join( diff --git a/frontend/lib/proxy-cache-policy.ts b/frontend/lib/proxy-cache-policy.ts index 2c9c5807..a01981ad 100644 --- a/frontend/lib/proxy-cache-policy.ts +++ b/frontend/lib/proxy-cache-policy.ts @@ -4,6 +4,8 @@ export type ProxyCachePolicy = { revalidateSeconds?: number; }; +const NO_STORE_CACHE_CONTROL = "no-store, max-age=0"; + export function isForceRefreshValue(value: string | null | undefined) { return String(value || "").trim().toLowerCase() === "true"; } @@ -15,7 +17,7 @@ export function buildForceRefreshProxyCachePolicy( if (isForceRefreshValue(forceRefresh)) { return { fetchMode: "no-store", - responseCacheControl: "no-store, max-age=0", + responseCacheControl: NO_STORE_CACHE_CONTROL, }; } return { @@ -35,7 +37,7 @@ export function buildCityDetailProxyCachePolicy( if (isForceRefreshValue(forceRefresh)) { return { fetchMode: "no-store", - responseCacheControl: "no-store, max-age=0", + responseCacheControl: NO_STORE_CACHE_CONTROL, }; } return { @@ -47,3 +49,18 @@ export function buildCityDetailProxyCachePolicy( revalidateSeconds, }; } + +export function buildScanTerminalResponseCacheControl( + data: unknown, + readyCacheControl: string, +) { + if (!data || typeof data !== "object") { + return NO_STORE_CACHE_CONTROL; + } + const payload = data as { stale?: unknown; status?: unknown }; + const status = String(payload.status || "").trim().toLowerCase(); + if (payload.stale === true || (status && status !== "ready")) { + return NO_STORE_CACHE_CONTROL; + } + return readyCacheControl; +} diff --git a/tests/test_deployment_runtime_config.py b/tests/test_deployment_runtime_config.py index 626d4790..946c6daa 100644 --- a/tests/test_deployment_runtime_config.py +++ b/tests/test_deployment_runtime_config.py @@ -91,11 +91,7 @@ def test_docker_compose_isolates_collector_from_web_and_bot_services(): assert "POLYWEATHER_SERVICE_ROLE: bot" in compose assert "POLYWEATHER_SERVICE_ROLE: collector" in collector_block assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'" in bot_block - assert ( - "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: " - "${POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED:-true}" - in web_block - ) + assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'true'" in web_block assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'" in collector_block assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in bot_block assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in web_block