2022-06-14 19:01:51 -07:00
|
|
|
import copy
|
|
|
|
import subprocess
|
2022-05-24 11:07:41 -07:00
|
|
|
import sys
|
2022-06-14 19:01:51 -07:00
|
|
|
from typing import Dict
|
|
|
|
|
2022-05-24 11:07:41 -07:00
|
|
|
import pytest
|
|
|
|
import requests
|
2022-03-01 11:21:22 -06:00
|
|
|
|
2022-03-16 10:03:44 -07:00
|
|
|
import ray
|
|
|
|
from ray import serve
|
2022-05-24 11:07:41 -07:00
|
|
|
from ray._private.test_utils import wait_for_condition
|
2022-02-24 08:00:26 -08:00
|
|
|
|
2022-02-25 06:41:07 -08:00
|
|
|
GET_OR_PUT_URL = "http://localhost:8265/api/serve/deployments/"
|
|
|
|
STATUS_URL = "http://localhost:8265/api/serve/deployments/status"
|
|
|
|
|
|
|
|
|
2022-02-24 08:00:26 -08:00
|
|
|
@pytest.fixture
|
2022-03-01 11:21:22 -06:00
|
|
|
def ray_start_stop():
|
2022-02-24 08:00:26 -08:00
|
|
|
subprocess.check_output(["ray", "start", "--head"])
|
|
|
|
yield
|
|
|
|
subprocess.check_output(["ray", "stop", "--force"])
|
|
|
|
|
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
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.")
|
2022-02-24 08:00:26 -08:00
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
# 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.")
|
2022-02-24 08:00:26 -08:00
|
|
|
|
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
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"}}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
}
|
2022-02-24 08:00:26 -08:00
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
# Use empty dictionary for Adder's ray_actor_options.
|
|
|
|
config2 = copy.deepcopy(config1)
|
|
|
|
config2["deployments"][1]["ray_actor_options"] = {}
|
2022-03-01 11:21:22 -06:00
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
config3 = {
|
|
|
|
"import_path": "ray.serve.tests.test_config_files.world.DagNode",
|
|
|
|
}
|
2022-03-01 11:21:22 -06:00
|
|
|
|
2022-02-24 08:00:26 -08:00
|
|
|
# Ensure the REST API is idempotent
|
2022-06-14 19:01:51 -07:00
|
|
|
num_iterations = 2
|
|
|
|
for iteration in range(1, num_iterations + 1):
|
|
|
|
print(f"*** Starting Iteration {iteration}/{num_iterations} ***\n")
|
2022-02-24 08:00:26 -08:00
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
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,
|
2022-02-24 08:00:26 -08:00
|
|
|
)
|
2022-06-14 19:01:51 -07:00
|
|
|
wait_for_condition(
|
|
|
|
lambda: requests.post("http://localhost:8000/", json=["MUL", 2]).json()
|
|
|
|
== "-4 pizzas please!",
|
|
|
|
timeout=15,
|
2022-03-01 11:21:22 -06:00
|
|
|
)
|
2022-06-14 19:01:51 -07:00
|
|
|
print("Deployments are live and reachable over HTTP.\n")
|
2022-05-24 11:07:41 -07:00
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
print("Sending PUT request for config2.")
|
|
|
|
deploy_and_check_config(config2)
|
2022-05-24 11:07:41 -07:00
|
|
|
wait_for_condition(
|
2022-06-14 19:01:51 -07:00
|
|
|
lambda: requests.post("http://localhost:8000/", json=["ADD", 2]).json()
|
|
|
|
== "4 pizzas please!",
|
|
|
|
timeout=15,
|
2022-05-24 11:07:41 -07:00
|
|
|
)
|
2022-06-14 19:01:51 -07:00
|
|
|
print("Adder deployment updated correctly.\n")
|
2022-05-24 11:07:41 -07:00
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
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,
|
2022-02-24 08:00:26 -08:00
|
|
|
)
|
2022-06-14 19:01:51 -07:00
|
|
|
print("Deployments are live and reachable over HTTP.\n")
|
2022-02-24 08:00:26 -08:00
|
|
|
|
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
def test_delete(ray_start_stop):
|
2022-06-02 11:06:53 -07:00
|
|
|
config = {
|
2022-06-14 19:01:51 -07:00
|
|
|
"import_path": "dir.subdir.a.add_and_sub.serve_dag",
|
2022-06-08 15:58:00 -07:00
|
|
|
"runtime_env": {
|
|
|
|
"working_dir": (
|
|
|
|
"https://github.com/ray-project/test_dag/archive/"
|
2022-06-14 19:01:51 -07:00
|
|
|
"76a741f6de31df78411b1f302071cde46f098418.zip"
|
2022-06-08 15:58:00 -07:00
|
|
|
)
|
|
|
|
},
|
2022-06-02 11:06:53 -07:00
|
|
|
"deployments": [
|
|
|
|
{
|
2022-06-14 19:01:51 -07:00
|
|
|
"name": "Subtract",
|
2022-06-08 15:58:00 -07:00
|
|
|
"ray_actor_options": {
|
2022-06-14 19:01:51 -07:00
|
|
|
"runtime_env": {
|
|
|
|
"py_modules": [
|
|
|
|
(
|
|
|
|
"https://github.com/ray-project/test_module/archive/"
|
|
|
|
"aa6f366f7daa78c98408c27d917a983caa9f888b.zip"
|
|
|
|
)
|
|
|
|
]
|
|
|
|
}
|
2022-06-02 11:06:53 -07:00
|
|
|
},
|
2022-06-14 19:01:51 -07:00
|
|
|
}
|
2022-06-02 11:06:53 -07:00
|
|
|
],
|
|
|
|
}
|
|
|
|
|
2022-02-24 08:00:26 -08:00
|
|
|
# Ensure the REST API is idempotent
|
2022-06-14 19:01:51 -07:00
|
|
|
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,
|
2022-03-01 11:21:22 -06:00
|
|
|
)
|
2022-06-14 19:01:51 -07:00
|
|
|
wait_for_condition(
|
|
|
|
lambda: requests.post("http://localhost:8000/", json=["SUB", 1]).json()
|
|
|
|
== -1,
|
|
|
|
timeout=15,
|
2022-02-24 08:00:26 -08:00
|
|
|
)
|
2022-06-14 19:01:51 -07:00
|
|
|
print("Deployments are live and reachable over HTTP.\n")
|
2022-02-24 08:00:26 -08:00
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
print("Sending DELETE request for config.")
|
|
|
|
delete_response = requests.delete(GET_OR_PUT_URL, timeout=15)
|
2022-02-24 08:00:26 -08:00
|
|
|
assert delete_response.status_code == 200
|
2022-06-14 19:01:51 -07:00
|
|
|
print("DELETE request sent successfully.")
|
2022-02-24 08:00:26 -08:00
|
|
|
|
2022-05-24 11:07:41 -07:00
|
|
|
# Make sure all deployments are deleted
|
|
|
|
wait_for_condition(
|
2022-06-14 19:01:51 -07:00
|
|
|
lambda: len(requests.get(GET_OR_PUT_URL, timeout=15).json()["deployments"])
|
2022-05-24 11:07:41 -07:00
|
|
|
== 0,
|
2022-06-14 19:01:51 -07:00
|
|
|
timeout=15,
|
2022-05-24 11:07:41 -07:00
|
|
|
)
|
2022-06-14 19:01:51 -07:00
|
|
|
print("GET request returned empty config successfully.")
|
2022-02-25 06:41:07 -08:00
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
with pytest.raises(requests.exceptions.HTTPError):
|
|
|
|
requests.post("http://localhost:8000/", json=["ADD", 1]).raise_for_status()
|
|
|
|
with pytest.raises(requests.exceptions.HTTPError):
|
|
|
|
requests.post("http://localhost:8000/", json=["SUB", 1]).raise_for_status()
|
|
|
|
print("Deployments have been deleted and are not reachable.\n")
|
2022-02-25 06:41:07 -08:00
|
|
|
|
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
def test_get_status(ray_start_stop):
|
|
|
|
print("Checking status info before any deployments.")
|
2022-02-25 06:41:07 -08:00
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
status_response = requests.get(STATUS_URL, timeout=30)
|
|
|
|
assert status_response.status_code == 200
|
|
|
|
print("Retrieved status info successfully.")
|
2022-02-25 06:41:07 -08:00
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
serve_status = status_response.json()
|
|
|
|
assert len(serve_status["deployment_statuses"]) == 0
|
|
|
|
assert serve_status["app_status"]["status"] == "RUNNING"
|
|
|
|
assert serve_status["app_status"]["deployment_timestamp"] == 0
|
|
|
|
assert serve_status["app_status"]["message"] == ""
|
|
|
|
print("Status info on fresh Serve application is correct.\n")
|
2022-02-25 06:41:07 -08:00
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
config = {
|
|
|
|
"import_path": "ray.serve.tests.test_config_files.world.DagNode",
|
|
|
|
}
|
2022-02-25 06:41:07 -08:00
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
print("Deploying config.")
|
|
|
|
deploy_and_check_config(config)
|
|
|
|
wait_for_condition(
|
|
|
|
lambda: requests.post("http://localhost:8000/").text == "wonderful world",
|
|
|
|
timeout=15,
|
2022-03-01 11:21:22 -06:00
|
|
|
)
|
2022-06-14 19:01:51 -07:00
|
|
|
print("Deployments are live and reachable over HTTP.\n")
|
2022-02-25 06:41:07 -08:00
|
|
|
|
2022-03-01 11:21:22 -06:00
|
|
|
status_response = requests.get(STATUS_URL, timeout=30)
|
2022-02-25 06:41:07 -08:00
|
|
|
assert status_response.status_code == 200
|
2022-06-14 19:01:51 -07:00
|
|
|
print("Retrieved status info successfully.")
|
|
|
|
|
2022-05-24 11:07:41 -07:00
|
|
|
serve_status = status_response.json()
|
2022-02-25 06:41:07 -08:00
|
|
|
|
2022-05-24 11:07:41 -07:00
|
|
|
deployment_statuses = serve_status["deployment_statuses"]
|
2022-06-14 19:01:51 -07:00
|
|
|
assert len(deployment_statuses) == 2
|
|
|
|
expected_deployment_names = {"f", "BasicDriver"}
|
2022-05-24 11:07:41 -07:00
|
|
|
for deployment_status in deployment_statuses:
|
2022-02-25 06:41:07 -08:00
|
|
|
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
|
2022-06-14 19:01:51 -07:00
|
|
|
print("Deployments' statuses are correct.")
|
2022-02-25 06:41:07 -08:00
|
|
|
|
2022-05-24 11:07:41 -07:00
|
|
|
assert serve_status["app_status"]["status"] in {"DEPLOYING", "RUNNING"}
|
2022-06-14 19:01:51 -07:00
|
|
|
assert serve_status["app_status"]["deployment_timestamp"] > 0
|
|
|
|
assert serve_status["app_status"]["message"] == ""
|
|
|
|
print("Serve app status is correct.")
|
2022-03-01 11:21:22 -06:00
|
|
|
|
|
|
|
|
2022-03-16 10:03:44 -07:00
|
|
|
def test_serve_namespace(ray_start_stop):
|
|
|
|
"""
|
|
|
|
Check that the Dashboard's Serve can interact with the Python API
|
2022-06-14 19:01:51 -07:00
|
|
|
when they both start in the "serve" namespace.
|
2022-03-16 10:03:44 -07:00
|
|
|
"""
|
|
|
|
|
2022-06-14 19:01:51 -07:00
|
|
|
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,
|
2022-03-16 10:03:44 -07:00
|
|
|
)
|
2022-06-14 19:01:51 -07:00
|
|
|
print("Deployments are live and reachable over HTTP.\n")
|
|
|
|
|
2022-03-16 10:03:44 -07:00
|
|
|
ray.init(address="auto", namespace="serve")
|
2022-06-14 19:01:51 -07:00
|
|
|
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.")
|
2022-03-16 10:03:44 -07:00
|
|
|
serve.shutdown()
|
|
|
|
|
|
|
|
|
2022-03-01 11:21:22 -06:00
|
|
|
if __name__ == "__main__":
|
|
|
|
sys.exit(pytest.main(["-v", __file__]))
|