Skip to content

Commit baca714

Browse files
Fix Render deploy timeout: lazy-load model on first request instead of startup
1 parent efbab37 commit baca714

1 file changed

Lines changed: 19 additions & 93 deletions

File tree

src/api/main.py

Lines changed: 19 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -130,106 +130,32 @@ def get_pipeline() -> InferencePipeline:
130130
return _pipeline
131131

132132

133+
async def ensure_model_loaded() -> InferencePipeline:
134+
"""Lazy-load the model on first forecast request."""
135+
global _pipeline
136+
if _pipeline is None or not _pipeline.is_loaded:
137+
config = get_app_config()
138+
_pipeline = InferencePipeline(config)
139+
_pipeline.load_model(MODEL_DIR)
140+
log.info("Model lazy-loaded: version=%s", _pipeline.model_version)
141+
return _pipeline
142+
143+
133144
# ═══════════════════════════════════════════════════════════
134145
# FastAPI Application
135146
# ═══════════════════════════════════════════════════════════
136147

137148
@asynccontextmanager
138149
async def lifespan(app: FastAPI):
139-
"""Startup: load the ML model. Shutdown: clean up resources."""
150+
"""Startup: instant health check. Model loaded lazily on first forecast."""
140151
global _pipeline, _start_time
141152

142-
log.info("=" * 60)
143-
log.info("Demand Forecasting API v%s — Starting up", "2.1.0")
144-
log.info("=" * 60)
145-
146-
# ── Load Model ──
147-
try:
148-
config = get_app_config()
149-
_pipeline = InferencePipeline(config)
150-
_pipeline.load_model(MODEL_DIR)
151-
log.info("Model loaded successfully. Version: %s", _pipeline.model_version)
152-
except Exception as e:
153-
log.error("Failed to load model: %s", e)
154-
log.warning("API will start but predictions may fall back to demo data.")
155-
_pipeline = None
156-
157-
# ── Kafka Consumer (Phase 2) ──
158-
shutdown_event = asyncio.Event()
159-
consumer_task = None
160-
if settings.kafka_consumer_enabled:
161-
from src.db.session import get_db
162-
from src.streaming.consumer import consume_sales_events
163-
164-
db = get_db()
165-
if not db.is_connected:
166-
await db.connect()
167-
168-
consumer_task = asyncio.create_task(
169-
consume_sales_events(
170-
db=db,
171-
bootstrap_servers=settings.kafka_bootstrap_servers,
172-
topic=settings.kafka_sales_topic,
173-
group_id=settings.kafka_consumer_group,
174-
shutdown_event=shutdown_event,
175-
)
176-
)
177-
log.info("Kafka consumer started on %s", settings.kafka_sales_topic)
178-
179-
# ── Redis Cache (Phase 3) ──
180-
if settings.redis_enabled:
181-
cache = get_cache()
182-
await cache.connect()
183-
log.info("Redis cache ready: %s", settings.redis_url)
184-
185-
# ── Drift Scheduler (Phase 2) ──
186-
scheduler = None
187-
if settings.drift_check_enabled:
188-
try:
189-
from apscheduler.schedulers.asyncio import AsyncIOScheduler
190-
191-
from src.monitoring.drift_checker import (
192-
get_default_windows,
193-
run_drift_check,
194-
)
195-
196-
scheduler = AsyncIOScheduler()
197-
scheduler.add_job(
198-
run_drift_check,
199-
"cron",
200-
hour=settings.drift_check_hour,
201-
minute=0,
202-
kwargs={
203-
"model_id": 1,
204-
**dict(zip(
205-
["reference_start", "reference_end", "current_start", "current_end"],
206-
get_default_windows(settings.drift_reference_days, settings.drift_current_days),
207-
)),
208-
},
209-
id="daily_drift_check",
210-
)
211-
scheduler.start()
212-
log.info("Drift scheduler started (daily at %02d:00)", settings.drift_check_hour)
213-
except ImportError:
214-
log.warning("apscheduler not installed. Drift scheduler disabled.")
215-
153+
log.info("Demand Forecasting API v3.1.0 — Starting")
154+
_pipeline = None # Lazy-load on first forecast request
216155
_start_time = datetime.now(timezone.utc)
217-
log.info("API ready — listening on %s:%s", settings.api_host, settings.api_port)
156+
log.info("API ready (model lazy-loads on first forecast)")
218157
yield
219-
220-
# ── Shutdown ──
221158
log.info("API shutting down")
222-
if consumer_task is not None:
223-
shutdown_event.set()
224-
consumer_task.cancel()
225-
try:
226-
await consumer_task
227-
except asyncio.CancelledError:
228-
pass
229-
if scheduler is not None:
230-
scheduler.shutdown(wait=False)
231-
if settings.redis_enabled:
232-
await get_cache().disconnect()
233159
_pipeline = None
234160

235161

@@ -282,7 +208,7 @@ async def demand_forecast(req: ForecastRequest):
282208
Ridge stacking for the final prediction.
283209
"""
284210
try:
285-
pipeline = get_pipeline()
211+
pipeline = await ensure_model_loaded()
286212
result = pipeline.predict(
287213
product_id=req.product_id,
288214
horizon_days=req.horizon_days,
@@ -334,7 +260,7 @@ async def order_forecast(days: int = Query(default=7, ge=1, le=30)):
334260
order volume prediction with day-of-week effects.
335261
"""
336262
try:
337-
pipeline = get_pipeline()
263+
pipeline = await ensure_model_loaded()
338264
result = pipeline.predict(product_id="orders", horizon_days=days)
339265

340266
predictions = []
@@ -374,7 +300,7 @@ async def electricity_forecast(hours: int = Query(default=24, ge=1, le=168)):
374300
energy-specific models with weather covariates.
375301
"""
376302
try:
377-
pipeline = get_pipeline()
303+
pipeline = await ensure_model_loaded()
378304
result = pipeline.predict(product_id="electricity", horizon_days=hours // 24 + 1)
379305

380306
predictions = []
@@ -421,7 +347,7 @@ async def model_explainability(
421347
to the ensemble prediction.
422348
"""
423349
try:
424-
pipeline = get_pipeline()
350+
pipeline = await ensure_model_loaded()
425351
result = pipeline.predict(product_id=product_id, horizon_days=horizon_days)
426352

427353
# Get LightGBM feature importance if available

0 commit comments

Comments
 (0)