[serve] add root_path setting (#21090)

Support hosting a serve instance under a path prefix.

Some clean-up should still be done for the different overlapping HttpOptions that now exist (host, port, root_path, root_url).
This commit is contained in:
iasoon 2022-01-27 23:36:22 +01:00 committed by GitHub
parent 559eefd06f
commit b0700e676b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 49 additions and 8 deletions

View file

@ -432,6 +432,9 @@ def start(
"127.0.0.1". To expose Serve publicly, you probably want to set
this to "0.0.0.0".
- port(int): Port for HTTP server. Defaults to 8000.
- root_path(str): Root path to mount the serve application
(for example, "/serve"). All deployment routes will be prefixed
with this path. Defaults to "".
- middlewares(list): A list of Starlette middlewares that will be
applied to the HTTP servers in the cluster. Defaults to [].
- location(str, serve.config.DeploymentMode): The deployment

View file

@ -257,6 +257,7 @@ class HTTPOptions(pydantic.BaseModel):
location: Optional[DeploymentMode] = DeploymentMode.HeadOnly
num_cpus: int = 0
root_url: str = ""
root_path: str = ""
fixed_number_replicas: Optional[int] = None
fixed_number_selection_seed: int = 0

View file

@ -270,7 +270,8 @@ class ServeController:
if SERVE_ROOT_URL_ENV_KEY in os.environ:
return os.environ[SERVE_ROOT_URL_ENV_KEY]
else:
return f"http://{http_config.host}:{http_config.port}"
return (f"http://{http_config.host}:{http_config.port}"
f"{http_config.root_path}")
return http_config.root_url
async def shutdown(self) -> List[GoalId]:
@ -336,9 +337,11 @@ class ServeController:
goal_id, updating = self.deployment_state_manager.deploy(
name, deployment_info)
if route_prefix is not None:
endpoint_info = EndpointInfo(route=route_prefix)
self.endpoint_state.update_endpoint(name, endpoint_info)
return goal_id, updating
def delete_deployment(self, name: str) -> Optional[GoalId]:

View file

@ -276,17 +276,21 @@ class HTTPProxy:
"""
assert scope["type"] == "http"
route_path = scope["path"]
self.request_counter.inc(tags={"route": scope["path"]})
if scope["path"] == "/-/routes":
# only use the non-root part of the path for routing
root_path = scope["root_path"]
route_path = scope["path"][len(root_path):]
self.request_counter.inc(tags={"route": route_path})
if route_path == "/-/routes":
return await starlette.responses.JSONResponse(self.route_info)(
scope, receive, send)
route_prefix, handle = self.prefix_router.match_route(scope["path"])
route_prefix, handle = self.prefix_router.match_route(route_path)
if route_prefix is None:
self.request_error_counter.inc(tags={
"route": scope["path"],
"route": route_path,
"error_code": "404"
})
return await self._not_found(scope, receive, send)
@ -296,8 +300,8 @@ class HTTPProxy:
# changed without restarting the replicas.
if route_prefix != "/":
assert not route_prefix.endswith("/")
scope["path"] = scope["path"].replace(route_prefix, "", 1)
scope["root_path"] = route_prefix
scope["path"] = route_path.replace(route_prefix, "", 1)
scope["root_path"] = root_path + route_prefix
status_code = await _send_request_to_handle(handle, scope, receive,
send)
@ -315,6 +319,7 @@ class HTTPProxyActor:
def __init__(self,
host: str,
port: int,
root_path: str,
controller_name: str,
controller_namespace: str,
http_middlewares: Optional[List[
@ -324,6 +329,7 @@ class HTTPProxyActor:
self.host = host
self.port = port
self.root_path = root_path
self.setup_complete = asyncio.Event()
@ -383,6 +389,7 @@ Please make sure your http-host and http-port are specified correctly.""")
self.wrapped_app,
host=self.host,
port=self.port,
root_path=self.root_path,
lifespan="off",
access_log=False)
server = uvicorn.Server(config=config)

View file

@ -112,6 +112,7 @@ class HTTPState:
).remote(
self._config.host,
self._config.port,
self._config.root_path,
controller_name=self._controller_name,
controller_namespace=self._controller_namespace,
http_middlewares=self._config.middlewares)

View file

@ -310,6 +310,32 @@ def test_http_root_url(ray_shutdown):
ray.shutdown()
@pytest.mark.skipif(sys.platform == "win32", reason="Failing on Windows")
def test_http_root_path(ray_shutdown):
@serve.deployment
def hello():
return "hello"
port = new_port()
root_path = "/serve"
serve.start(http_options=dict(root_path=root_path, port=port))
hello.deploy()
# check whether url is prefixed correctly
assert hello.url == f"http://127.0.0.1:{port}{root_path}/hello"
# check routing works as expected
resp = requests.get(hello.url)
assert resp.status_code == 200
assert resp.text == "hello"
# check advertized routes are prefixed correctly
resp = requests.get(f"http://127.0.0.1:{port}{root_path}/-/routes")
assert resp.status_code == 200
assert resp.json() == {"/hello": "hello"}
@pytest.mark.skipif(sys.platform == "win32", reason="Failing on Windows")
def test_http_proxy_fail_loudly(ray_shutdown):
# Test that if the http server fail to start, serve.start should fail.
with pytest.raises(ValueError):