Fix Pydantic error in Settings class and improve supervisord configuration

This commit is contained in:
Automated Action 2025-06-03 10:38:05 +00:00
parent a5aebe0377
commit 578ec671c3
3 changed files with 107 additions and 24 deletions

View File

@ -1,9 +1,29 @@
import os import os
from pathlib import Path from pathlib import Path
from typing import List from typing import List, ClassVar
from pydantic import AnyHttpUrl, validator from pydantic import AnyHttpUrl, validator
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
# Set up DB_DIR outside the class to keep config cleaner
def get_db_dir() -> Path:
# Main database location - use absolute path for reliability
db_dir = Path("/app/storage/db")
# Fallback to project-relative path if /app is not available
if not db_dir.exists() and not os.access("/app", os.W_OK):
# Use project root-relative path as a fallback
project_root = Path(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
db_dir = project_root / "app" / "storage" / "db"
# Create directory if it doesn't exist
try:
db_dir.mkdir(parents=True, exist_ok=True)
print(f"Created database directory at: {db_dir}")
except Exception as e:
print(f"Warning: Could not create database directory: {e}")
return db_dir
class Settings(BaseSettings): class Settings(BaseSettings):
model_config = SettingsConfigDict( model_config = SettingsConfigDict(
env_file=".env", env_file=".env",
@ -20,15 +40,18 @@ class Settings(BaseSettings):
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 8 days ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 8 days
# SQLite settings # SQLite settings
# Main database location - use absolute path for reliability # This is a class variable, not a field
DB_DIR = Path(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) / "app" / "storage" / "db" DB_DIR: ClassVar[Path] = get_db_dir()
DB_DIR.mkdir(parents=True, exist_ok=True)
SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite"
# Fallback to in-memory SQLite if file access fails # Fallback to in-memory SQLite if file access fails
USE_IN_MEMORY_DB: bool = os.getenv("USE_IN_MEMORY_DB", "").lower() in ("true", "1", "yes") USE_IN_MEMORY_DB: bool = os.getenv("USE_IN_MEMORY_DB", "").lower() in ("true", "1", "yes")
if USE_IN_MEMORY_DB: if USE_IN_MEMORY_DB:
SQLALCHEMY_DATABASE_URL = "sqlite://" # In-memory SQLite database SQLALCHEMY_DATABASE_URL: str = "sqlite://" # In-memory SQLite database
print("Using in-memory SQLite database")
else:
SQLALCHEMY_DATABASE_URL: str = f"sqlite:///{DB_DIR}/db.sqlite"
print(f"Using file-based SQLite database at: {SQLALCHEMY_DATABASE_URL}")
# CORS settings # CORS settings
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = [] BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []

View File

@ -1,6 +1,8 @@
from sqlalchemy import create_engine, event from pathlib import Path
from sqlalchemy import create_engine, event, text
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from tenacity import retry, stop_after_attempt, wait_fixed
from app.core.config import settings from app.core.config import settings
from app.core.logging import get_logger from app.core.logging import get_logger
@ -8,19 +10,45 @@ from app.core.logging import get_logger
logger = get_logger("db.session") logger = get_logger("db.session")
# Wrap in function for easier error handling and retries # Wrap in function for easier error handling and retries
@retry(stop=stop_after_attempt(3), wait=wait_fixed(2), reraise=True)
def create_db_engine(): def create_db_engine():
"""Create SQLAlchemy engine with retry logic and fallback options."""
# Use in-memory database if configured or as fallback # Use in-memory database if configured or as fallback
if settings.USE_IN_MEMORY_DB: if settings.USE_IN_MEMORY_DB:
logger.info("Using in-memory SQLite database") logger.info("Using in-memory SQLite database")
db_url = "sqlite://" db_url = "sqlite://"
connect_args = {"check_same_thread": False} connect_args = {"check_same_thread": False}
else: else:
# Ensure the directory exists
db_path = Path(settings.SQLALCHEMY_DATABASE_URL.replace("sqlite:///", ""))
db_dir = db_path.parent
try:
db_dir.mkdir(parents=True, exist_ok=True)
logger.info(f"Ensured database directory exists at: {db_dir}")
# Verify write permissions by touching a test file
test_file = db_dir / ".test_write_access"
test_file.touch()
test_file.unlink()
logger.info(f"Verified write access to database directory: {db_dir}")
except Exception as e:
logger.error(f"Database directory access error: {e}")
# Force in-memory database
logger.info("Forcing in-memory SQLite database due to directory access error")
return create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
pool_pre_ping=True,
)
logger.info(f"Using file-based SQLite database at: {settings.SQLALCHEMY_DATABASE_URL}") logger.info(f"Using file-based SQLite database at: {settings.SQLALCHEMY_DATABASE_URL}")
db_url = settings.SQLALCHEMY_DATABASE_URL db_url = settings.SQLALCHEMY_DATABASE_URL
connect_args = {"check_same_thread": False} connect_args = {"check_same_thread": False}
# Create engine with better error handling # Create engine with better error handling
try: try:
logger.info(f"Creating SQLAlchemy engine with URL: {db_url}")
engine = create_engine( engine = create_engine(
db_url, db_url,
connect_args=connect_args, connect_args=connect_args,
@ -32,13 +60,9 @@ def create_db_engine():
def on_connect(dbapi_connection, connection_record): def on_connect(dbapi_connection, connection_record):
logger.info("Database connection established") logger.info("Database connection established")
@event.listens_for(engine, "engine_connect")
def on_engine_connect(connection):
logger.info("Engine connected")
# Test connection # Test connection
with engine.connect() as conn: with engine.connect() as conn:
conn.execute("SELECT 1") conn.execute(text("SELECT 1"))
logger.info("Database connection test successful") logger.info("Database connection test successful")
return engine return engine
@ -50,26 +74,48 @@ def create_db_engine():
logger.info("Falling back to in-memory SQLite database") logger.info("Falling back to in-memory SQLite database")
return create_engine( return create_engine(
"sqlite://", "sqlite://",
connect_args={"check_same_thread": False} connect_args={"check_same_thread": False},
pool_pre_ping=True,
) )
# Re-raise if in-memory DB was already the target # Re-raise if in-memory DB was already the target
raise raise
# Initialize engine # Initialize engine with safer exception handling
engine = None
SessionLocal = None
try: try:
engine = create_db_engine() engine = create_db_engine()
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) if engine:
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
logger.info("Database session factory created successfully")
else:
logger.error("Failed to create database engine")
except Exception as e: except Exception as e:
logger.error(f"Failed to initialize database engine: {e}", exc_info=True) logger.error(f"Failed to initialize database engine: {e}", exc_info=True)
engine = None # Set up in-memory database as last resort
SessionLocal = None try:
logger.info("Setting up emergency in-memory SQLite database")
engine = create_engine("sqlite://", connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
except Exception as fallback_error:
logger.critical(f"Critical database failure: {fallback_error}", exc_info=True)
# Allow application to start but in degraded mode
# Dependency to get DB session # Dependency to get DB session
def get_db(): def get_db():
"""FastAPI dependency for database sessions with error handling."""
if not SessionLocal: if not SessionLocal:
logger.error("Database session not initialized") logger.error("Database session not initialized, attempting to initialize")
raise SQLAlchemyError("Database connection failed") # Last chance to initialize
global engine, SessionLocal
try:
engine = create_engine("sqlite://", connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
except Exception as e:
logger.critical(f"Could not initialize database even as fallback: {e}")
raise SQLAlchemyError("Database connection critically failed")
db = SessionLocal() db = SessionLocal()
try: try:
logger.debug("DB session created") logger.debug("DB session created")

View File

@ -5,18 +5,32 @@ logfile_maxbytes=50MB
logfile_backups=10 logfile_backups=10
loglevel=info loglevel=info
pidfile=/tmp/supervisord.pid pidfile=/tmp/supervisord.pid
user=root
[program:app-8001] [program:app-8001]
command=python -m uvicorn main:app --host 0.0.0.0 --port 8001 --log-level debug command=python -m uvicorn main:app --host 0.0.0.0 --port 8001 --log-level debug
directory=/projects/bloggingapi-a05jzl directory=/projects/bloggingapi-a05jzl
autostart=true autostart=true
autorestart=true autorestart=true
startretries=3 startretries=5
numprocs=1 numprocs=1
startsecs=2 startsecs=1
# Use either redirect_stderr or stderr_logfile, not both
redirect_stderr=true redirect_stderr=true
stdout_logfile=/tmp/app-8001.log stdout_logfile=/tmp/app-8001.log
stdout_logfile_maxbytes=50MB stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10 stdout_logfile_backups=10
environment=PORT=8001,PYTHONUNBUFFERED=1,PYTHONPATH=/projects/bloggingapi-a05jzl environment=PORT=8001,PYTHONUNBUFFERED=1,PYTHONPATH=/projects/bloggingapi-a05jzl,USE_IN_MEMORY_DB=true
[program:app-8002]
command=python -m uvicorn main:app --host 0.0.0.0 --port 8002 --log-level debug
directory=/projects/bloggingapi-a05jzl
autostart=true
autorestart=true
startretries=5
numprocs=1
startsecs=1
redirect_stderr=true
stdout_logfile=/tmp/app-8002.log
stdout_logfile_maxbytes=50MB
stdout_logfile_backups=10
environment=PORT=8002,PYTHONUNBUFFERED=1,PYTHONPATH=/projects/bloggingapi-a05jzl,USE_IN_MEMORY_DB=true