11"""
22Launch an image with a temporary user via JupyterHub
33"""
4+ import asyncio
45import base64
56import json
67import random
@@ -233,7 +234,14 @@ async def launch(
233234
234235 # start server
235236 app_log .info (f"Starting server{ _server_name } for user { username } with image { image } " )
236- ready_event_container = []
237+ ready_event_future = asyncio .Future ()
238+
239+ def _cancel_ready_event (f = None ):
240+ if not ready_event_future .done ():
241+ if f and f .exception ():
242+ ready_event_future .set_exception (f .exception ())
243+ else :
244+ ready_event_future .cancel ()
237245 try :
238246 resp = await self .api_request (
239247 'users/{}/servers/{}' .format (escaped_username , server_name ),
@@ -257,16 +265,26 @@ async def handle_chunk(chunk):
257265 event = json .loads (line .split (":" , 1 )[1 ])
258266 if event_callback :
259267 await event_callback (event )
268+
269+ # stream ends when server is ready or fails
260270 if event .get ("ready" , False ):
261- # stream ends when server is ready
262- ready_event_container .append (event )
271+ if not ready_event_future .done ():
272+ ready_event_future .set_result (event )
273+ elif event .get ("failed" , False ):
274+ if not ready_event_future .done ():
275+ ready_event_future .set_exception (
276+ web .HTTPError (
277+ 500 , event .get ("message" , "unknown error" )
278+ )
279+ )
263280
264281 url_parts = ["users" , username ]
265282 if server_name :
266283 url_parts .extend (["servers" , server_name , "progress" ])
267284 else :
268285 url_parts .extend (["server/progress" ])
269286 progress_api_url = url_path_join (* url_parts )
287+ self .log .debug ("Requesting progress for {username}: {progress_api_url}" )
270288 resp_future = self .api_request (
271289 progress_api_url ,
272290 streaming_callback = handle_chunk ,
@@ -277,12 +295,14 @@ async def handle_chunk(chunk):
277295 timedelta (seconds = self .launch_timeout ), resp_future
278296 )
279297 except (gen .TimeoutError , TimeoutError ):
298+ _cancel_ready_event ()
280299 raise web .HTTPError (
281300 500 ,
282301 f"Image { image } for user { username } took too long to launch" ,
283302 )
284303
285304 except HTTPError as e :
305+ _cancel_ready_event ()
286306 if e .response :
287307 body = e .response .body
288308 else :
@@ -292,13 +312,20 @@ async def handle_chunk(chunk):
292312 f"Error starting server{ _server_name } for user { username } : { e } \n { body } "
293313 )
294314 raise web .HTTPError (500 , f"Failed to launch image { image } " )
315+ except Exception :
316+ _cancel_ready_event ()
317+ raise
295318
296319 # verify that the server is running!
297- if not ready_event_container :
320+ try :
321+ # this should already be done, but it's async so wait a finite time
322+ ready_event = await gen .with_timeout (
323+ timedelta (seconds = 5 ), ready_event_future
324+ )
325+ except (gen .TimeoutError , TimeoutError ):
298326 raise web .HTTPError (
299327 500 , f"Image { image } for user { username } failed to launch"
300328 )
301- ready_event = ready_event_container [0 ]
302329
303330 data ["url" ] = url_path_join (self .hub_url , ready_event ["url" ])
304331 return data
0 commit comments