44"""
55
66import asyncio
7+ import json
78import signal
89import sys
910import traceback
1011from typing import Any , Dict , Set
1112
1213from ...http_client import AsyncClientSession , ClientSession , TooManyRequests
14+ from .rp_capture import capture
15+ from .rp_http import send_result
1316from .rp_job import _job_stop_url , get_job , get_stop_signals , handle_job
1417from .rp_logger import RunPodLogger , _reset_batch_id , _set_batch_id
1518from .worker_state import JobsProgress , IS_LOCAL_TEST
@@ -44,6 +47,8 @@ class JobScaler:
4447
4548 def __init__ (self , config : Dict [str , Any ]):
4649 self ._shutdown_event = asyncio .Event ()
50+ self ._init_ready = asyncio .Event ()
51+ self ._init_error : dict [str , Any ] | None = None
4752 self .current_concurrency = 1
4853 self .config = config
4954 self .job_progress = JobsProgress () # Cache the singleton instance
@@ -139,14 +144,26 @@ async def run(self):
139144 # Create an async session that will be closed when the worker is killed.
140145 async with AsyncClientSession () as session :
141146 # Create the worker's concurrent loops.
147+ init_task = asyncio .create_task (self ._run_init ())
142148 jobtake_task = asyncio .create_task (self .get_jobs (session ))
143149 jobrun_task = asyncio .create_task (self .run_jobs (session ))
144150 jobstop_task = asyncio .create_task (self .monitor_stop_signals (session ))
145151
146- tasks = [jobtake_task , jobrun_task , jobstop_task ]
152+ # The initializer is not a loop: an initializer with no init_timeout can
153+ # block forever, so only the request loops decide when the worker stops.
154+ await asyncio .gather (jobtake_task , jobrun_task , jobstop_task )
147155
148- # Run the worker's concurrent loops until shutdown.
149- await asyncio .gather (* tasks )
156+ # Shutting down: abandon a still-running initializer. Its work sits on a
157+ # daemon thread, so dropping it here lets the process exit.
158+ init_task .cancel ()
159+ try :
160+ await init_task
161+ except asyncio .CancelledError :
162+ pass
163+
164+ # If the initializer fails, let the platform respawn the worker.
165+ if self ._init_error is not None :
166+ sys .exit (1 )
150167
151168 def is_alive (self ):
152169 """
@@ -179,6 +196,10 @@ async def get_jobs(self, session: ClientSession):
179196 Adds jobs to the JobsQueue
180197 """
181198 while self .is_alive ():
199+ if self ._init_error is not None :
200+ # Initialization is terminal for this worker. Request draining is
201+ # owned by run_jobs; job-take has no more work to do.
202+ return
182203 await self .set_scale ()
183204
184205 jobs_needed = self .current_concurrency - self .current_occupancy ()
@@ -200,6 +221,12 @@ async def get_jobs(self, session: ClientSession):
200221 log .debug ("JobScaler.get_jobs | No jobs acquired." )
201222 continue
202223
224+ if self ._init_error is not None :
225+ # If initialization fails, fail all in-flight requests.
226+ for job in acquired_jobs :
227+ await self ._fail_job (session , job , self ._init_error )
228+ return
229+
203230 for job in acquired_jobs :
204231 await self .jobs_queue .put (job )
205232 self .job_progress .add (job )
@@ -346,6 +373,21 @@ async def handle_job(self, session: ClientSession, job: dict):
346373 try :
347374 log .debug ("Handling Job" , job ["id" ])
348375
376+ # Hold the handler until initialization finishes.
377+ if self .config .get ("initializer" ) is not None :
378+ if not await self ._wait_for_init ():
379+ log .warn (
380+ "Shutting down before initialization finished; leaving this "
381+ "request for another worker." ,
382+ job ["id" ],
383+ )
384+ return
385+
386+ if self ._init_error is not None :
387+ # If initialization fails, fail the current request and don't run the handler.
388+ await self ._fail_job (session , job , self ._init_error )
389+ return
390+
349391 await self .jobs_handler (session , self .config , job )
350392
351393 if self .config .get ("refresh_worker" , False ):
@@ -369,3 +411,57 @@ async def handle_job(self, session: ClientSession, job: dict):
369411
370412 log .debug ("Finished Job" , job ["id" ])
371413 _reset_batch_id (batch_id_token )
414+
415+ async def _wait_for_init (self ) -> bool :
416+ """Wait for initialization to finish, or for the worker to start shutting down.
417+ Returns whether initialization actually finished."""
418+ ready = asyncio .create_task (self ._init_ready .wait ())
419+ stopping = asyncio .create_task (self ._shutdown_event .wait ())
420+ try :
421+ await asyncio .wait ({ready , stopping }, return_when = asyncio .FIRST_COMPLETED )
422+ finally :
423+ ready .cancel ()
424+ stopping .cancel ()
425+ return self ._init_ready .is_set ()
426+
427+ async def _fail_job (
428+ self , session : ClientSession , job : dict , payload : Dict [str , Any ]
429+ ):
430+ """Fail a request with a structured error (reason + logs)."""
431+ log .error (f"Failing job due to init failure. | { job ['id' ]} " )
432+ await send_result (session , {"error" : json .dumps (payload )}, job , is_stream = False )
433+
434+ async def _run_init (self ):
435+ """Run initializer concurrently with the loop. Upon completion, opens the gate to run
436+ job handlers. On failure, records the reason, then drains and shuts the worker."""
437+ initializer = self .config .get ("initializer" )
438+ if initializer is None :
439+ self ._init_ready .set ()
440+ return
441+
442+ from .rp_initializer import (
443+ InitializerError ,
444+ InitializerTimeout ,
445+ build_init_failed_payload ,
446+ run_initializer_async ,
447+ )
448+
449+ try :
450+ with capture () as cap :
451+ try :
452+ await run_initializer_async (
453+ initializer , self .config .get ("init_timeout" )
454+ )
455+ except (InitializerError , InitializerTimeout ) as exc :
456+ self ._init_error = build_init_failed_payload (exc , cap .getvalue ())
457+ if self ._init_error is not None :
458+ log .error (f"init_failed | { json .dumps (self ._init_error )} " )
459+ finally :
460+ # Always release held handlers.
461+ self ._init_ready .set ()
462+
463+ if self ._init_error is not None :
464+ # Stop long-running loops before waiting for acquired requests to drain.
465+ self .kill_worker ()
466+ while self .current_occupancy () > 0 :
467+ await asyncio .sleep (0.1 )
0 commit comments