@@ -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
138149async 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