From b8cf42dfa94052b85be235a96430de22ee6d1c9d Mon Sep 17 00:00:00 2001 From: Frost Ming <me@frostming.com> Date: Fri, 27 Sep 2024 11:59:04 +0800 Subject: [PATCH 1/2] fix: expose errors occurring before the first yield of stream response Signed-off-by: Frost Ming <me@frostming.com> --- src/_bentoml_sdk/io_models.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/_bentoml_sdk/io_models.py b/src/_bentoml_sdk/io_models.py index f21713f531e..9fd94dda7f3 100644 --- a/src/_bentoml_sdk/io_models.py +++ b/src/_bentoml_sdk/io_models.py @@ -173,6 +173,7 @@ async def from_http_request(cls, request: Request, serde: Serde) -> IODescriptor @classmethod async def to_http_response(cls, obj: t.Any, serde: Serde) -> Response: """Convert a output value to HTTP response""" + import itertools import mimetypes from pydantic import RootModel @@ -183,10 +184,24 @@ async def to_http_response(cls, obj: t.Any, serde: Serde) -> Response: from _bentoml_impl.serde import JSONSerde if inspect.isasyncgen(obj): + try: + # try if there is any error before the first yield + # if so, the error can be surfaced in the response + first_chunk = await obj.__anext__() + except StopAsyncIteration: + pre_chunks = [] + else: + pre_chunks = [first_chunk] + + async def gen() -> t.AsyncGenerator[t.Any, None]: + for chunk in pre_chunks: + yield chunk + async for chunk in obj: + yield chunk async def async_stream() -> t.AsyncGenerator[str | bytes, None]: try: - async for item in obj: + async for item in gen(): if isinstance(item, (str, bytes)): yield item else: @@ -201,6 +216,11 @@ async def async_stream() -> t.AsyncGenerator[str | bytes, None]: return StreamingResponse(async_stream(), media_type=cls.mime_type()) elif inspect.isgenerator(obj): + trial, obj = itertools.tee(obj) + try: + next(trial) # try if there is any error before the first yield + except StopIteration: + pass def content_stream() -> t.Generator[str | bytes, None, None]: try: From 08935a8ed4f785d682a090c4d9465dfc8d954b0c Mon Sep 17 00:00:00 2001 From: Frost Ming <me@frostming.com> Date: Fri, 27 Sep 2024 17:32:10 +0800 Subject: [PATCH 2/2] fix: log endpoint and console url when developing Signed-off-by: Frost Ming <me@frostming.com> --- src/bentoml/_internal/cloud/deployment.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/bentoml/_internal/cloud/deployment.py b/src/bentoml/_internal/cloud/deployment.py index f14837dc953..ef7e664f353 100644 --- a/src/bentoml/_internal/cloud/deployment.py +++ b/src/bentoml/_internal/cloud/deployment.py @@ -832,6 +832,8 @@ def is_bento_changed(bento_info: Bento) -> bool: spinner = Spinner(console=console) needs_update = is_bento_changed(bento_info) + spinner.log(f"💻 View Dashboard: {self.admin_console}") + endpoint_url: str | None = None try: spinner.start() upload_id = spinner.transmission_progress.add_task( @@ -861,6 +863,9 @@ def is_bento_changed(bento_info: Bento) -> bool: requirements_hash = self._init_deployment_files( bento_dir, spinner=spinner ) + if endpoint_url is None: + endpoint_url = self.get_endpoint_urls(False)[0] + spinner.log(f"🌐 Endpoint: {endpoint_url}") with self._tail_logs(spinner.console): spinner.update("👀 Watching for changes") for changes in watchfiles.watch( @@ -937,13 +942,11 @@ def is_bento_changed(bento_info: Bento) -> bool: return except KeyboardInterrupt: spinner.log( - "The deployment is still running, view the dashboard:\n" - f" [blue]{self.admin_console}[/]\n\n" - "Next steps:\n" - "* Push the bento to BentoCloud:\n" - " [blue]$ bentoml build --push[/]\n\n" + "\nWatcher stopped. Next steps:\n" "* Attach to this deployment again:\n" f" [blue]$ bentoml develop --attach {self.name} --cluster {self.cluster}[/]\n\n" + "* Push the bento to BentoCloud:\n" + " [blue]$ bentoml build --push[/]\n\n" "* Terminate the deployment:\n" f" [blue]$ bentoml deployment terminate {self.name} --cluster {self.cluster}[/]" )