Skip to content

Commit 4e8fad5

Browse files
authored
feat: custom readiness hook method (#5337)
* feat: custom readiness hook method Signed-off-by: Frost Ming <me@frostming.com> * fix: add is_alive hook Signed-off-by: Frost Ming <me@frostming.com> --------- Signed-off-by: Frost Ming <me@frostming.com>
1 parent a868994 commit 4e8fad5

File tree

2 files changed

+77
-0
lines changed

2 files changed

+77
-0
lines changed

docs/source/build-with-bentoml/lifecycle-hooks.rst

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,44 @@ Use the ``@bentoml.on_shutdown`` decorator to specify a method as a shutdown hoo
148148
@bentoml.on_shutdown
149149
async def async_shutdown(self):
150150
print("Async cleanup actions on Service shutdown.")
151+
152+
Health check hooks
153+
^^^^^^^^^^^^^^^^^^
154+
155+
Health check hooks allow you to specify custom logic for determining when your Service is healthy and ready to handle requests.
156+
This is particularly useful when your Service depends on external resources that need to be checked before the Service can be considered operational.
157+
158+
You can define the following methods in your service class to implement health checks:
159+
160+
- ``__is_alive__``: This method is called to check if the Service is alive. It should return a boolean value indicating the Service's health status. This responds to the ``/livez`` endpoint.
161+
- ``__is_ready__``: This method is called to check if the Service is ready to handle requests. It should return a boolean value indicating the Service's readiness status. This responds to the ``/readyz`` endpoint.
162+
163+
Both can be asynchronous functions.
164+
165+
For example:
166+
167+
.. code-block:: python
168+
169+
import bentoml
170+
171+
@bentoml.service(workers=4)
172+
class HookService:
173+
def __init__(self) -> None:
174+
self.db_connection = None
175+
self.cache = None
176+
177+
@bentoml.on_startup
178+
def init_resources(self):
179+
self.db_connection = setup_database()
180+
self.cache = setup_cache()
181+
182+
def __is_ready__(self) -> bool:
183+
# Check if required resources are available
184+
if self.db_connection is None or self.cache is None:
185+
return False
186+
return self.db_connection.is_connected() and self.cache.is_available()
187+
188+
When you call the ``/readyz`` endpoint, it returns:
189+
190+
- HTTP 200 if the Service is ready (the hook returns ``True``)
191+
- HTTP 503 if the Service is not ready (the hook returns ``False``)

src/_bentoml_impl/server/app.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,12 +358,48 @@ async def destroy_instance(self, _: Starlette) -> None:
358358
set_current_service(None)
359359
await self._result_store.__aexit__(None, None, None)
360360

361+
async def livez(self, _: Request) -> Response:
362+
from starlette.exceptions import HTTPException
363+
364+
from bentoml._internal.utils import is_async_callable
365+
366+
if hasattr(self.service.inner, "__is_alive__"):
367+
assert self._service_instance is not None, "Service must be initialized"
368+
if is_async_callable(self.service.inner.__is_alive__):
369+
is_alive = await self._service_instance.__is_alive__()
370+
else:
371+
is_alive = await anyio.to_thread.run_sync(
372+
self._service_instance.__is_alive__
373+
)
374+
if not is_alive:
375+
raise HTTPException(
376+
status_code=503,
377+
detail="Service is dead because .__is_alive__() returns False.",
378+
)
379+
return await super().livez(_)
380+
361381
async def readyz(self, _: Request) -> Response:
362382
from starlette.exceptions import HTTPException
363383
from starlette.responses import PlainTextResponse
364384

385+
from bentoml._internal.utils import is_async_callable
386+
365387
from ..client import RemoteProxy
366388

389+
if hasattr(self.service.inner, "__is_ready__"):
390+
assert self._service_instance is not None, "Service must be initialized"
391+
if is_async_callable(self.service.inner.__is_ready__):
392+
is_ready = await self._service_instance.__is_ready__()
393+
else:
394+
is_ready = await anyio.to_thread.run_sync(
395+
self._service_instance.__is_ready__
396+
)
397+
if not is_ready:
398+
raise HTTPException(
399+
status_code=503,
400+
detail="Service is not ready because .__is_ready__() returns False.",
401+
)
402+
367403
if BentoMLContainer.api_server_config.runner_probe.enabled.get():
368404
dependency_statuses: list[t.Coroutine[None, None, bool]] = []
369405
for dependency in self.service.dependencies.values():

0 commit comments

Comments
 (0)