Compare commits

...

5 Commits

Author SHA1 Message Date
8b91c75b32 fix: add module-level database initialization for uvicorn reliability
Add database initialization at module load time to ensure it runs
regardless of how uvicorn handles the lifespan context manager.

Issue: The lifespan function wasn't being triggered consistently when
uvicorn loads the app module, causing "no such table: jobs" errors.

Solution: Initialize database when the module is imported (after app
creation), providing a reliable fallback that works in all deployment
scenarios.

This provides defense-in-depth:
1. Lifespan function (ideal path)
2. Module-level initialization (fallback/guarantee)

Both paths check deployment mode and call the appropriate init function.
2025-11-02 15:36:12 -05:00
bdb3f6a6a2 refactor: move database initialization from entrypoint to application
Move database initialization logic from shell script to Python application
lifespan, following separation of concerns and improving maintainability.

Benefits:
- Single source of truth for database initialization (api/main.py lifespan)
- Better testability - Python code vs shell scripts
- Clearer logging with structured messages
- Easier to debug and maintain
- Infrastructure (entrypoint.sh) focuses on service orchestration
- Application (api/main.py) owns its data layer

Changes:
- Removed database init from entrypoint.sh
- Enhanced lifespan function with detailed logging
- Simplified entrypoint script (now 4 steps instead of 5)
- All tests pass (28/28 API endpoint tests)
2025-11-02 15:32:53 -05:00
3502a7ffa8 fix: respect dev mode in entrypoint database initialization
- Update entrypoint.sh to check DEPLOYMENT_MODE before initializing database
- DEV mode: calls initialize_dev_database() which resets the database
- PROD mode: calls initialize_database() which preserves existing data
- Adds clear logging to show which mode is being used

This ensures the dev database is properly reset on container startup,
matching the behavior of the lifespan function in api/main.py.
2025-11-02 15:30:11 -05:00
68d9f241e1 fix: use closure to capture db_path in lifespan context manager
- Fix lifespan function to access db_path from create_app scope via closure
- Prevents "no such table: jobs" error by ensuring database initialization runs
- Previous version tried to access app.state.db_path before it was set

The issue was that app.state is set after FastAPI instantiation, but the
lifespan function needs the db_path during startup. Using closure allows
the lifespan function to capture db_path from the create_app function scope.
2025-11-02 15:24:29 -05:00
4fec5826bb fix: initialize dev database on API startup to prevent stale job blocking
- Add database initialization to API lifespan event handler
- DEV mode: Reset database on startup (unless PRESERVE_DEV_DATA=true)
- PROD mode: Ensure database schema exists
- Migrate from deprecated @app.on_event to modern lifespan context manager
- Fixes 400 error "Another simulation job is already running" on fresh container starts

This ensures the dev database is reset when the API server starts in dev mode,
preventing stale "running" or "pending" jobs from blocking new job creation.
2025-11-02 15:20:51 -05:00
2 changed files with 54 additions and 17 deletions

View File

@@ -17,6 +17,7 @@ from pathlib import Path
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field, field_validator
from contextlib import asynccontextmanager
from api.job_manager import JobManager
from api.simulation_worker import SimulationWorker
@@ -127,21 +128,46 @@ def create_app(
Returns:
Configured FastAPI app
"""
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Initialize database on startup, cleanup on shutdown if needed"""
from tools.deployment_config import is_dev_mode, get_db_path
from api.database import initialize_dev_database, initialize_database
# Startup - use closure to access db_path from create_app scope
logger.info("🚀 FastAPI application starting...")
logger.info("📊 Initializing database...")
if is_dev_mode():
# Initialize dev database (reset unless PRESERVE_DEV_DATA=true)
logger.info(" 🔧 DEV mode detected - initializing dev database")
dev_db_path = get_db_path(db_path)
initialize_dev_database(dev_db_path)
log_dev_mode_startup_warning()
else:
# Ensure production database schema exists
logger.info(" 🏭 PROD mode - ensuring database schema exists")
initialize_database(db_path)
logger.info("✅ Database initialized")
logger.info("🌐 API server ready to accept requests")
yield
# Shutdown (if needed in future)
logger.info("🛑 FastAPI application shutting down...")
app = FastAPI(
title="AI-Trader Simulation API",
description="REST API for triggering and monitoring AI trading simulations",
version="1.0.0"
version="1.0.0",
lifespan=lifespan
)
# Store paths in app state
app.state.db_path = db_path
app.state.config_path = config_path
@app.on_event("startup")
async def startup_event():
"""Display DEV mode warning on startup if applicable"""
log_dev_mode_startup_warning()
@app.post("/simulate/trigger", response_model=SimulateTriggerResponse, status_code=200)
async def trigger_simulation(request: SimulateTriggerRequest):
"""
@@ -496,11 +522,27 @@ def create_app(
# Create default app instance
app = create_app()
# Ensure database is initialized when module is loaded
# This handles cases where lifespan might not be triggered properly
logger.info("🔧 Module-level database initialization check...")
from tools.deployment_config import is_dev_mode, get_db_path
from api.database import initialize_dev_database, initialize_database
_db_path = app.state.db_path
if is_dev_mode():
logger.info(" 🔧 DEV mode - initializing dev database at module load")
_dev_db_path = get_db_path(_db_path)
initialize_dev_database(_dev_db_path)
else:
logger.info(" 🏭 PROD mode - ensuring database exists at module load")
initialize_database(_db_path)
logger.info("✅ Module-level database initialization complete")
if __name__ == "__main__":
import uvicorn
# Display DEV mode warning if applicable
log_dev_mode_startup_warning()
# Note: Database initialization happens in lifespan AND at module load
# for maximum reliability
uvicorn.run(app, host="0.0.0.0", port=8080)

View File

@@ -36,12 +36,7 @@ fi
echo "✅ Environment variables validated"
# Step 1: Initialize database
echo "📊 Initializing database..."
python -c "from api.database import initialize_database; initialize_database('data/jobs.db')"
echo "✅ Database initialized"
# Step 2: Merge and validate configuration
# Step 1: Merge and validate configuration
echo "🔧 Merging and validating configuration..."
python -c "from tools.config_merger import merge_and_validate; merge_and_validate()" || {
echo "❌ Configuration validation failed"
@@ -50,7 +45,7 @@ python -c "from tools.config_merger import merge_and_validate; merge_and_validat
export CONFIG_PATH=/tmp/runtime_config.json
echo "✅ Configuration validated and merged"
# Step 3: Start MCP services in background
# Step 2: Start MCP services in background
echo "🔧 Starting MCP services..."
cd /app
python agent_tools/start_mcp_services.py &
@@ -59,11 +54,11 @@ MCP_PID=$!
# Setup cleanup trap before starting uvicorn
trap "echo '🛑 Stopping services...'; kill $MCP_PID 2>/dev/null; exit 0" EXIT SIGTERM SIGINT
# Step 4: Wait for services to initialize
# Step 3: Wait for services to initialize
echo "⏳ Waiting for MCP services to start..."
sleep 3
# Step 5: Start FastAPI server with uvicorn (this blocks)
# Step 4: Start FastAPI server with uvicorn (this blocks)
# Note: Container always uses port 8080 internally
# The API_PORT env var only affects the host port mapping in docker-compose.yml
echo "🌐 Starting FastAPI server on port 8080..."