diff --git a/Dockerfile b/Dockerfile index 2266452..d80cb63 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,6 +36,10 @@ COPY run.py /app/ # Port the application listens on EXPOSE 8000 +# Health check to ensure the container is responding to requests +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + # Command to run the application using Uvicorn # The command format is: uvicorn [module:app_object] --host [ip] --port [port] # We use the standard uvicorn worker configuration diff --git a/README.md b/README.md index d8fdedf..4396001 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Copy `.env.example` to `.env` (done automatically by `make dev-setup`) and fill | `ENABLE_ETL_SCHEDULER` | | Set `true` to run the ETL on a schedule | | `ETL_CRON` | | Cron expression (UTC) — takes precedence over `ETL_INTERVAL_MINUTES` | | `ETL_INTERVAL_MINUTES` | | ETL polling interval in minutes (default `15`) | +| `SHUTDOWN_TIMEOUT_SECONDS` | | Graceful shutdown timeout in seconds (default `30`) | | `BQ_ENABLED` | | Set `true` to enable BigQuery loading | | `BQ_PROJECT_ID` | | GCP project ID | | `BQ_DATASET` | | BigQuery dataset name | diff --git a/run.py b/run.py index 6e0d946..38d86e9 100644 --- a/run.py +++ b/run.py @@ -2,6 +2,13 @@ from src.main import app # noqa: F401 if __name__ == "__main__": - uvicorn.run("src.main:app", host="0.0.0.0", port=8000) + from src.config import get_settings + settings = get_settings() + uvicorn.run( + "src.main:app", + host="0.0.0.0", + port=8000, + timeout_graceful_shutdown=settings.SHUTDOWN_TIMEOUT_SECONDS + ) diff --git a/src/config.py b/src/config.py index e206e2e..8dd888f 100644 --- a/src/config.py +++ b/src/config.py @@ -4,11 +4,6 @@ from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict -from pydantic_settings import BaseSettings - -class Settings(BaseSettings): - - class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") @@ -38,6 +33,7 @@ class Settings(BaseSettings): POOL_SIZE: int = 5 POOL_MAX_OVERFLOW: int = 10 REPORT_CACHE_MINUTES: int = 60 + SHUTDOWN_TIMEOUT_SECONDS: int = 30 SERVICE_API_KEY: str = "default_service_secret_change_me" ADMIN_API_KEY: str = "default_admin_secret_change_me" diff --git a/src/main.py b/src/main.py index 614bc90..60e1beb 100644 --- a/src/main.py +++ b/src/main.py @@ -197,11 +197,14 @@ def on_startup() -> None: @app.on_event("shutdown") def on_shutdown() -> None: global etl_scheduler + log_info("Shutdown initiated: waiting for in-flight requests and scheduler...") if etl_scheduler is not None: try: - etl_scheduler.shutdown(wait=False) - except Exception: - pass + # wait=True ensures running jobs complete before scheduler stops + etl_scheduler.shutdown(wait=True) + log_info("ETL scheduler shut down successfully.") + except Exception as exc: + log_error("Error during scheduler shutdown", {"error": str(exc)}) # ---------------------------------------------------------------------------