Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow starting server with bentoml.Service instance #3829

Merged
Merged
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Developer Guide

Before getting started, check out the `#bentoml-contributors` channel in the [BentoML community slack](https://l.linklyhq.com/l/ktOh).
Before getting started, check out the `#bentoml-contributors` channel in the [BentoML community slack](https://l.bentoml.com/join-slack).

If you are interested in contributing to existing issues and feature requets, check out the [good-first-issue](https://github.com/bentoml/BentoML/issues?q=is%3Aopen+is%3Aissue+label%3Agood-first-issue) and [help-wanted](https://github.com/bentoml/BentoML/issues?q=is%3Aopen+is%3Aissue+label%3Ahelp-wanted) issues list.

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ for the easiest and fastest way to deploy your bento.
- [Examples](https://github.com/bentoml/BentoML/tree/main/examples) - Gallery of sample projects using BentoML
- [ML Framework Guides](https://docs.bentoml.org/en/latest/frameworks/index.html) - Best practices and example usages by the ML framework of your choice
- [Advanced Guides](https://docs.bentoml.org/en/latest/guides/index.html) - Learn about BentoML's internals, architecture and advanced features
- Need help? [Join BentoML Community Slack 💬](https://l.linklyhq.com/l/ktOh)
- Need help? [Join BentoML Community Slack 💬](https://l.bentoml.com/join-slack)

---

Expand Down Expand Up @@ -140,7 +140,7 @@ For a more detailed user guide, check out the [BentoML Tutorial](https://docs.be

## Community

- For general questions and support, join the [community slack](https://l.linklyhq.com/l/ktOh).
- For general questions and support, join the [community slack](https://l.bentoml.com/join-slack).
- To receive release notification, star & watch the BentoML project on [GitHub](https://github.com/bentoml/BentoML).
- To report a bug or suggest a feature request, use [GitHub Issues](https://github.com/bentoml/BentoML/issues/new/choose).
- To stay informed with community updates, follow the [BentoML Blog](http://modelserving.com) and [@bentomlai](http://twitter.com/bentomlai) on Twitter.
Expand All @@ -149,7 +149,7 @@ For a more detailed user guide, check out the [BentoML Tutorial](https://docs.be

There are many ways to contribute to the project:

- If you have any feedback on the project, share it under the `#bentoml-contributors` channel in the [community slack](https://l.linklyhq.com/l/ktOh).
- If you have any feedback on the project, share it under the `#bentoml-contributors` channel in the [community slack](https://l.bentoml.com/join-slack).
- Report issues you're facing and "Thumbs up" on issues and feature requests that are relevant to you.
- Investigate bugs and reviewing other developer's pull requests.
- Contributing code or documentation to the project by submitting a GitHub pull request. Check out the [Development Guide](https://github.com/bentoml/BentoML/blob/main/DEVELOPMENT.md).
Expand Down
2 changes: 1 addition & 1 deletion docs/source/guides/migration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -346,4 +346,4 @@ in one place, and enables advanced GitOps and CI/CD workflow.


🎉 Ta-da, you have migrated your project to BentoML 1.0.0. Have more questions?
`Join the BentoML Slack community <https://l.linklyhq.com/l/ktPp>`_.
`Join the BentoML Slack community <https://l.bentoml.com/join-slack>`_.
2 changes: 1 addition & 1 deletion requirements/tests-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ protobuf<4.0dev
grpcio
grpcio-health-checking
opentelemetry-instrumentation-grpc==0.35b0
Pillow
Pillow
2 changes: 1 addition & 1 deletion src/bentoml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

To learn more, visit BentoML documentation at: http://docs.bentoml.org
To get involved with the development, find us on GitHub: https://github.com/bentoml
And join us in the BentoML slack community: https://l.linklyhq.com/l/ktOh
And join us in the BentoML slack community: https://l.bentoml.com/join-slack
"""

from typing import TYPE_CHECKING
Expand Down
13 changes: 10 additions & 3 deletions src/bentoml/_internal/bento/bento.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,15 @@ def from_bento_model(cls, bento_model: Model) -> BentoModelInfo:
)


def get_service_import_str(svc: Service | str):
from ..service import Service

if isinstance(svc, Service):
return svc.get_service_import_origin()[0]
else:
return svc


@attr.frozen(repr=False)
class BentoInfo:
# for backward compatibility in case new fields are added to BentoInfo.
Expand All @@ -420,9 +429,7 @@ class BentoInfo:
__omit_if_default__ = True

tag: Tag
service: str = attr.field(
converter=lambda svc: svc if isinstance(svc, str) else svc._import_str
)
service: str = attr.field(converter=get_service_import_str)
name: str = attr.field(init=False)
version: str = attr.field(init=False)
# using factory explicitly instead of default because omit_if_default is enabled for BentoInfo
Expand Down
8 changes: 2 additions & 6 deletions src/bentoml/_internal/service/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from ..tag import Tag
from ..bento import Bento
from ..models import ModelStore
from .service import on_import_svc
from .service import on_load_bento
from ...exceptions import NotFound
from ...exceptions import BentoMLException
Expand Down Expand Up @@ -169,11 +168,8 @@ def recover_standalone_env_change():
instance, Service
), f'import target "{module_name}:{attrs_str}" is not a bentoml.Service instance'

on_import_svc(
svc=instance,
working_dir=working_dir,
import_str=f"{module_name}:{attrs_str}",
)
# set import_str for retrieving the service import origin
object.__setattr__(instance, "_import_str", f"{module_name}:{attrs_str}")
return instance
except ImportServiceError:
if sys_path_modified and working_dir:
Expand Down
84 changes: 67 additions & 17 deletions src/bentoml/_internal/service/service.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from __future__ import annotations

import os
import sys
import typing as t
import inspect
import logging
import importlib
from typing import TYPE_CHECKING
Expand Down Expand Up @@ -119,6 +122,7 @@ class Service:
# Working dir and Import path of the service, set when the service was imported
_working_dir: str | None = attr.field(init=False, default=None)
_import_str: str | None = attr.field(init=False, default=None)
_caller_module: str | None = attr.field(init=False, default=None)

def __reduce__(self):
"""
Expand Down Expand Up @@ -181,7 +185,7 @@ def get_or_pull(bento_tag):
else:
from bentoml._internal.service.loader import import_service

return (import_service, (self._import_str, self._working_dir))
return (import_service, self.get_service_import_origin())

def __init__(
self,
Expand Down Expand Up @@ -226,6 +230,54 @@ def __init__(
models=[] if models is None else models,
)

# Set import origin info - import_str can not be determined at this stage yet as
# the variable name is only available in module vars after __init__ is returned
# get_service_import_origin below will use the _caller_module for retriving the
# correct import_str for this service
caller_module = inspect.currentframe().f_back.f_globals["__name__"]
object.__setattr__(self, "_caller_module", caller_module)
object.__setattr__(self, "_working_dir", os.getcwd())

def get_service_import_origin(self) -> tuple[str, str]:
"""
Returns the module name and working directory of the service
"""
if not self._import_str:
import_module = self._caller_module
if import_module == "__main__":
if hasattr(sys.modules["__main__"], "__file__"):
import_module = sys.modules["__main__"].__file__
else:
raise BentoMLException(
"Failed to get service import origin, bentoml.Service object defined interactively in console or notebook is not supported"
)

if self._caller_module not in sys.modules:
raise BentoMLException(
"Failed to get service import origin, bentoml.Service object must be defined in a module"
)

for name, value in vars(sys.modules[self._caller_module]).items():
aarnphm marked this conversation as resolved.
Show resolved Hide resolved
if value is self:
object.__setattr__(self, "_import_str", f"{import_module}:{name}")
break

if not self._import_str:
raise BentoMLException(
"Failed to get service import origin, bentoml.Service object must be assigned to a variable at module level"
parano marked this conversation as resolved.
Show resolved Hide resolved
)

assert self._working_dir is not None

return self._import_str, self._working_dir

def is_service_importable(self) -> bool:
aarnphm marked this conversation as resolved.
Show resolved Hide resolved
if self._caller_module == "__main__":
if not hasattr(sys.modules["__main__"], "__file__"):
return False

return True

def api(
self,
input: IODescriptor[t.Any], # pylint: disable=redefined-builtin
Expand All @@ -247,13 +299,15 @@ def decorator(func: D) -> D:
def __str__(self):
if self.bento:
return f'bentoml.Service(tag="{self.tag}", ' f'path="{self.bento.path}")'
elif self._import_str and self._working_dir:

try:
import_str, working_dir = self.get_service_import_origin()
return (
f'bentoml.Service(name="{self.name}", '
f'import_str="{self._import_str}", '
f'working_dir="{self._working_dir}")'
f'import_str="{import_str}", '
f'working_dir="{working_dir}")'
)
else:
except BentoMLException:
return (
f'bentoml.Service(name="{self.name}", '
f'runners=[{",".join([r.name for r in self.runners])}])'
Expand All @@ -263,16 +317,17 @@ def __repr__(self):
return self.__str__()

def __eq__(self, other: Service):
if self is other:
return True

if self.bento and other.bento:
return self.bento.tag == other.bento.tag

if (
self._working_dir == other._working_dir
and self._import_str == other._import_str
):
return True

return False
try:
if self.get_service_import_origin() == other.get_service_import_origin():
return True
except BentoMLException:
return False

@property
def doc(self) -> str:
Expand Down Expand Up @@ -384,8 +439,3 @@ def add_grpc_handlers(self, handlers: list[grpc.GenericRpcHandler]) -> None:
def on_load_bento(svc: Service, bento: Bento):
object.__setattr__(svc, "bento", bento)
object.__setattr__(svc, "tag", bento.info.tag)


def on_import_svc(svc: Service, working_dir: str, import_str: str):
object.__setattr__(svc, "_working_dir", working_dir)
object.__setattr__(svc, "_import_str", import_str)
46 changes: 36 additions & 10 deletions src/bentoml/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
from simple_di import inject
from simple_di import Provide

from .exceptions import BentoMLException
from ._internal.tag import Tag
from ._internal.bento import Bento
from ._internal.service import Service
from ._internal.configuration.containers import BentoMLContainer

if TYPE_CHECKING:
Expand All @@ -28,7 +30,7 @@


class Server(ABC):
bento: str | Bento | Tag
servable: str | Bento | Tag | Service
host: str
port: int

Expand All @@ -40,7 +42,7 @@ class Server(ABC):

def __init__(
self,
bento: str | Bento | Tag,
servable: str | Bento | Tag | Service,
serve_cmd: str,
reload: bool,
production: bool,
Expand All @@ -50,15 +52,37 @@ def __init__(
working_dir: str | None,
api_workers: int | None,
backlog: int,
bento: str | Bento | Tag | Service | None = None,
):
self.bento = bento
if bento is not None:
if not servable:
logger.warning(
"'bento' is deprecated, either remove it as a kwargs or pass '%s' as the first positional argument",
bento,
)
servable = bento
else:
raise BentoMLException(
"Cannot use both 'bento' and 'servable' as kwargs as 'bento' is deprecated."
)

if isinstance(bento, Bento):
bento_str = str(bento.tag)
elif isinstance(bento, Tag):
bento_str = str(bento)
self.servable = servable
# backward compatibility
self.bento = servable

working_dir = None
if isinstance(servable, Bento):
bento_str = str(servable.tag)
elif isinstance(servable, Tag):
bento_str = str(servable)
elif isinstance(servable, Service):
if not servable.is_service_importable():
raise BentoMLException(
"Cannot use 'bentoml.Service' as a server if it is defined in interactive session or Jupyter Notebooks."
)
bento_str, working_dir = servable.get_service_import_origin()
else:
bento_str = bento
bento_str = servable

args: list[str] = [
sys.executable,
Expand All @@ -74,6 +98,8 @@ def __init__(
str(backlog),
]

if working_dir:
args.extend(["--working-dir", working_dir])
if not production:
args.append("--development")
if reload:
Expand Down Expand Up @@ -183,7 +209,7 @@ class HTTPServer(Server):
@inject
def __init__(
self,
bento: str | Bento | Tag,
bento: str | Bento | Tag | Service,
reload: bool = False,
production: bool = True,
env: t.Literal["conda"] | None = None,
Expand Down Expand Up @@ -279,7 +305,7 @@ class GrpcServer(Server):
@inject
def __init__(
self,
bento: str | Bento | Tag,
bento: str | Bento | Tag | Service,
reload: bool = False,
production: bool = True,
env: t.Literal["conda"] | None = None,
Expand Down
4 changes: 3 additions & 1 deletion tests/e2e/bento_server_http/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ def fixture_server_config_file(request: FixtureRequest) -> str:


@pytest.fixture(autouse=True, scope="package")
def bento_directory(request):
def bento_directory(request: FixtureRequest):
bento_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
os.chdir(bento_path)
sys.path.insert(0, bento_path)
yield
os.chdir(request.config.invocation_dir)
sys.path.pop(0)


@pytest.fixture(scope="session")
Expand Down
26 changes: 26 additions & 0 deletions tests/e2e/bento_server_http/tests/test_serve.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,29 @@ def test_http_server_ctx(bentoml_home: str):
# the process, it should be negative.
# on all other platforms, this should be 0.
assert server.process.poll() <= 0


def test_serve_from_svc():
from service import svc

server = bentoml.HTTPServer(svc, port=12348)
server.start()
client = server.get_client()
resp = client.health()
assert resp.status == 200
server.stop()

timeout = 60
start_time = time.time()
while time.time() - start_time < timeout:
retcode = server.process.poll()
if retcode is not None and retcode <= 0:
break
if sys.platform == "win32":
# on Windows, because of the way that terminate is run, it seems the exit code is set.
assert isinstance(server.process.poll(), int)
else:
# on POSIX negative return codes mean the process was terminated; since we will be terminating
# the process, it should be negative.
# on all other platforms, this should be 0.
assert server.process.poll() <= 0