mirror of
https://github.com/vale981/ray
synced 2025-03-10 05:16:49 -04:00

These Serve CLI commands start Serve if it's not already running: * `serve deploy` * `serve config` * `serve status` * `serve shutdown` #27026 introduces the ability to specify a `host` and `port` in the Serve config file. However, once Serve starts running, changing these options requires tearing down the entire Serve application and relaunching it. This limitation is an issue because users can inadvertently start Serve by running one of the `GET`-based CLI commands (i.e. `serve config` or `serve status`) before running `serve deploy`. This change makes `serve deploy` the only CLI command that can start a Serve application on a Ray cluster. The other commands have updated behavior when Serve is not yet running on the cluster. * `serve config`: prints an empty config body. ```yaml import_path: '' runtime_env: {} deployments: [] ``` * `serve status`: prints an empty status body, with a new `app_status` `status` value: `NOT_STARTED`. ```yaml app_status: status: NOT_STARTED message: '' deployment_timestamp: 0 deployment_statuses: [] ``` * `serve shutdown`: performs a no-op.
253 lines
8.7 KiB
Python
253 lines
8.7 KiB
Python
import copy
|
|
import sys
|
|
from typing import Dict
|
|
|
|
import pytest
|
|
import requests
|
|
|
|
import ray
|
|
from ray import serve
|
|
from ray._private.test_utils import wait_for_condition
|
|
import ray._private.ray_constants as ray_constants
|
|
from ray.serve.tests.conftest import * # noqa: F401 F403
|
|
|
|
GET_OR_PUT_URL = "http://localhost:52365/api/serve/deployments/"
|
|
STATUS_URL = "http://localhost:52365/api/serve/deployments/status"
|
|
|
|
|
|
def deploy_and_check_config(config: Dict):
|
|
put_response = requests.put(GET_OR_PUT_URL, json=config, timeout=30)
|
|
assert put_response.status_code == 200
|
|
print("PUT request sent successfully.")
|
|
|
|
# Config should be immediately retrievable
|
|
get_response = requests.get(GET_OR_PUT_URL, timeout=15)
|
|
assert get_response.status_code == 200
|
|
assert get_response.json() == config
|
|
print("GET request returned correct config.")
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "darwin", reason="Flaky on OSX.")
|
|
def test_put_get(ray_start_stop):
|
|
config1 = {
|
|
"import_path": (
|
|
"ray.serve.tests.test_config_files.test_dag.conditional_dag.serve_dag"
|
|
),
|
|
"runtime_env": {},
|
|
"deployments": [
|
|
{
|
|
"name": "Multiplier",
|
|
"user_config": {"factor": 1},
|
|
},
|
|
{
|
|
"name": "Adder",
|
|
"ray_actor_options": {
|
|
"runtime_env": {"env_vars": {"override_increment": "1"}}
|
|
},
|
|
},
|
|
],
|
|
}
|
|
|
|
# Use empty dictionary for Adder's ray_actor_options.
|
|
config2 = copy.deepcopy(config1)
|
|
config2["deployments"][1]["ray_actor_options"] = {}
|
|
|
|
config3 = {
|
|
"import_path": "ray.serve.tests.test_config_files.world.DagNode",
|
|
}
|
|
|
|
# Ensure the REST API is idempotent
|
|
num_iterations = 2
|
|
for iteration in range(1, num_iterations + 1):
|
|
print(f"*** Starting Iteration {iteration}/{num_iterations} ***\n")
|
|
|
|
print("Sending PUT request for config1.")
|
|
deploy_and_check_config(config1)
|
|
wait_for_condition(
|
|
lambda: requests.post("http://localhost:8000/", json=["ADD", 2]).json()
|
|
== "3 pizzas please!",
|
|
timeout=15,
|
|
)
|
|
wait_for_condition(
|
|
lambda: requests.post("http://localhost:8000/", json=["MUL", 2]).json()
|
|
== "-4 pizzas please!",
|
|
timeout=15,
|
|
)
|
|
print("Deployments are live and reachable over HTTP.\n")
|
|
|
|
print("Sending PUT request for config2.")
|
|
deploy_and_check_config(config2)
|
|
wait_for_condition(
|
|
lambda: requests.post("http://localhost:8000/", json=["ADD", 2]).json()
|
|
== "4 pizzas please!",
|
|
timeout=15,
|
|
)
|
|
print("Adder deployment updated correctly.\n")
|
|
|
|
print("Sending PUT request for config3.")
|
|
deploy_and_check_config(config3)
|
|
wait_for_condition(
|
|
lambda: requests.post("http://localhost:8000/").text == "wonderful world",
|
|
timeout=15,
|
|
)
|
|
print("Deployments are live and reachable over HTTP.\n")
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "darwin", reason="Flaky on OSX.")
|
|
def test_delete(ray_start_stop):
|
|
config = {
|
|
"import_path": "dir.subdir.a.add_and_sub.serve_dag",
|
|
"runtime_env": {
|
|
"working_dir": (
|
|
"https://github.com/ray-project/test_dag/archive/"
|
|
"76a741f6de31df78411b1f302071cde46f098418.zip"
|
|
)
|
|
},
|
|
"deployments": [
|
|
{
|
|
"name": "Subtract",
|
|
"ray_actor_options": {
|
|
"runtime_env": {
|
|
"py_modules": [
|
|
(
|
|
"https://github.com/ray-project/test_module/archive/"
|
|
"aa6f366f7daa78c98408c27d917a983caa9f888b.zip"
|
|
)
|
|
]
|
|
}
|
|
},
|
|
}
|
|
],
|
|
}
|
|
|
|
# Ensure the REST API is idempotent
|
|
num_iterations = 2
|
|
for iteration in range(1, num_iterations + 1):
|
|
print(f"*** Starting Iteration {iteration}/{num_iterations} ***\n")
|
|
|
|
print("Sending PUT request for config.")
|
|
deploy_and_check_config(config)
|
|
wait_for_condition(
|
|
lambda: requests.post("http://localhost:8000/", json=["ADD", 1]).json()
|
|
== 2,
|
|
timeout=15,
|
|
)
|
|
wait_for_condition(
|
|
lambda: requests.post("http://localhost:8000/", json=["SUB", 1]).json()
|
|
== -1,
|
|
timeout=15,
|
|
)
|
|
print("Deployments are live and reachable over HTTP.\n")
|
|
|
|
print("Sending DELETE request for config.")
|
|
delete_response = requests.delete(GET_OR_PUT_URL, timeout=15)
|
|
assert delete_response.status_code == 200
|
|
print("DELETE request sent successfully.")
|
|
|
|
# Make sure all deployments are deleted
|
|
wait_for_condition(
|
|
lambda: len(requests.get(GET_OR_PUT_URL, timeout=15).json()["deployments"])
|
|
== 0,
|
|
timeout=15,
|
|
)
|
|
print("GET request returned empty config successfully.")
|
|
|
|
with pytest.raises(requests.exceptions.ConnectionError):
|
|
requests.post("http://localhost:8000/", json=["ADD", 1]).raise_for_status()
|
|
with pytest.raises(requests.exceptions.ConnectionError):
|
|
requests.post("http://localhost:8000/", json=["SUB", 1]).raise_for_status()
|
|
print("Deployments have been deleted and are not reachable.\n")
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "darwin", reason="Flaky on OSX.")
|
|
def test_get_status(ray_start_stop):
|
|
print("Checking status info before any deployments.")
|
|
|
|
status_response = requests.get(STATUS_URL, timeout=30)
|
|
assert status_response.status_code == 200
|
|
print("Retrieved status info successfully.")
|
|
|
|
serve_status = status_response.json()
|
|
assert len(serve_status["deployment_statuses"]) == 0
|
|
assert serve_status["app_status"]["status"] == "NOT_STARTED"
|
|
assert serve_status["app_status"]["deployment_timestamp"] == 0
|
|
assert serve_status["app_status"]["message"] == ""
|
|
print("Status info on fresh Serve application is correct.\n")
|
|
|
|
config = {
|
|
"import_path": "ray.serve.tests.test_config_files.world.DagNode",
|
|
}
|
|
|
|
print("Deploying config.")
|
|
deploy_and_check_config(config)
|
|
wait_for_condition(
|
|
lambda: requests.post("http://localhost:8000/").text == "wonderful world",
|
|
timeout=15,
|
|
)
|
|
print("Deployments are live and reachable over HTTP.\n")
|
|
|
|
status_response = requests.get(STATUS_URL, timeout=30)
|
|
assert status_response.status_code == 200
|
|
print("Retrieved status info successfully.")
|
|
|
|
serve_status = status_response.json()
|
|
|
|
deployment_statuses = serve_status["deployment_statuses"]
|
|
assert len(deployment_statuses) == 2
|
|
expected_deployment_names = {"f", "BasicDriver"}
|
|
for deployment_status in deployment_statuses:
|
|
assert deployment_status["name"] in expected_deployment_names
|
|
expected_deployment_names.remove(deployment_status["name"])
|
|
assert deployment_status["status"] in {"UPDATING", "HEALTHY"}
|
|
assert deployment_status["message"] == ""
|
|
assert len(expected_deployment_names) == 0
|
|
print("Deployments' statuses are correct.")
|
|
|
|
assert serve_status["app_status"]["status"] in {"DEPLOYING", "RUNNING"}
|
|
assert serve_status["app_status"]["deployment_timestamp"] > 0
|
|
assert serve_status["app_status"]["message"] == ""
|
|
print("Serve app status is correct.")
|
|
|
|
|
|
@pytest.mark.skipif(sys.platform == "darwin", reason="Flaky on OSX.")
|
|
def test_serve_namespace(ray_start_stop):
|
|
"""
|
|
Check that the Dashboard's Serve can interact with the Python API
|
|
when they both start in the "serve" namespace.
|
|
"""
|
|
|
|
config = {
|
|
"import_path": "ray.serve.tests.test_config_files.world.DagNode",
|
|
}
|
|
|
|
print("Deploying config.")
|
|
deploy_and_check_config(config)
|
|
wait_for_condition(
|
|
lambda: requests.post("http://localhost:8000/").text == "wonderful world",
|
|
timeout=15,
|
|
)
|
|
print("Deployments are live and reachable over HTTP.\n")
|
|
|
|
ray.init(address="auto", namespace="serve")
|
|
client = serve.start()
|
|
print("Connected to Serve with Python API.")
|
|
serve_status = client.get_serve_status()
|
|
assert (
|
|
len(serve_status.deployment_statuses) == 2
|
|
and serve_status.get_deployment_status("f") is not None
|
|
)
|
|
print("Successfully retrieved deployment statuses with Python API.")
|
|
print("Shutting down Python API.")
|
|
serve.shutdown()
|
|
|
|
|
|
def test_default_dashboard_agent_listen_port():
|
|
"""
|
|
Defaults in the code and the documentation assume
|
|
the dashboard agent listens to HTTP on port 52365.
|
|
"""
|
|
assert ray_constants.DEFAULT_DASHBOARD_AGENT_LISTEN_PORT == 52365
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-v", __file__]))
|