2626from jumpstarter .common .streams import connect_router_stream
2727from jumpstarter .config .tls import TLSConfigV1Alpha1
2828from jumpstarter .driver import Driver
29- from jumpstarter .exporter .hooks import HookContext , HookExecutor
29+ from jumpstarter .exporter .hooks import HookContext , HookExecutionError , HookExecutor
3030from jumpstarter .exporter .session import Session
3131
3232logger = logging .getLogger (__name__ )
@@ -49,6 +49,7 @@ class Exporter(AsyncContextManagerMixin, Metadata):
4949 _pre_lease_ready : Event | None = field (init = False , default = None )
5050 _current_status : ExporterStatus = field (init = False , default = ExporterStatus .OFFLINE )
5151 _current_session : Session | None = field (init = False , default = None )
52+ _session_socket_path : str | None = field (init = False , default = None )
5253
5354 def stop (self , wait_for_lease_exit = False , should_unregister = False ):
5455 """Signal the exporter to stop.
@@ -183,13 +184,18 @@ async def listen(retries=5, backoff=3):
183184
184185 tg .start_soon (listen )
185186
186- # Wait for pre-lease hook to complete before processing connections
187- if self ._pre_lease_ready is not None :
188- logger .info ("Waiting for pre-lease hook to complete before accepting connections" )
189- await self ._pre_lease_ready .wait ()
190- logger .info ("Pre-lease hook completed, now accepting connections" )
191-
187+ # Create session before hooks run
192188 async with self .session () as path :
189+ # Store socket path for hook execution
190+ self ._session_socket_path = path
191+
192+ # Wait for before-lease hook to complete before processing connections
193+ if self ._pre_lease_ready is not None :
194+ logger .info ("Waiting for before-lease hook to complete before accepting connections" )
195+ await self ._pre_lease_ready .wait ()
196+ logger .info ("before-lease hook completed, now accepting connections" )
197+
198+ # Process client connections
193199 async for request in listen_rx :
194200 logger .info ("Handling new connection request on lease %s" , lease_name )
195201 tg .start_soon (
@@ -231,19 +237,15 @@ async def status(retries=5, backoff=3):
231237 tg .start_soon (status )
232238 async for status in status_rx :
233239 if self .lease_name != "" and self .lease_name != status .lease_name :
234- # Post -lease hook for the previous lease
240+ # After -lease hook for the previous lease
235241 if self .hook_executor and self ._current_client_name :
236242 hook_context = HookContext (
237243 lease_name = self .lease_name ,
238244 client_name = self ._current_client_name ,
239245 )
240- # Shield the post -lease hook from cancellation and await it
246+ # Shield the after -lease hook from cancellation and await it
241247 with CancelScope (shield = True ):
242- await self ._update_status (ExporterStatus .AFTER_LEASE_HOOK , "Running afterLease hooks" )
243- # Pass the current session to hook executor for logging
244- self .hook_executor .main_session = self ._current_session
245- await self .hook_executor .execute_post_lease_hook (hook_context )
246- await self ._update_status (ExporterStatus .AVAILABLE , "Available for new lease" )
248+ await self .run_after_lease_hook (hook_context )
247249
248250 self .lease_name = status .lease_name
249251 logger .info ("Lease status changed, killing existing connections" )
@@ -267,37 +269,14 @@ async def status(retries=5, backoff=3):
267269 logger .info ("Currently leased by %s under %s" , status .client_name , status .lease_name )
268270 self ._current_client_name = status .client_name
269271
270- # Pre -lease hook when transitioning from unleased to leased
272+ # Before -lease hook when transitioning from unleased to leased
271273 if not previous_leased :
272274 if self .hook_executor :
273275 hook_context = HookContext (
274276 lease_name = status .lease_name ,
275277 client_name = status .client_name ,
276278 )
277-
278- # Start pre-lease hook asynchronously
279- async def run_before_lease_hook (hook_ctx ):
280- try :
281- await self ._update_status (
282- ExporterStatus .BEFORE_LEASE_HOOK , "Running beforeLease hooks"
283- )
284- # Pass the current session to hook executor for logging
285- self .hook_executor .main_session = self ._current_session
286- await self .hook_executor .execute_pre_lease_hook (hook_ctx )
287- await self ._update_status (ExporterStatus .LEASE_READY , "Ready for commands" )
288- logger .info ("beforeLease hook completed successfully" )
289- except Exception as e :
290- logger .error ("beforeLease hook failed: %s" , e )
291- # Still transition to ready even if hook fails
292- await self ._update_status (
293- ExporterStatus .LEASE_READY , f"Ready (beforeLease hook failed: { e } )"
294- )
295- finally :
296- # Always set the event to unblock connections
297- if self ._pre_lease_ready :
298- self ._pre_lease_ready .set ()
299-
300- tg .start_soon (run_before_lease_hook , hook_context )
279+ tg .start_soon (self .run_before_lease_hook , self , hook_context )
301280 else :
302281 # No hook configured, set event immediately
303282 await self ._update_status (ExporterStatus .LEASE_READY , "Ready for commands" )
@@ -306,18 +285,21 @@ async def run_before_lease_hook(hook_ctx):
306285 else :
307286 logger .info ("Currently not leased" )
308287
309- # Post -lease hook when transitioning from leased to unleased
288+ # After -lease hook when transitioning from leased to unleased
310289 if previous_leased and self .hook_executor and self ._current_client_name :
311290 hook_context = HookContext (
312291 lease_name = self .lease_name ,
313292 client_name = self ._current_client_name ,
314293 )
315- # Shield the post -lease hook from cancellation and await it
294+ # Shield the after -lease hook from cancellation and await it
316295 with CancelScope (shield = True ):
317296 await self ._update_status (ExporterStatus .AFTER_LEASE_HOOK , "Running afterLease hooks" )
318297 # Pass the current session to hook executor for logging
319298 self .hook_executor .main_session = self ._current_session
320- await self .hook_executor .execute_post_lease_hook (hook_context )
299+ # Use session socket if available, otherwise create new session
300+ await self .hook_executor .execute_after_lease_hook (
301+ hook_context , socket_path = self ._session_socket_path
302+ )
321303 await self ._update_status (ExporterStatus .AVAILABLE , "Available for new lease" )
322304
323305 self ._current_client_name = ""
@@ -330,3 +312,69 @@ async def run_before_lease_hook(hook_ctx):
330312
331313 self ._previous_leased = current_leased
332314 self ._tg = None
315+
316+ async def run_before_lease_hook (self , hook_ctx : HookContext ):
317+ """
318+ Execute the before-lease hook for the current exporter session.
319+
320+ Args:
321+ hook_ctx (HookContext): The current hook execution context
322+ """
323+ try :
324+ await self ._update_status (ExporterStatus .BEFORE_LEASE_HOOK , "Running beforeLease hooks" )
325+ # Pass the current session to hook executor for logging
326+ self .hook_executor .main_session = self ._current_session
327+
328+ # Wait for socket path to be available
329+ while self ._session_socket_path is None :
330+ await sleep (0.1 )
331+
332+ # Execute hook with main session socket
333+ await self .hook_executor .execute_before_lease_hook (hook_ctx , socket_path = self ._session_socket_path )
334+ await self ._update_status (ExporterStatus .LEASE_READY , "Ready for commands" )
335+ logger .info ("beforeLease hook completed successfully" )
336+ except HookExecutionError as e :
337+ # Hook failed with on_failure='block' - end lease and set failed status
338+ logger .error ("beforeLease hook failed (on_failure=block): %s" , e )
339+ await self ._update_status (
340+ ExporterStatus .BEFORE_LEASE_HOOK_FAILED , f"beforeLease hook failed (on_failure=block): { e } "
341+ )
342+ # Note: We don't take the exporter offline for before_lease hook failures
343+ # The lease is simply not ready, and the exporter remains available for future leases
344+ except Exception as e :
345+ # Unexpected error during hook execution
346+ logger .error ("beforeLease hook failed with unexpected error: %s" , e , exc_info = True )
347+ await self ._update_status (ExporterStatus .BEFORE_LEASE_HOOK_FAILED , f"beforeLease hook failed: { e } " )
348+ finally :
349+ # Always set the event to unblock connections
350+ if self ._pre_lease_ready :
351+ self ._pre_lease_ready .set ()
352+
353+ async def run_after_lease_hook (self , hook_ctx : HookContext ):
354+ """
355+ Execute the after-lease hook for the current exporter session.
356+
357+ Args:
358+ hook_ctx (HookContext): The current hook execution context
359+ """
360+ try :
361+ await self ._update_status (ExporterStatus .AFTER_LEASE_HOOK , "Running afterLease hooks" )
362+ # Pass the current session to hook executor for logging
363+ self .hook_executor .main_session = self ._current_session
364+ # Use session socket if available, otherwise create new session
365+ await self .hook_executor .execute_after_lease_hook (hook_ctx , socket_path = self ._session_socket_path )
366+ await self ._update_status (ExporterStatus .AVAILABLE , "Available for new lease" )
367+ logger .info ("afterLease hook completed successfully" )
368+ except HookExecutionError as e :
369+ # Hook failed with on_failure='block' - set failed status and shut down exporter
370+ logger .error ("afterLease hook failed (on_failure=block): %s" , e )
371+ await self ._update_status (
372+ ExporterStatus .AFTER_LEASE_HOOK_FAILED , f"afterLease hook failed (on_failure=block): { e } "
373+ )
374+ # Shut down the exporter after after_lease hook failure with on_failure='block'
375+ logger .error ("Shutting down exporter due to afterLease hook failure" )
376+ self .stop ()
377+ except Exception as e :
378+ # Unexpected error during hook execution
379+ logger .error ("afterLease hook failed with unexpected error: %s" , e , exc_info = True )
380+ await self ._update_status (ExporterStatus .AFTER_LEASE_HOOK_FAILED , f"afterLease hook failed: { e } " )
0 commit comments