Add production cache warmer
This commit is contained in:
@@ -207,6 +207,37 @@ warm_public_route() {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
wait_for_scan_terminal_ready() {
|
||||||
|
local name="$1"
|
||||||
|
local url="$2"
|
||||||
|
local timeout="${3:-35}"
|
||||||
|
local attempts="${4:-8}"
|
||||||
|
local delay="${5:-5}"
|
||||||
|
local output=""
|
||||||
|
local compact=""
|
||||||
|
local status=""
|
||||||
|
|
||||||
|
for i in $(seq 1 "$attempts"); do
|
||||||
|
if output=$(curl -fsS --max-time "$timeout" "$url" 2>&1); then
|
||||||
|
compact="$(printf '%s' "$output" | tr -d '\n\r\t ')"
|
||||||
|
if printf '%s' "$compact" | grep -q '"status":"ready"'; then
|
||||||
|
echo "✅ $name ready after attempt $i/$attempts"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
status="$(printf '%s' "$compact" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p' | head -n 1)"
|
||||||
|
echo " $name not ready attempt $i/$attempts status=${status:-unknown}"
|
||||||
|
else
|
||||||
|
echo " $name request failed attempt $i/$attempts ($output)"
|
||||||
|
fi
|
||||||
|
if [ "$i" != "$attempts" ]; then
|
||||||
|
sleep "$delay"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "❌ $name did not return status=ready"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
read_env_file_value() {
|
read_env_file_value() {
|
||||||
local key="$1"
|
local key="$1"
|
||||||
if [ ! -f ".env" ]; then
|
if [ ! -f ".env" ]; then
|
||||||
@@ -278,12 +309,16 @@ fi
|
|||||||
echo "Updating observation collector..."
|
echo "Updating observation collector..."
|
||||||
compose_up_retry "observation collector" -d --no-deps polyweather_collector
|
compose_up_retry "observation collector" -d --no-deps polyweather_collector
|
||||||
|
|
||||||
|
echo "Updating cache warmer..."
|
||||||
|
compose_up_retry "cache warmer" -d --no-deps polyweather_warmer
|
||||||
|
|
||||||
echo "Updating frontend..."
|
echo "Updating frontend..."
|
||||||
compose_up_retry "frontend" -d --no-deps polyweather_frontend
|
compose_up_retry "frontend" -d --no-deps polyweather_frontend
|
||||||
|
|
||||||
echo "Waiting for frontend..."
|
echo "Waiting for frontend..."
|
||||||
wait_for_local_service "frontend root" "http://127.0.0.1:3001/" 5 40 2 || FAILED_FRONTEND=1
|
wait_for_local_service "frontend root" "http://127.0.0.1:3001/" 5 40 2 || FAILED_FRONTEND=1
|
||||||
wait_for_local_service "frontend terminal" "http://127.0.0.1:3001/terminal" 10 20 2 || FAILED_FRONTEND=1
|
wait_for_local_service "frontend terminal" "http://127.0.0.1:3001/terminal" 10 20 2 || FAILED_FRONTEND=1
|
||||||
|
wait_for_scan_terminal_ready "scan terminal snapshot" "http://127.0.0.1:3001/api/scan/terminal" 35 8 5 || FAILED_FRONTEND=1
|
||||||
FAILED_FRONTEND="${FAILED_FRONTEND:-0}"
|
FAILED_FRONTEND="${FAILED_FRONTEND:-0}"
|
||||||
if [ "$FAILED_FRONTEND" = "1" ]; then
|
if [ "$FAILED_FRONTEND" = "1" ]; then
|
||||||
echo "❌ Frontend did not become healthy"
|
echo "❌ Frontend did not become healthy"
|
||||||
@@ -292,6 +327,7 @@ if [ "$FAILED_FRONTEND" = "1" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
warm_public_route "terminal" "https://polyweather.top/terminal" 20 4 3
|
warm_public_route "terminal" "https://polyweather.top/terminal" 20 4 3
|
||||||
|
warm_public_route "scan terminal" "https://polyweather.top/api/scan/terminal" 35 3 2
|
||||||
warm_public_route "auth snapshot" "https://polyweather.top/api/auth/me?prefer_snapshot=1" 10 3 2
|
warm_public_route "auth snapshot" "https://polyweather.top/api/auth/me?prefer_snapshot=1" 10 3 2
|
||||||
warm_public_route "local cities recent stats" "http://127.0.0.1:8000/api/cities?refresh_deb_recent=1" 15 2 2
|
warm_public_route "local cities recent stats" "http://127.0.0.1:8000/api/cities?refresh_deb_recent=1" 15 2 2
|
||||||
warm_public_route "cities" "https://polyweather.top/api/cities" 20 3 2
|
warm_public_route "cities" "https://polyweather.top/api/cities" 20 3 2
|
||||||
|
|||||||
+55
-5
@@ -2,7 +2,7 @@ services:
|
|||||||
polyweather_redis:
|
polyweather_redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: polyweather_redis
|
container_name: polyweather_redis
|
||||||
command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy noeviction
|
command: redis-server --appendonly yes --maxmemory ${POLYWEATHER_REDIS_MAXMEMORY:-512mb} --maxmemory-policy noeviction
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
interval: 10s
|
interval: 10s
|
||||||
@@ -100,9 +100,13 @@ services:
|
|||||||
env_file: *id001
|
env_file: *id001
|
||||||
environment:
|
environment:
|
||||||
POLYWEATHER_REDIS_URL: ${POLYWEATHER_REDIS_URL:-redis://polyweather_redis:6379/0}
|
POLYWEATHER_REDIS_URL: ${POLYWEATHER_REDIS_URL:-redis://polyweather_redis:6379/0}
|
||||||
|
POLYWEATHER_EVENT_STORE: ${POLYWEATHER_EVENT_STORE:-redis}
|
||||||
|
POLYWEATHER_REDIS_REQUIRED: ${POLYWEATHER_REDIS_REQUIRED:-true}
|
||||||
|
POLYWEATHER_REDIS_STREAM_MAXLEN: ${POLYWEATHER_REDIS_STREAM_MAXLEN:-100000}
|
||||||
|
POLYWEATHER_SCAN_TERMINAL_REDIS_CACHE_ENABLED: ${POLYWEATHER_SCAN_TERMINAL_REDIS_CACHE_ENABLED:-true}
|
||||||
POLYWEATHER_COLLECTOR_PATCH_ENDPOINT: ''
|
POLYWEATHER_COLLECTOR_PATCH_ENDPOINT: ''
|
||||||
POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY:-1}
|
POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY:-2}
|
||||||
POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY:-1}
|
POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY:-2}
|
||||||
POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS:-3000}
|
POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS:-3000}
|
||||||
POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS:-8000}
|
POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS:-8000}
|
||||||
POLYWEATHER_OBSERVATION_COLLECTOR_AMOS_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_AMOS_SEC:-60}
|
POLYWEATHER_OBSERVATION_COLLECTOR_AMOS_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_AMOS_SEC:-60}
|
||||||
@@ -114,8 +118,8 @@ services:
|
|||||||
POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC:-300}
|
POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC:-300}
|
||||||
POLYWEATHER_OBSERVATION_COLLECTOR_TICK_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_TICK_SEC:-30}
|
POLYWEATHER_OBSERVATION_COLLECTOR_TICK_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_TICK_SEC:-30}
|
||||||
POLYWEATHER_SCAN_TERMINAL_BUILD_TIMEOUT_SEC: '30'
|
POLYWEATHER_SCAN_TERMINAL_BUILD_TIMEOUT_SEC: '30'
|
||||||
POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS: ${POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS:-1}
|
POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS: ${POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS:-4}
|
||||||
POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'
|
POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: ${POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED:-false}
|
||||||
POLYWEATHER_SERVICE_ROLE: web
|
POLYWEATHER_SERVICE_ROLE: web
|
||||||
UVICORN_WORKERS: ${UVICORN_WORKERS:-2}
|
UVICORN_WORKERS: ${UVICORN_WORKERS:-2}
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -164,6 +168,7 @@ services:
|
|||||||
POLYWEATHER_OBSERVATION_COLLECTOR_INITIAL_DELAY_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_INITIAL_DELAY_SEC:-15}
|
POLYWEATHER_OBSERVATION_COLLECTOR_INITIAL_DELAY_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_INITIAL_DELAY_SEC:-15}
|
||||||
POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC:-300}
|
POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC:-300}
|
||||||
POLYWEATHER_OBSERVATION_COLLECTOR_TICK_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_TICK_SEC:-30}
|
POLYWEATHER_OBSERVATION_COLLECTOR_TICK_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_TICK_SEC:-30}
|
||||||
|
POLYWEATHER_OBSERVATION_COLLECTOR_CACHE_REFRESH_WORKERS: ${POLYWEATHER_OBSERVATION_COLLECTOR_CACHE_REFRESH_WORKERS:-2}
|
||||||
POLYWEATHER_REDIS_URL: ${POLYWEATHER_REDIS_URL:-redis://polyweather_redis:6379/0}
|
POLYWEATHER_REDIS_URL: ${POLYWEATHER_REDIS_URL:-redis://polyweather_redis:6379/0}
|
||||||
POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'
|
POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'
|
||||||
POLYWEATHER_SERVICE_ROLE: collector
|
POLYWEATHER_SERVICE_ROLE: collector
|
||||||
@@ -186,6 +191,51 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/var/lib/polyweather
|
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/var/lib/polyweather
|
||||||
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/app/data
|
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/app/data
|
||||||
|
polyweather_warmer:
|
||||||
|
command: python -m web.cache_warmer_worker
|
||||||
|
container_name: polyweather_warmer
|
||||||
|
depends_on:
|
||||||
|
polyweather_redis:
|
||||||
|
condition: service_healthy
|
||||||
|
polyweather_web:
|
||||||
|
condition: service_started
|
||||||
|
logging:
|
||||||
|
driver: "json-file"
|
||||||
|
options:
|
||||||
|
max-size: "50m"
|
||||||
|
max-file: "3"
|
||||||
|
cpus: ${POLYWEATHER_WARMER_CPUS:-0.75}
|
||||||
|
env_file: *id001
|
||||||
|
environment:
|
||||||
|
POLYWEATHER_EVENT_STORE: ${POLYWEATHER_EVENT_STORE:-redis}
|
||||||
|
POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'
|
||||||
|
POLYWEATHER_REDIS_URL: ${POLYWEATHER_REDIS_URL:-redis://polyweather_redis:6379/0}
|
||||||
|
POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'
|
||||||
|
POLYWEATHER_SERVICE_ROLE: warmer
|
||||||
|
POLYWEATHER_WARMER_CITY_BATCH_SIZE: ${POLYWEATHER_WARMER_CITY_BATCH_SIZE:-8}
|
||||||
|
POLYWEATHER_WARMER_CITY_INTERVAL_SEC: ${POLYWEATHER_WARMER_CITY_INTERVAL_SEC:-60}
|
||||||
|
POLYWEATHER_WARMER_ENABLED: ${POLYWEATHER_WARMER_ENABLED:-true}
|
||||||
|
POLYWEATHER_WARMER_SCAN_INTERVAL_SEC: ${POLYWEATHER_WARMER_SCAN_INTERVAL_SEC:-300}
|
||||||
|
POLYWEATHER_WARMER_TICK_SEC: ${POLYWEATHER_WARMER_TICK_SEC:-60}
|
||||||
|
healthcheck:
|
||||||
|
interval: 60s
|
||||||
|
retries: 3
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- python
|
||||||
|
- -c
|
||||||
|
- import sqlite3; c=sqlite3.connect('/var/lib/polyweather/polyweather.db');
|
||||||
|
c.execute('SELECT 1'); c.close()
|
||||||
|
timeout: 10s
|
||||||
|
image: ghcr.io/yangyuan-zhen/polyweather-backend:${IMAGE_TAG:-latest}
|
||||||
|
mem_limit: ${POLYWEATHER_WARMER_MEM_LIMIT:-768m}
|
||||||
|
memswap_limit: ${POLYWEATHER_WARMER_MEMSWAP_LIMIT:-1g}
|
||||||
|
pids_limit: 256
|
||||||
|
restart: unless-stopped
|
||||||
|
user: ${UID:-1000}:${GID:-1000}
|
||||||
|
volumes:
|
||||||
|
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/var/lib/polyweather
|
||||||
|
- ${POLYWEATHER_RUNTIME_DATA_DIR:-/var/lib/polyweather}:/app/data
|
||||||
x-polyweather-base:
|
x-polyweather-base:
|
||||||
env_file: *id001
|
env_file: *id001
|
||||||
image: ghcr.io/yangyuan-zhen/polyweather-backend:${IMAGE_TAG:-latest}
|
image: ghcr.io/yangyuan-zhen/polyweather-backend:${IMAGE_TAG:-latest}
|
||||||
|
|||||||
@@ -28,11 +28,18 @@ export function runTests() {
|
|||||||
deployScript.includes("http://127.0.0.1:3001/terminal"),
|
deployScript.includes("http://127.0.0.1:3001/terminal"),
|
||||||
"deploy script must wait for the local frontend before relying on Cloudflare/public smoke checks",
|
"deploy script must wait for the local frontend before relying on Cloudflare/public smoke checks",
|
||||||
);
|
);
|
||||||
|
assert(
|
||||||
|
deployScript.includes("wait_for_scan_terminal_ready") &&
|
||||||
|
deployScript.includes("http://127.0.0.1:3001/api/scan/terminal") &&
|
||||||
|
deployScript.includes('"status":"ready"'),
|
||||||
|
"deploy script must wait for the scan terminal snapshot to be ready so first users do not see initialization payloads",
|
||||||
|
);
|
||||||
assert(
|
assert(
|
||||||
deployScript.includes("warm_public_route") &&
|
deployScript.includes("warm_public_route") &&
|
||||||
deployScript.includes("https://polyweather.top/terminal") &&
|
deployScript.includes("https://polyweather.top/terminal") &&
|
||||||
|
deployScript.includes("https://polyweather.top/api/scan/terminal") &&
|
||||||
deployScript.includes("https://polyweather.top/api/auth/me?prefer_snapshot=1"),
|
deployScript.includes("https://polyweather.top/api/auth/me?prefer_snapshot=1"),
|
||||||
"deploy script must warm terminal and auth snapshot routes after container replacement",
|
"deploy script must warm terminal, scan terminal, and auth snapshot routes after container replacement",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
deployScript.includes("validate_frontend_api_base_url") &&
|
deployScript.includes("validate_frontend_api_base_url") &&
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from web.cache_warmer_service import CacheWarmer, build_priority_city_batch
|
||||||
|
|
||||||
|
|
||||||
|
def test_priority_city_batch_prefers_local_active_hours_over_night():
|
||||||
|
cities = {
|
||||||
|
"alpha day": {"tz": 0},
|
||||||
|
"beta night": {"tz": 12 * 3600},
|
||||||
|
"gamma morning": {"tz": -4 * 3600},
|
||||||
|
}
|
||||||
|
|
||||||
|
selected = build_priority_city_batch(
|
||||||
|
cities,
|
||||||
|
now_utc=datetime(2026, 6, 14, 12, 0, tzinfo=timezone.utc),
|
||||||
|
batch_size=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert selected == ["alpha day", "gamma morning"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_cache_warmer_warms_scan_and_city_panel_without_force_refresh():
|
||||||
|
now_ts = datetime(2026, 6, 14, 12, 0, tzinfo=timezone.utc).timestamp()
|
||||||
|
scan_calls = []
|
||||||
|
city_calls = []
|
||||||
|
|
||||||
|
def scan_warmer(filters, *, force_refresh):
|
||||||
|
scan_calls.append((filters, force_refresh))
|
||||||
|
return {"status": "ready"}
|
||||||
|
|
||||||
|
def city_panel_warmer(city, *, force_refresh):
|
||||||
|
city_calls.append((city, force_refresh))
|
||||||
|
return {"city": city}
|
||||||
|
|
||||||
|
warmer = CacheWarmer(
|
||||||
|
city_provider=lambda: {
|
||||||
|
"alpha day": {"tz": 0},
|
||||||
|
"beta night": {"tz": 12 * 3600},
|
||||||
|
},
|
||||||
|
scan_warmer=scan_warmer,
|
||||||
|
city_panel_warmer=city_panel_warmer,
|
||||||
|
scan_interval_sec=300,
|
||||||
|
city_interval_sec=60,
|
||||||
|
city_batch_size=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
completed = warmer.run_due_once(now_ts=now_ts)
|
||||||
|
|
||||||
|
assert completed == 2
|
||||||
|
assert scan_calls == [({}, False)]
|
||||||
|
assert city_calls == [("alpha day", False)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_cache_warmer_skips_work_when_intervals_are_not_due():
|
||||||
|
now_ts = datetime(2026, 6, 14, 12, 0, tzinfo=timezone.utc).timestamp()
|
||||||
|
scan_calls = []
|
||||||
|
city_calls = []
|
||||||
|
warmer = CacheWarmer(
|
||||||
|
city_provider=lambda: {"alpha day": {"tz": 0}},
|
||||||
|
scan_warmer=lambda filters, *, force_refresh: scan_calls.append(True),
|
||||||
|
city_panel_warmer=lambda city, *, force_refresh: city_calls.append(city),
|
||||||
|
scan_interval_sec=300,
|
||||||
|
city_interval_sec=60,
|
||||||
|
city_batch_size=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
warmer.run_due_once(now_ts=now_ts)
|
||||||
|
scan_calls.clear()
|
||||||
|
city_calls.clear()
|
||||||
|
|
||||||
|
completed = warmer.run_due_once(now_ts=now_ts + 20)
|
||||||
|
|
||||||
|
assert completed == 0
|
||||||
|
assert scan_calls == []
|
||||||
|
assert city_calls == []
|
||||||
@@ -83,6 +83,10 @@ def test_docker_compose_isolates_collector_from_web_and_bot_services():
|
|||||||
1,
|
1,
|
||||||
)[0]
|
)[0]
|
||||||
collector_block = compose.split(" polyweather_collector:", 1)[1].split(
|
collector_block = compose.split(" polyweather_collector:", 1)[1].split(
|
||||||
|
"\n polyweather_warmer:",
|
||||||
|
1,
|
||||||
|
)[0]
|
||||||
|
warmer_block = compose.split(" polyweather_warmer:", 1)[1].split(
|
||||||
"\nx-polyweather-base:",
|
"\nx-polyweather-base:",
|
||||||
1,
|
1,
|
||||||
)[0]
|
)[0]
|
||||||
@@ -90,16 +94,24 @@ def test_docker_compose_isolates_collector_from_web_and_bot_services():
|
|||||||
assert "POLYWEATHER_SERVICE_ROLE: web" in compose
|
assert "POLYWEATHER_SERVICE_ROLE: web" in compose
|
||||||
assert "POLYWEATHER_SERVICE_ROLE: bot" in compose
|
assert "POLYWEATHER_SERVICE_ROLE: bot" in compose
|
||||||
assert "POLYWEATHER_SERVICE_ROLE: collector" in collector_block
|
assert "POLYWEATHER_SERVICE_ROLE: collector" in collector_block
|
||||||
|
assert "POLYWEATHER_SERVICE_ROLE: warmer" in warmer_block
|
||||||
|
assert "redis-server --appendonly yes --maxmemory ${POLYWEATHER_REDIS_MAXMEMORY:-512mb} --maxmemory-policy noeviction" in compose
|
||||||
assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'" in bot_block
|
assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'" in bot_block
|
||||||
assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'" in web_block
|
assert "POLYWEATHER_EVENT_STORE: ${POLYWEATHER_EVENT_STORE:-redis}" in web_block
|
||||||
|
assert "POLYWEATHER_REDIS_REQUIRED: ${POLYWEATHER_REDIS_REQUIRED:-true}" in web_block
|
||||||
|
assert "POLYWEATHER_REDIS_STREAM_MAXLEN: ${POLYWEATHER_REDIS_STREAM_MAXLEN:-100000}" in web_block
|
||||||
|
assert "POLYWEATHER_SCAN_TERMINAL_REDIS_CACHE_ENABLED: ${POLYWEATHER_SCAN_TERMINAL_REDIS_CACHE_ENABLED:-true}" in web_block
|
||||||
|
assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: ${POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED:-false}" in web_block
|
||||||
assert "POLYWEATHER_SCAN_TERMINAL_BUILD_TIMEOUT_SEC: '30'" in web_block
|
assert "POLYWEATHER_SCAN_TERMINAL_BUILD_TIMEOUT_SEC: '30'" in web_block
|
||||||
assert "POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS: ${POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS:-1}" in web_block
|
assert "POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS: ${POLYWEATHER_SCAN_TERMINAL_MAX_WORKERS:-4}" in web_block
|
||||||
assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'" in collector_block
|
assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'" in collector_block
|
||||||
|
assert "POLYWEATHER_SCAN_TERMINAL_PREWARM_ENABLED: 'false'" in warmer_block
|
||||||
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in bot_block
|
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in bot_block
|
||||||
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in web_block
|
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in web_block
|
||||||
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'true'" in collector_block
|
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'true'" in collector_block
|
||||||
assert "POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY:-1}" in web_block
|
assert "POLYWEATHER_OBSERVATION_COLLECTOR_ENABLED: 'false'" in warmer_block
|
||||||
assert "POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY:-1}" in web_block
|
assert "POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_CONCURRENCY:-2}" in web_block
|
||||||
|
assert "POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY: ${POLYWEATHER_CITY_DETAIL_BATCH_GLOBAL_CONCURRENCY:-2}" in web_block
|
||||||
assert "POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS:-3000}" in web_block
|
assert "POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_QUEUE_WAIT_MS:-3000}" in web_block
|
||||||
assert "POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS:-8000}" in web_block
|
assert "POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS: ${POLYWEATHER_CITY_DETAIL_BATCH_PARTIAL_TIMEOUT_MS:-8000}" in web_block
|
||||||
assert "UVICORN_WORKERS: ${UVICORN_WORKERS:-2}" in web_block
|
assert "UVICORN_WORKERS: ${UVICORN_WORKERS:-2}" in web_block
|
||||||
@@ -111,7 +123,15 @@ def test_docker_compose_isolates_collector_from_web_and_bot_services():
|
|||||||
in collector_block
|
in collector_block
|
||||||
)
|
)
|
||||||
assert "command: python -m web.observation_collector_worker" in collector_block
|
assert "command: python -m web.observation_collector_worker" in collector_block
|
||||||
|
assert "command: python -m web.cache_warmer_worker" in warmer_block
|
||||||
assert "POLYWEATHER_OBSERVATION_COLLECTOR_AMSC_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_AMSC_SEC:-60}" in collector_block
|
assert "POLYWEATHER_OBSERVATION_COLLECTOR_AMSC_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_AMSC_SEC:-60}" in collector_block
|
||||||
|
assert "POLYWEATHER_OBSERVATION_COLLECTOR_CACHE_REFRESH_WORKERS: ${POLYWEATHER_OBSERVATION_COLLECTOR_CACHE_REFRESH_WORKERS:-2}" in collector_block
|
||||||
|
assert "POLYWEATHER_WARMER_ENABLED: ${POLYWEATHER_WARMER_ENABLED:-true}" in warmer_block
|
||||||
|
assert "POLYWEATHER_WARMER_TICK_SEC: ${POLYWEATHER_WARMER_TICK_SEC:-60}" in warmer_block
|
||||||
|
assert "POLYWEATHER_WARMER_SCAN_INTERVAL_SEC: ${POLYWEATHER_WARMER_SCAN_INTERVAL_SEC:-300}" in warmer_block
|
||||||
|
assert "POLYWEATHER_WARMER_CITY_INTERVAL_SEC: ${POLYWEATHER_WARMER_CITY_INTERVAL_SEC:-60}" in warmer_block
|
||||||
|
assert "POLYWEATHER_WARMER_CITY_BATCH_SIZE: ${POLYWEATHER_WARMER_CITY_BATCH_SIZE:-8}" in warmer_block
|
||||||
|
assert "cpus: ${POLYWEATHER_WARMER_CPUS:-0.75}" in warmer_block
|
||||||
assert "TELEGRAM_AIRPORT_PUSH_INTERVAL_SEC: ${POLYWEATHER_BOT_AIRPORT_PUSH_INTERVAL_SEC:-60}" in bot_block
|
assert "TELEGRAM_AIRPORT_PUSH_INTERVAL_SEC: ${POLYWEATHER_BOT_AIRPORT_PUSH_INTERVAL_SEC:-60}" in bot_block
|
||||||
assert "POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC:-300}" in collector_block
|
assert "POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC: ${POLYWEATHER_OBSERVATION_COLLECTOR_MADIS_SEC:-300}" in collector_block
|
||||||
|
|
||||||
@@ -186,8 +206,13 @@ def test_deploy_script_retries_startup_smoke_checks():
|
|||||||
script = (ROOT / "deploy.sh").read_text(encoding="utf-8")
|
script = (ROOT / "deploy.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "smoke_check()" in script
|
assert "smoke_check()" in script
|
||||||
|
assert "wait_for_scan_terminal_ready()" in script
|
||||||
|
assert '"status":"ready"' in script
|
||||||
|
assert 'wait_for_scan_terminal_ready "scan terminal snapshot" "http://127.0.0.1:3001/api/scan/terminal"' in script
|
||||||
|
assert script.index("wait_for_scan_terminal_ready") < script.index("run_public_smoke_checks")
|
||||||
assert 'smoke_check "healthz" "https://api.polyweather.top/healthz" 15 3 5' in script
|
assert 'smoke_check "healthz" "https://api.polyweather.top/healthz" 15 3 5' in script
|
||||||
assert 'warm_public_route "local cities recent stats" "http://127.0.0.1:8000/api/cities?refresh_deb_recent=1"' in script
|
assert 'warm_public_route "local cities recent stats" "http://127.0.0.1:8000/api/cities?refresh_deb_recent=1"' in script
|
||||||
|
assert 'warm_public_route "scan terminal" "https://polyweather.top/api/scan/terminal"' in script
|
||||||
assert 'smoke_check "local cities" "http://127.0.0.1:8000/api/cities" 10 6 3' not in script
|
assert 'smoke_check "local cities" "http://127.0.0.1:8000/api/cities" 10 6 3' not in script
|
||||||
assert 'smoke_check "frontend cities" "https://polyweather.top/api/cities" 20 5 5' in script
|
assert 'smoke_check "frontend cities" "https://polyweather.top/api/cities" 20 5 5' in script
|
||||||
assert 'smoke_check "frontend" "https://www.polyweather.top/" 15 3 5' in script
|
assert 'smoke_check "frontend" "https://www.polyweather.top/" 15 3 5' in script
|
||||||
@@ -211,6 +236,7 @@ def test_deploy_script_retries_compose_recreate_races():
|
|||||||
assert "removal of container .* is already in progress" in script
|
assert "removal of container .* is already in progress" in script
|
||||||
assert 'compose_up_retry "backend services" -d --no-deps polyweather_web polyweather' in script
|
assert 'compose_up_retry "backend services" -d --no-deps polyweather_web polyweather' in script
|
||||||
assert 'compose_up_retry "observation collector" -d --no-deps polyweather_collector' in script
|
assert 'compose_up_retry "observation collector" -d --no-deps polyweather_collector' in script
|
||||||
|
assert 'compose_up_retry "cache warmer" -d --no-deps polyweather_warmer' in script
|
||||||
assert 'compose_up_retry "frontend" -d --no-deps polyweather_frontend' in script
|
assert 'compose_up_retry "frontend" -d --no-deps polyweather_frontend' in script
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
"""Low-priority cache warmer for user-facing aggregate payloads."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Any, Callable, Iterable, List, Mapping, Optional, Sequence
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
ASIA_CORE_CITIES = (
|
||||||
|
"hong kong",
|
||||||
|
"taipei",
|
||||||
|
"tokyo",
|
||||||
|
"seoul",
|
||||||
|
"busan",
|
||||||
|
"shanghai",
|
||||||
|
"beijing",
|
||||||
|
"guangzhou",
|
||||||
|
"qingdao",
|
||||||
|
"shenzhen",
|
||||||
|
"chongqing",
|
||||||
|
"chengdu",
|
||||||
|
"singapore",
|
||||||
|
"kuala lumpur",
|
||||||
|
"jakarta",
|
||||||
|
)
|
||||||
|
EUROPE_CORE_CITIES = (
|
||||||
|
"istanbul",
|
||||||
|
"ankara",
|
||||||
|
"moscow",
|
||||||
|
"tel aviv",
|
||||||
|
"london",
|
||||||
|
"paris",
|
||||||
|
"madrid",
|
||||||
|
"milan",
|
||||||
|
"warsaw",
|
||||||
|
"amsterdam",
|
||||||
|
"helsinki",
|
||||||
|
)
|
||||||
|
US_CORE_CITIES = (
|
||||||
|
"new york",
|
||||||
|
"los angeles",
|
||||||
|
"san francisco",
|
||||||
|
"austin",
|
||||||
|
"houston",
|
||||||
|
"chicago",
|
||||||
|
"dallas",
|
||||||
|
"miami",
|
||||||
|
"atlanta",
|
||||||
|
"seattle",
|
||||||
|
)
|
||||||
|
DEFAULT_HOT_CITIES = ASIA_CORE_CITIES + EUROPE_CORE_CITIES + US_CORE_CITIES
|
||||||
|
SOURCE_PRIORITY_WEIGHT = {
|
||||||
|
"hko": 10,
|
||||||
|
"cwa": 9,
|
||||||
|
"noaa": 8,
|
||||||
|
"mgm": 8,
|
||||||
|
"metar": 6,
|
||||||
|
"wunderground": 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _env_bool(name: str, default: bool) -> bool:
|
||||||
|
raw = os.getenv(name)
|
||||||
|
if raw is None:
|
||||||
|
return default
|
||||||
|
return raw.strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def _env_int(name: str, default: int) -> int:
|
||||||
|
raw = os.getenv(name)
|
||||||
|
if raw is None:
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
return int(raw)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_city_name(city: str) -> str:
|
||||||
|
return str(city or "").strip().lower().replace("-", " ")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_city_list(raw: Optional[str]) -> Sequence[str]:
|
||||||
|
if not raw:
|
||||||
|
return ()
|
||||||
|
out: List[str] = []
|
||||||
|
for part in raw.split(","):
|
||||||
|
city = _normalize_city_name(part)
|
||||||
|
if city and city not in out:
|
||||||
|
out.append(city)
|
||||||
|
return tuple(out)
|
||||||
|
|
||||||
|
|
||||||
|
def _coerce_utc_offset_seconds(meta: Mapping[str, Any]) -> int:
|
||||||
|
for key in ("tz", "tz_offset", "utc_offset_seconds"):
|
||||||
|
raw = meta.get(key)
|
||||||
|
if raw is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
value = float(raw)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if abs(value) <= 24:
|
||||||
|
value *= 3600
|
||||||
|
return int(value)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _local_hour(meta: Mapping[str, Any], now_utc: datetime) -> int:
|
||||||
|
offset = _coerce_utc_offset_seconds(meta)
|
||||||
|
return int((now_utc + timedelta(seconds=offset)).hour)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class WarmCandidate:
|
||||||
|
city: str
|
||||||
|
score: int
|
||||||
|
local_hour: int
|
||||||
|
settlement_source: str
|
||||||
|
|
||||||
|
|
||||||
|
def rank_priority_cities(
|
||||||
|
cities: Mapping[str, Mapping[str, Any]],
|
||||||
|
*,
|
||||||
|
now_utc: Optional[datetime] = None,
|
||||||
|
hot_cities: Optional[Iterable[str]] = None,
|
||||||
|
) -> List[WarmCandidate]:
|
||||||
|
"""Rank cities by local activity window and source usefulness."""
|
||||||
|
|
||||||
|
now = now_utc or datetime.now(timezone.utc)
|
||||||
|
if now.tzinfo is None:
|
||||||
|
now = now.replace(tzinfo=timezone.utc)
|
||||||
|
hot_set = {
|
||||||
|
_normalize_city_name(city)
|
||||||
|
for city in (DEFAULT_HOT_CITIES if hot_cities is None else hot_cities)
|
||||||
|
}
|
||||||
|
ranked: List[WarmCandidate] = []
|
||||||
|
for raw_city, raw_meta in cities.items():
|
||||||
|
city = _normalize_city_name(raw_city)
|
||||||
|
if not city:
|
||||||
|
continue
|
||||||
|
meta = raw_meta or {}
|
||||||
|
hour = _local_hour(meta, now)
|
||||||
|
source = str(meta.get("settlement_source") or "metar").strip().lower()
|
||||||
|
score = SOURCE_PRIORITY_WEIGHT.get(source, 3)
|
||||||
|
if city in hot_set:
|
||||||
|
score += 30
|
||||||
|
if 9 <= hour < 18:
|
||||||
|
score += 40
|
||||||
|
elif 6 <= hour < 22:
|
||||||
|
score += 25
|
||||||
|
else:
|
||||||
|
score += 2
|
||||||
|
ranked.append(
|
||||||
|
WarmCandidate(
|
||||||
|
city=city,
|
||||||
|
score=score,
|
||||||
|
local_hour=hour,
|
||||||
|
settlement_source=source,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ranked.sort(key=lambda item: (-item.score, item.local_hour, item.city))
|
||||||
|
return ranked
|
||||||
|
|
||||||
|
|
||||||
|
def build_priority_city_batch(
|
||||||
|
cities: Mapping[str, Mapping[str, Any]],
|
||||||
|
*,
|
||||||
|
now_utc: Optional[datetime] = None,
|
||||||
|
batch_size: int = 8,
|
||||||
|
hot_cities: Optional[Iterable[str]] = None,
|
||||||
|
) -> List[str]:
|
||||||
|
ranked = rank_priority_cities(cities, now_utc=now_utc, hot_cities=hot_cities)
|
||||||
|
limit = max(0, int(batch_size or 0))
|
||||||
|
return [candidate.city for candidate in ranked[:limit]]
|
||||||
|
|
||||||
|
|
||||||
|
class CacheWarmer:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
city_provider: Callable[[], Mapping[str, Mapping[str, Any]]],
|
||||||
|
scan_warmer: Callable[..., Any],
|
||||||
|
city_panel_warmer: Callable[..., Any],
|
||||||
|
scan_interval_sec: int = 300,
|
||||||
|
city_interval_sec: int = 60,
|
||||||
|
city_batch_size: int = 8,
|
||||||
|
hot_cities: Optional[Iterable[str]] = None,
|
||||||
|
) -> None:
|
||||||
|
self.city_provider = city_provider
|
||||||
|
self.scan_warmer = scan_warmer
|
||||||
|
self.city_panel_warmer = city_panel_warmer
|
||||||
|
self.scan_interval_sec = max(60, int(scan_interval_sec or 300))
|
||||||
|
self.city_interval_sec = max(30, int(city_interval_sec or 60))
|
||||||
|
self.city_batch_size = max(1, min(32, int(city_batch_size or 8)))
|
||||||
|
self.hot_cities = tuple(hot_cities or DEFAULT_HOT_CITIES)
|
||||||
|
self._last_scan_ts = 0.0
|
||||||
|
self._last_city_ts = 0.0
|
||||||
|
self._city_cursor = 0
|
||||||
|
|
||||||
|
def run_due_once(self, *, now_ts: Optional[float] = None) -> int:
|
||||||
|
now = float(time.time() if now_ts is None else now_ts)
|
||||||
|
completed = 0
|
||||||
|
if now - self._last_scan_ts >= self.scan_interval_sec:
|
||||||
|
self._last_scan_ts = now
|
||||||
|
if self._warm_scan():
|
||||||
|
completed += 1
|
||||||
|
if now - self._last_city_ts >= self.city_interval_sec:
|
||||||
|
self._last_city_ts = now
|
||||||
|
completed += self._warm_city_batch(now_ts=now)
|
||||||
|
return completed
|
||||||
|
|
||||||
|
def _warm_scan(self) -> bool:
|
||||||
|
try:
|
||||||
|
self.scan_warmer({}, force_refresh=False)
|
||||||
|
return True
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("cache warmer scan payload failed: {}", exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _warm_city_batch(self, *, now_ts: float) -> int:
|
||||||
|
try:
|
||||||
|
cities = self.city_provider() or {}
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("cache warmer city provider failed: {}", exc)
|
||||||
|
return 0
|
||||||
|
ranked = rank_priority_cities(
|
||||||
|
cities,
|
||||||
|
now_utc=datetime.fromtimestamp(now_ts, timezone.utc),
|
||||||
|
hot_cities=self.hot_cities,
|
||||||
|
)
|
||||||
|
if not ranked:
|
||||||
|
return 0
|
||||||
|
start = self._city_cursor % len(ranked)
|
||||||
|
self._city_cursor += self.city_batch_size
|
||||||
|
selected = ranked[start : start + self.city_batch_size]
|
||||||
|
if len(selected) < self.city_batch_size:
|
||||||
|
selected.extend(ranked[: self.city_batch_size - len(selected)])
|
||||||
|
|
||||||
|
completed = 0
|
||||||
|
for candidate in selected:
|
||||||
|
try:
|
||||||
|
self.city_panel_warmer(candidate.city, force_refresh=False)
|
||||||
|
completed += 1
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(
|
||||||
|
"cache warmer city panel failed city={} source={}: {}",
|
||||||
|
candidate.city,
|
||||||
|
candidate.settlement_source,
|
||||||
|
exc,
|
||||||
|
)
|
||||||
|
return completed
|
||||||
|
|
||||||
|
|
||||||
|
def build_default_cache_warmer() -> CacheWarmer:
|
||||||
|
from web.core import CITIES
|
||||||
|
from web.scan_terminal_service import build_scan_terminal_payload
|
||||||
|
from web.services.city_runtime import _refresh_city_panel_cache
|
||||||
|
|
||||||
|
hot_cities = _parse_city_list(os.getenv("POLYWEATHER_WARMER_HOT_CITIES"))
|
||||||
|
return CacheWarmer(
|
||||||
|
city_provider=lambda: CITIES,
|
||||||
|
scan_warmer=build_scan_terminal_payload,
|
||||||
|
city_panel_warmer=_refresh_city_panel_cache,
|
||||||
|
scan_interval_sec=_env_int("POLYWEATHER_WARMER_SCAN_INTERVAL_SEC", 300),
|
||||||
|
city_interval_sec=_env_int("POLYWEATHER_WARMER_CITY_INTERVAL_SEC", 60),
|
||||||
|
city_batch_size=_env_int("POLYWEATHER_WARMER_CITY_BATCH_SIZE", 8),
|
||||||
|
hot_cities=hot_cities or DEFAULT_HOT_CITIES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def warmer_enabled() -> bool:
|
||||||
|
return _env_bool("POLYWEATHER_WARMER_ENABLED", True)
|
||||||
|
|
||||||
|
|
||||||
|
def warmer_tick_sec() -> int:
|
||||||
|
return max(10, _env_int("POLYWEATHER_WARMER_TICK_SEC", 60))
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"""Standalone cache warmer process for production aggregate payloads."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import signal
|
||||||
|
import threading
|
||||||
|
from types import FrameType
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from web.cache_warmer_service import (
|
||||||
|
build_default_cache_warmer,
|
||||||
|
warmer_enabled,
|
||||||
|
warmer_tick_sec,
|
||||||
|
)
|
||||||
|
|
||||||
|
_STOP_EVENT = threading.Event()
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_stop_signal(signum: int, _frame: Optional[FrameType]) -> None:
|
||||||
|
logger.info("cache warmer worker stopping signal={}", signum)
|
||||||
|
_STOP_EVENT.set()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
signal.signal(signal.SIGINT, _handle_stop_signal)
|
||||||
|
signal.signal(signal.SIGTERM, _handle_stop_signal)
|
||||||
|
|
||||||
|
if not warmer_enabled():
|
||||||
|
logger.warning("cache warmer worker disabled; idling")
|
||||||
|
while not _STOP_EVENT.wait(3600):
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
warmer = build_default_cache_warmer()
|
||||||
|
tick_sec = warmer_tick_sec()
|
||||||
|
logger.info(
|
||||||
|
"cache warmer worker started tick={}s city_batch={}",
|
||||||
|
tick_sec,
|
||||||
|
warmer.city_batch_size,
|
||||||
|
)
|
||||||
|
while not _STOP_EVENT.is_set():
|
||||||
|
try:
|
||||||
|
completed = warmer.run_due_once()
|
||||||
|
logger.debug("cache warmer run completed work_items={}", completed)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception("cache warmer loop failed: {}", exc)
|
||||||
|
_STOP_EVENT.wait(tick_sec)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user