perf: gzip responses; multi-worker serving with single collection leader
Documentation / Validate Documentation (push) Failing after 8s
Documentation / Build Sphinx Documentation (push) Successful in 15s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 23s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 12s
Documentation / Generate API Documentation (push) Successful in 9s
Documentation / Documentation Summary (push) Successful in 2s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 0s
Documentation / Validate Documentation (push) Failing after 8s
Documentation / Build Sphinx Documentation (push) Successful in 15s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Test Suite (3.11) (push) Failing after 23s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Build Docker Image (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Integration Test with Services (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Staging (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Deploy to Production (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Performance Test (push) Skipped
CI/CD Pipeline - Northern Thailand Ping River Monitor / Code Quality (push) Successful in 12s
Documentation / Generate API Documentation (push) Successful in 9s
Documentation / Documentation Summary (push) Successful in 2s
CI/CD Pipeline - Northern Thailand Ping River Monitor / Cleanup (push) Successful in 0s
GZipMiddleware (min 500 bytes) compresses the dashboard HTML ~4x and station JSON up to ~100x, end-to-end through the Caddy TLS terminator — production load testing showed the deployment is bandwidth-bound once the response caches hit, so compression is the capacity lever. WEB_WORKERS (default 2) runs uvicorn multi-process via the app import string. Every worker executes the lifespan, so a localhost lock port (COLLECTION_LEADER_PORT, default 8901) elects exactly one background-collection leader per machine — RID/HII polling stays once-per-cycle instead of once-per-worker; the lock releases with the process. Locust clients now send Accept-Encoding so future runs measure compressed transfer, as browsers do.
This commit is contained in:
+42
-2
@@ -24,6 +24,7 @@ from fastapi import (
|
||||
Response,
|
||||
)
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from fastapi.responses import FileResponse, HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
@@ -110,6 +111,26 @@ app_state = {
|
||||
}
|
||||
|
||||
|
||||
def _acquire_collection_leadership(port: int):
|
||||
"""Elect one background-collection leader per machine via a localhost bind.
|
||||
|
||||
With multiple uvicorn workers every process runs this lifespan; only the
|
||||
worker holding the lock port runs the scraper/HII loops, so external APIs
|
||||
are polled once per cycle instead of once per worker. The socket is held
|
||||
for the process lifetime and releases automatically if the worker dies.
|
||||
"""
|
||||
import socket
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.bind(("127.0.0.1", port))
|
||||
sock.listen(1)
|
||||
return sock
|
||||
except OSError:
|
||||
sock.close()
|
||||
return None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Application lifespan manager"""
|
||||
@@ -148,8 +169,18 @@ async def lifespan(app: FastAPI):
|
||||
health_manager.add_check(MemoryHealthCheck(max_memory_mb=1000))
|
||||
app_state["health_manager"] = health_manager
|
||||
|
||||
# Start background scraping task
|
||||
app_state["scraping_task"] = asyncio.create_task(background_scraping_task())
|
||||
# Start background scraping in exactly one worker per machine
|
||||
app_state["leader_lock"] = _acquire_collection_leadership(
|
||||
Config.COLLECTION_LEADER_PORT
|
||||
)
|
||||
if app_state["leader_lock"]:
|
||||
app_state["scraping_task"] = asyncio.create_task(background_scraping_task())
|
||||
logger.info("This worker is the background-collection leader")
|
||||
else:
|
||||
logger.info(
|
||||
"Another worker holds collection leadership; "
|
||||
"background scraping disabled in this process"
|
||||
)
|
||||
|
||||
logger.info("Water Monitor API started successfully")
|
||||
|
||||
@@ -164,6 +195,8 @@ async def lifespan(app: FastAPI):
|
||||
await app_state["scraping_task"]
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
if app_state.get("leader_lock"):
|
||||
app_state["leader_lock"].close()
|
||||
|
||||
logger.info("Water Monitor API shutdown complete")
|
||||
|
||||
@@ -185,6 +218,13 @@ app.mount(
|
||||
# Origins come from CORS_ALLOW_ORIGINS (comma-separated). When none are configured
|
||||
# we fall back to a wildcard WITHOUT credentials (a safe, spec-valid combination);
|
||||
# credentials are only enabled when explicit origins are provided.
|
||||
# Compress large responses end-to-end: the dashboard HTML and station JSON
|
||||
# payloads shrink ~5-6x, which matters both on the internal VPN hop to the
|
||||
# Caddy TLS terminator and on the public leg (Caddy passes Content-Encoding
|
||||
# through). Load testing showed the deployment is bandwidth-bound, not
|
||||
# compute-bound, once the response caches are hit.
|
||||
app.add_middleware(GZipMiddleware, minimum_size=500)
|
||||
|
||||
_cors_origins = Config.CORS_ALLOW_ORIGINS or ["*"]
|
||||
_cors_allow_credentials = bool(Config.CORS_ALLOW_ORIGINS)
|
||||
app.add_middleware(
|
||||
|
||||
Reference in New Issue
Block a user