How does one add asgi middlewares to the services ...
# ask-for-help
j
How does one add asgi middlewares to the services created using the
@bentoml.service
decorator? Since we do not directly create an instance of the
Service
, I don't think we can directly use the
add_asgi_middleware
method, neither does adding the attribute
middlewares
to our own class seem to work e.g. I tried the following and it did not work
Copy code
@bentoml.service
class MyService:
    middlewares = [(MyMiddleware, {})]
never mind, i managed to figure it out...i just implemented a decorator to use after the
@bentoml.service
decorator, such that by then, i can just use
svc.add_asgi_middleware()
. Full code that implements readiness probe decorators:
Copy code
from __future__ import annotations

import inspect
import json
import logging
import os
import traceback
import typing as t

import bentoml
from starlette.requests import Request
from starlette.responses import JSONResponse


def create_reroute_middleware(
    origin_route: str,
    destination_route: str,
    api_input: dict = {"input": ""},
):
    """Create reroute middleware

    Transforms a GET request from the origin route to a POST request to the destination
    route. This can be used to create or override GET routes, transforming them
    into POST requests against routes defined by the @bentoml.api decorator.

    For instance, we could intercept a call to /readyz and send a POST request
    to a custom route e.g. /myreadyz, which under the hood calls /predict.

    Parameters
    ----------
    origin_route
        Route that the user / client would call using a GET request
    destination_route
        Route to forward the request to using a POST request
    api_input
        Input to send to the destination API
    """
    class RerouteMiddleWare:
        def __init__(self, app):
            <http://self.app|self.app> = app

        async def __call__(self, scope, receive, send):
            async def _receive():
                await receive()
                message = {
                    "type": "http.request",
                    "body": json.dumps(api_input).encode(),
                }
                return message

            if "path" in scope and scope["method"] == "GET" and scope["path"] == origin_route:
                scope["path"] = destination_route
                scope["method"] = "POST"
                return await <http://self.app|self.app>(scope, _receive, send)

            return await <http://self.app|self.app>(scope, receive, send)

    return RerouteMiddleWare


def inject_myreadyz_route(api_input: dict, *, method_to_call: str = "predict"):
    """Inject a POST /myreadyz route

    This decorator should be used BEFORE the @bentoml.service decorator, to inject
    a route into the service.

    Parameters
    ----------
    api_input
        Input to send to `method_to_call`
    method_to_call
        Method to call, usually your primary predict method inside the service

    Example
    -------
    >>> @bentoml.service
        @inject_myreadyz_route(api_input={"input": [1, 2, 3]})
        class MyService:
            ...
    """
    def decorator(cls):
        m = getattr(cls, method_to_call)

        def handle_exception(e: Exception, ctx: bentoml.Context) -> str:
            logging.exception(e)
            ctx.response.status_code = 500
            return json.dumps(
                {
                    "status": "Not OK",
                    "error": repr(e),
                    "traceback": traceback.format_exc(),
                }
            )

        if inspect.iscoroutinefunction(m):
            async def myreadyz(self, input: str, ctx: bentoml.Context) -> str:
                try:
                    await getattr(self, method_to_call)(**api_input)
                    return json.dumps({"status": "OK"})
                except Exception as e:
                    return handle_exception(e=e, ctx=ctx)
        else:
            def myreadyz(self, input: str, ctx: bentoml.Context) -> str:
                try:
                    getattr(self, method_to_call)(**api_input)
                    return json.dumps({"status": "OK"})
                except Exception as e:
                    return handle_exception(e=e, ctx=ctx)

        setattr(cls, "myreadyz", bentoml.api(myreadyz, batchable=False))
        return cls

    return decorator


def inject_readyz_middleware(cls):
    """Inject readyz middleware

    Intercepts GET /readyz and sends them to POST /myreadyz.

    Should be used AFTER the @bentoml.service decorator

    Example
    -------
    >>> @inject_readyz_middleware
        @bentoml.service
        @inject_myreadyz_route(api_input={"input": [1, 2, 3]})
        class MyService:
            ...
    """
    ReadyzMiddleware = create_reroute_middleware(
        origin_route="/readyz",
        destination_route="/myreadyz",
        api_input={"input": ""},
    )
    cls.add_asgi_middleware(ReadyzMiddleware)
    return cls


@inject_readyz_middleware
@bentoml.service(resources={"cpu": "200m", "memory": "1Gi"}, traffic={"timeout": 10})
@inject_myreadyz_route(api_input={"input": "test"}, method_to_call="predict")
class MyService:
    @bentoml.api(batchable=False)
    def predict(self, input: str) -> list[str]:
        ...
j
Hello, getting back to this. you could
Copy code
class MyService:
    ...

MyService.add_asgi_middleware(...)
we are in the process of documenting this! this behaves the same as svc.add_asgi_middleware()
j
Oh thanks, that's good to know. Would bentoml consider adding a way for users to implement a method within their service to modify the behaviour of the readiness check? I believe that a properly configured readiness probe is important for service reliability. I am not familiar enough with bentoml 1.2, but for previous versions, the default readiness probe behaviour marks pods as ready before runners are loaded properly and ready to accept requests (the entrypoint service has started up, but models may still be in the process of loading), resulting in your k8s cluster routing requests to pods that are still in the process of starting up. Rather than having everyone implement this middleware in their code, it would be nice if we could for instance implement a
is_ready
method within our entrypoint service and have bentoml use this method in the
/readyz
route.