|
11 | 11 |
|
12 | 12 | import asyncio |
13 | 13 | import contextlib |
| 14 | +import os |
14 | 15 | import logging |
15 | 16 | import sys |
16 | 17 | import time |
17 | 18 | from collections.abc import AsyncIterator |
18 | 19 | from contextlib import asynccontextmanager |
19 | 20 |
|
20 | 21 | import httpx |
21 | | -import uvicorn |
22 | 22 | from fastapi import FastAPI |
23 | 23 | from fastapi.responses import JSONResponse |
24 | 24 | from fastapi.routing import APIRoute |
|
30 | 30 | from database import dispose_database, init_database, init_db |
31 | 31 | from datasources.exceptions import BackendStartupTimeout |
32 | 32 | from middleware.openapi import install_custom_openapi |
| 33 | +from middleware.runtime_ssl import RuntimeSSLOptions, run_uvicorn |
33 | 34 | from services.rca_job_service import rca_job_service |
34 | 35 | from services.security_service import InternalAuthMiddleware |
35 | 36 |
|
@@ -72,6 +73,32 @@ def _generate_operation_id(route: APIRoute) -> str: |
72 | 73 | return route.name |
73 | 74 |
|
74 | 75 |
|
| 76 | +async def _bootstrap_database() -> None: |
| 77 | + timeout_seconds = float(os.getenv("DATABASE_STARTUP_TIMEOUT", "180")) |
| 78 | + retry_delay_seconds = float(os.getenv("DATABASE_STARTUP_RETRY_DELAY", "2")) |
| 79 | + deadline = time.monotonic() + timeout_seconds |
| 80 | + attempt = 0 |
| 81 | + |
| 82 | + while True: |
| 83 | + attempt += 1 |
| 84 | + try: |
| 85 | + if settings.database_url: |
| 86 | + init_database(settings.database_url) |
| 87 | + init_db() |
| 88 | + log.info("Resolver database initialization completed") |
| 89 | + return |
| 90 | + except Exception as exc: # pylint: disable=broad-exception-caught |
| 91 | + if time.monotonic() >= deadline: |
| 92 | + raise RuntimeError("Resolver database did not become ready before startup timeout") from exc |
| 93 | + log.warning( |
| 94 | + "Resolver database not ready (attempt %d, retrying in %.1fs): %s", |
| 95 | + attempt, |
| 96 | + retry_delay_seconds, |
| 97 | + exc, |
| 98 | + ) |
| 99 | + await asyncio.sleep(retry_delay_seconds) |
| 100 | + |
| 101 | + |
75 | 102 | class ResolverReadyResponse(BaseModel): |
76 | 103 | ready: bool = Field(description="Whether resolver dependencies are currently ready.") |
77 | 104 | backends: dict[str, str] = Field( |
@@ -175,8 +202,7 @@ async def _wait_for_all_bg(data_settings: Settings, tenant_id: str) -> None: |
175 | 202 | @asynccontextmanager |
176 | 203 | async def lifespan(_app: FastAPI) -> AsyncIterator[None]: |
177 | 204 | if settings.database_url: |
178 | | - init_database(settings.database_url) |
179 | | - init_db() |
| 205 | + await _bootstrap_database() |
180 | 206 | await rca_job_service.startup_recovery() |
181 | 207 |
|
182 | 208 | tenant_id = settings.default_tenant_id |
@@ -245,21 +271,11 @@ async def ready() -> JSONResponse: |
245 | 271 |
|
246 | 272 |
|
247 | 273 | if __name__ == "__main__": |
248 | | - if settings.ssl_enabled: |
249 | | - uvicorn.run( |
250 | | - "main:app", |
251 | | - host=settings.host, |
252 | | - port=settings.port, |
253 | | - log_level="info", |
254 | | - access_log=True, |
255 | | - ssl_certfile=settings.ssl_certfile, |
256 | | - ssl_keyfile=settings.ssl_keyfile, |
257 | | - ) |
258 | | - else: |
259 | | - uvicorn.run( |
260 | | - "main:app", |
261 | | - host=settings.host, |
262 | | - port=settings.port, |
263 | | - log_level="info", |
264 | | - access_log=True, |
265 | | - ) |
| 274 | + run_uvicorn( |
| 275 | + app, |
| 276 | + host=settings.host, |
| 277 | + port=settings.port, |
| 278 | + log_level="info", |
| 279 | + access_log=True, |
| 280 | + ssl_options=RuntimeSSLOptions.from_settings(settings), |
| 281 | + ) |
0 commit comments