diff --git a/app/__init__.py b/app/__init__.py index 3a90b55..c7cbaaf 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -31,21 +31,41 @@ logging.basicConfig( logger = logging.getLogger(__name__) +async def wait_for_db(retries: int = 10, base_delay: float = 3.0, max_delay: float = 30.0): + """Wait for the database to become available with exponential backoff.""" + for attempt in range(1, retries + 1): + try: + with engine.connect() as conn: + conn.execute(text("SELECT 1")) + logger.info("Database connection successful!") + return + except Exception as e: + if attempt == retries: + logger.error(f"Database unreachable after {retries} attempts: {e}") + raise + delay = min(base_delay * (2 ** (attempt - 1)), max_delay) + logger.warning( + f"Database not ready (attempt {attempt}/{retries}): {e}. " + f"Retrying in {delay:.0f}s..." + ) + await asyncio.sleep(delay) @asynccontextmanager async def lifespan(app: FastAPI): - logger.info("Testing database connection...") - try: - with engine.connect() as conn: - conn.execute(text("SELECT 1")) - conn.commit() - logger.info("Database connection successful!") - except Exception as e: - logger.error(f"Database connection failed: {e}") - raise + logger.info("Starting SaaS application...") - await redis_client.connect() - await run_in_threadpool(sync_redis_client.connect) + await wait_for_db() + + app.state.redis_available = False + + try: + await redis_client.connect() + await run_in_threadpool(sync_redis_client.connect) + app.state.redis_available = True + logger.info("Redis connected successfully.") + except Exception as e: + logger.error(f"Redis unavailable at startup: {e}") + logger.warning("Continuing without Redis — event queue disabled.") logger.info( f"{settings.PROJECT_NAME} v{settings.VERSION} started ({settings.APP_ENV})" @@ -55,9 +75,9 @@ async def lifespan(app: FastAPI): logger.info("Redis Event Consumer STARTED") while True: try: - if not redis_client.client: - await asyncio.sleep(5) - continue + if not app.state.redis_available or not redis_client.client: + await asyncio.sleep(5) + continue result = await redis_client.client.blpop("saas:events:queue", timeout=5) @@ -69,7 +89,7 @@ async def lifespan(app: FastAPI): except Exception as e: logger.error(f"Error processing event {event_id}: {e}") - except Exception as e: + except Exception: await asyncio.sleep(1) async def fallback_poller(): @@ -94,9 +114,12 @@ async def lifespan(app: FastAPI): for task in background_tasks: task.cancel() await asyncio.gather(*background_tasks, return_exceptions=True) - await redis_client.close() - await run_in_threadpool(sync_redis_client.close) - + if app.state.redis_available: + try: + await redis_client.close() + await run_in_threadpool(sync_redis_client.close) + except Exception: + pass def create_app() -> FastAPI: app = FastAPI(