Skip to content

Commit 5c5e6b1

Browse files
committed
use Future to wait for ready event
- streaming callback is scheduled asynchronously, so we aren't guaranteed that it's been called when request finishes - resolve on failure as well, not just success
1 parent c87de6b commit 5c5e6b1

File tree

1 file changed

+32
-5
lines changed

1 file changed

+32
-5
lines changed

binderhub/launcher.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Launch an image with a temporary user via JupyterHub
33
"""
4+
import asyncio
45
import base64
56
import json
67
import 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

Comments
 (0)