chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Dict, Optional, Pattern
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from ray import serve
|
||||
from ray._common.test_utils import wait_for_condition
|
||||
from ray.serve._private.constants import (
|
||||
RAY_SERVE_ENABLE_HA_PROXY,
|
||||
SERVE_DEFAULT_APP_NAME,
|
||||
)
|
||||
from ray.serve._private.test_utils import get_application_url
|
||||
from ray.util.state import list_actors
|
||||
|
||||
CONNECTION_ERROR_MSG = "connection error"
|
||||
|
||||
|
||||
def ping_endpoint(app_name: str = SERVE_DEFAULT_APP_NAME, params: str = ""):
|
||||
try:
|
||||
url = get_application_url("HTTP", app_name=app_name)
|
||||
return httpx.get(f"{url}/{params}").text
|
||||
except httpx.HTTPError:
|
||||
return CONNECTION_ERROR_MSG
|
||||
|
||||
|
||||
def check_app_status(app_name: str, expected_status: str):
|
||||
status_response = subprocess.check_output(["serve", "status"])
|
||||
status = yaml.safe_load(status_response)["applications"]
|
||||
assert status[app_name]["status"] == expected_status
|
||||
return True
|
||||
|
||||
|
||||
def check_app_running(app_name: str):
|
||||
return check_app_status(app_name, "RUNNING")
|
||||
|
||||
|
||||
def check_http_response(expected_text: str, json: Optional[Dict] = None):
|
||||
url = get_application_url("HTTP")
|
||||
resp = httpx.post(url, json=json)
|
||||
assert resp.text == expected_text
|
||||
return True
|
||||
|
||||
|
||||
def test_start_shutdown(ray_start_stop):
|
||||
subprocess.check_output(["serve", "start"])
|
||||
# deploy a simple app
|
||||
import_path = "ray.serve.tests.test_config_files.arg_builders.build_echo_app"
|
||||
|
||||
deploy_response = subprocess.check_output(["serve", "deploy", import_path])
|
||||
assert b"Sent deploy request successfully." in deploy_response
|
||||
|
||||
wait_for_condition(
|
||||
check_http_response,
|
||||
expected_text="DEFAULT",
|
||||
timeout=15,
|
||||
)
|
||||
|
||||
ret = subprocess.check_output(["serve", "shutdown", "-y"])
|
||||
assert b"Sent shutdown request; applications will be deleted asynchronously" in ret
|
||||
|
||||
def check_no_apps():
|
||||
status = subprocess.check_output(["serve", "status"])
|
||||
return b"applications: {}" in status
|
||||
|
||||
wait_for_condition(check_no_apps, timeout=15)
|
||||
|
||||
# Test shutdown when no Serve instance is running
|
||||
ret = subprocess.check_output(["serve", "shutdown", "-y"], stderr=subprocess.STDOUT)
|
||||
assert b"No Serve instance found running" in ret
|
||||
|
||||
|
||||
def test_start_shutdown_without_serve_running(ray_start_stop):
|
||||
# Test shutdown when no Serve instance is running
|
||||
ret = subprocess.check_output(["serve", "shutdown", "-y"], stderr=subprocess.STDOUT)
|
||||
assert b"No Serve instance found running" in ret
|
||||
|
||||
|
||||
# def test_start_shutdown_without_ray_running():
|
||||
# # Test shutdown when Ray is not running
|
||||
# ret = subprocess.check_output(["serve", "shutdown", "-y"], stderr=subprocess.STDOUT)
|
||||
# assert b"Unable to shutdown Serve on the cluster" in ret
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="File path incorrect on Windows.")
|
||||
def test_shutdown(ray_start_stop):
|
||||
"""Deploys a config file and shuts down the Serve application."""
|
||||
|
||||
# Check that `serve shutdown` works even if no Serve app is running
|
||||
subprocess.check_output(["serve", "shutdown", "-y"])
|
||||
|
||||
def num_live_deployments():
|
||||
status_response = subprocess.check_output(["serve", "status"])
|
||||
serve_status = yaml.safe_load(status_response)["applications"][
|
||||
SERVE_DEFAULT_APP_NAME
|
||||
]
|
||||
return len(serve_status["deployments"])
|
||||
|
||||
config_file_name = os.path.join(
|
||||
os.path.dirname(__file__), "test_config_files", "basic_graph.yaml"
|
||||
)
|
||||
|
||||
# Check idempotence
|
||||
num_iterations = 2
|
||||
for iteration in range(1, num_iterations + 1):
|
||||
print(f"*** Starting Iteration {iteration}/{num_iterations} ***\n")
|
||||
|
||||
print("Deploying config.")
|
||||
subprocess.check_output(["serve", "deploy", config_file_name])
|
||||
wait_for_condition(lambda: num_live_deployments() == 2, timeout=15)
|
||||
print("Deployment successful. Deployments are live.")
|
||||
|
||||
# `serve config` and `serve status` should print non-empty schemas
|
||||
config_response = subprocess.check_output(["serve", "config"])
|
||||
yaml.safe_load(config_response)
|
||||
|
||||
status_response = subprocess.check_output(["serve", "status"])
|
||||
status = yaml.safe_load(status_response)
|
||||
assert len(status["applications"])
|
||||
print("`serve config` and `serve status` print non-empty responses.\n")
|
||||
|
||||
print("Deleting Serve app.")
|
||||
subprocess.check_output(["serve", "shutdown", "-y"])
|
||||
|
||||
# `serve config` and `serve status` should print messages indicating
|
||||
# nothing is deployed
|
||||
def serve_config_empty_warning():
|
||||
config_response = subprocess.check_output(["serve", "config"]).decode(
|
||||
"utf-8"
|
||||
)
|
||||
return config_response == "No configuration was found.\n"
|
||||
|
||||
def serve_status_empty():
|
||||
status_response = subprocess.check_output(["serve", "status"])
|
||||
status = yaml.safe_load(status_response)
|
||||
return len(status["applications"]) == 0
|
||||
|
||||
wait_for_condition(serve_config_empty_warning)
|
||||
wait_for_condition(serve_status_empty)
|
||||
print("`serve config` and `serve status` print empty responses.\n")
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="File path incorrect on Windows.")
|
||||
def test_deploy_with_http_options(ray_start_stop):
|
||||
"""Deploys config with host and port options specified"""
|
||||
|
||||
f1 = os.path.join(
|
||||
os.path.dirname(__file__), "test_config_files", "basic_graph_http.yaml"
|
||||
)
|
||||
success_message_fragment = b"Sent deploy request successfully."
|
||||
|
||||
with open(f1, "r") as config_file:
|
||||
config = yaml.safe_load(config_file)
|
||||
|
||||
deploy_response = subprocess.check_output(["serve", "deploy", f1])
|
||||
assert success_message_fragment in deploy_response
|
||||
|
||||
wait_for_condition(
|
||||
lambda: httpx.post("http://localhost:8005/", json=None).text
|
||||
== "wonderful world",
|
||||
timeout=15,
|
||||
)
|
||||
|
||||
# Config should contain matching host and port options
|
||||
info_response = subprocess.check_output(["serve", "config"])
|
||||
info = yaml.safe_load(info_response)
|
||||
|
||||
# TODO(zcin): the assertion should just be `info == config` here but the output
|
||||
# formatting removes a lot of info.
|
||||
assert info == config["applications"][0]
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="File path incorrect on Windows.")
|
||||
@pytest.mark.parametrize("use_command", [True, False])
|
||||
def test_idempotence_after_controller_death(ray_start_stop, use_command: bool):
|
||||
"""Check that CLI is idempotent even if controller dies."""
|
||||
config_file_name = os.path.join(
|
||||
os.path.dirname(__file__), "test_config_files", "basic_graph.yaml"
|
||||
)
|
||||
success_message_fragment = b"Sent deploy request successfully."
|
||||
deploy_response = subprocess.check_output(["serve", "deploy", config_file_name])
|
||||
assert success_message_fragment in deploy_response
|
||||
|
||||
expected_num_actors = 4
|
||||
if RAY_SERVE_ENABLE_HA_PROXY:
|
||||
# fallback proxy
|
||||
expected_num_actors += 1
|
||||
|
||||
serve.start()
|
||||
wait_for_condition(
|
||||
lambda: len(list_actors(filters=[("state", "=", "ALIVE")]))
|
||||
== expected_num_actors,
|
||||
timeout=15,
|
||||
)
|
||||
|
||||
# Kill controller
|
||||
if use_command:
|
||||
subprocess.check_output(["serve", "shutdown", "-y"])
|
||||
else:
|
||||
serve.shutdown()
|
||||
|
||||
status_response = subprocess.check_output(["serve", "status"])
|
||||
status_info = yaml.safe_load(status_response)
|
||||
|
||||
assert len(status_info["applications"]) == 0
|
||||
|
||||
deploy_response = subprocess.check_output(["serve", "deploy", config_file_name])
|
||||
assert success_message_fragment in deploy_response
|
||||
|
||||
# Restore testing controller
|
||||
serve.start()
|
||||
wait_for_condition(
|
||||
lambda: len(list_actors(filters=[("state", "=", "ALIVE")]))
|
||||
== expected_num_actors,
|
||||
timeout=15,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="File path incorrect on Windows.")
|
||||
def test_control_c_shutdown_serve_components(ray_start_stop):
|
||||
"""Test ctrl+c after `serve run` shuts down serve components."""
|
||||
|
||||
p = subprocess.Popen(["serve", "run", "ray.serve.tests.test_cli_3.echo_app"])
|
||||
|
||||
# Make sure Serve components are up and running
|
||||
wait_for_condition(check_app_running, app_name=SERVE_DEFAULT_APP_NAME)
|
||||
assert httpx.get("http://localhost:8000/-/healthz").text == "success"
|
||||
assert json.loads(httpx.get("http://localhost:8000/-/routes").text) == {
|
||||
"/": "default"
|
||||
}
|
||||
assert httpx.get("http://localhost:8000/").text == "hello"
|
||||
|
||||
# Send ctrl+c to shutdown Serve components
|
||||
p.send_signal(signal.SIGINT)
|
||||
p.wait()
|
||||
|
||||
# Make sure Serve components are shutdown
|
||||
status_response = subprocess.check_output(["serve", "status"])
|
||||
status = yaml.safe_load(status_response)
|
||||
assert status == {"applications": {}, "proxies": {}, "target_capacity": None}
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="File path incorrect on Windows.")
|
||||
@pytest.mark.parametrize(
|
||||
"ray_start_stop_in_specific_directory",
|
||||
[
|
||||
os.path.join(os.path.dirname(__file__), "test_config_files"),
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
def test_deploy_with_access_to_current_directory(ray_start_stop_in_specific_directory):
|
||||
"""Test serve deploy using modules in the current directory succeeds.
|
||||
|
||||
There was an issue where dashboard client doesn't add the current directory to
|
||||
the sys.path and failed to deploy a Serve app defined in the directory. This
|
||||
test ensures that files in the current directory can be accessed and deployed.
|
||||
|
||||
See: https://github.com/ray-project/ray/issues/43889
|
||||
"""
|
||||
# Deploy Serve application with a config in the current directory.
|
||||
subprocess.check_output(["serve", "deploy", "use_current_working_directory.yaml"])
|
||||
|
||||
# Ensure serve deploy eventually succeeds.
|
||||
def check_deploy_successfully():
|
||||
status_response = subprocess.check_output(["serve", "status"])
|
||||
assert b"RUNNING" in status_response
|
||||
return True
|
||||
|
||||
wait_for_condition(check_deploy_successfully, timeout=5)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="File path incorrect on Windows.")
|
||||
class TestRayReinitialization:
|
||||
@pytest.fixture
|
||||
def import_file_name(self) -> str:
|
||||
return "ray.serve.tests.test_config_files.ray_already_initialized:app"
|
||||
|
||||
@pytest.fixture
|
||||
def pattern(self) -> Pattern:
|
||||
return re.compile(r"Connecting to existing Ray cluster at address: (.*)\.\.\.")
|
||||
|
||||
@pytest.fixture
|
||||
def ansi_escape(self) -> Pattern:
|
||||
return re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
||||
|
||||
def test_run_without_address(self, import_file_name, ray_start_stop):
|
||||
"""Test serve run with ray already initialized and run without address argument.
|
||||
|
||||
When the imported file already initialized a ray instance and serve doesn't run
|
||||
with address argument, then serve does not reinitialize another ray instance and
|
||||
cause error.
|
||||
"""
|
||||
p = subprocess.Popen(["serve", "run", import_file_name])
|
||||
wait_for_condition(lambda: ping_endpoint() == "foobar", timeout=10)
|
||||
p.send_signal(signal.SIGINT)
|
||||
p.wait()
|
||||
|
||||
def test_run_with_address_same_address(self, import_file_name, ray_start_stop):
|
||||
"""Test serve run with ray already initialized and run with address argument
|
||||
that has the same address as existing ray instance.
|
||||
|
||||
When the imported file already initialized a ray instance and serve runs with
|
||||
address argument same as the ray instance, then serve does not reinitialize
|
||||
another ray instance and cause error.
|
||||
"""
|
||||
p = subprocess.Popen(
|
||||
["serve", "run", "--address=127.0.0.1:6379", import_file_name]
|
||||
)
|
||||
wait_for_condition(lambda: ping_endpoint() == "foobar", timeout=10)
|
||||
p.send_signal(signal.SIGINT)
|
||||
p.wait()
|
||||
|
||||
def test_run_with_address_different_address(
|
||||
self, import_file_name, pattern, ansi_escape, ray_start_stop
|
||||
):
|
||||
"""Test serve run with ray already initialized and run with address argument
|
||||
that has the different address as existing ray instance.
|
||||
|
||||
When the imported file already initialized a ray instance and serve runs with
|
||||
address argument different as the ray instance, then serve does not reinitialize
|
||||
another ray instance and cause error and logs warning to the user.
|
||||
"""
|
||||
p = subprocess.Popen(
|
||||
["serve", "run", "--address=ray://123.45.67.89:50005", import_file_name],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
wait_for_condition(lambda: ping_endpoint() == "foobar", timeout=10)
|
||||
p.send_signal(signal.SIGINT)
|
||||
p.wait()
|
||||
process_output, _ = p.communicate()
|
||||
logs = process_output.decode("utf-8").strip()
|
||||
ray_address = ansi_escape.sub("", pattern.search(logs).group(1))
|
||||
expected_warning_message = (
|
||||
"An address was passed to `serve run` but the imported module also "
|
||||
f"connected to Ray at a different address: '{ray_address}'. You do not "
|
||||
"need to call `ray.init` in your code when using `serve run`."
|
||||
)
|
||||
assert expected_warning_message in logs
|
||||
|
||||
def test_run_with_auto_address(
|
||||
self, import_file_name, pattern, ansi_escape, ray_start_stop
|
||||
):
|
||||
"""Test serve run with ray already initialized and run with "auto" address
|
||||
argument.
|
||||
|
||||
When the imported file already initialized a ray instance and serve runs with
|
||||
address argument same as the ray instance, then serve does not reinitialize
|
||||
another ray instance and cause error.
|
||||
"""
|
||||
p = subprocess.Popen(
|
||||
["serve", "run", "--address=auto", import_file_name],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
wait_for_condition(lambda: ping_endpoint() == "foobar", timeout=10)
|
||||
p.send_signal(signal.SIGINT)
|
||||
p.wait()
|
||||
process_output, _ = p.communicate()
|
||||
logs = process_output.decode("utf-8").strip()
|
||||
ray_address = ansi_escape.sub("", pattern.search(logs).group(1))
|
||||
expected_warning_message = (
|
||||
"An address was passed to `serve run` but the imported module also "
|
||||
f"connected to Ray at a different address: '{ray_address}'. You do not "
|
||||
"need to call `ray.init` in your code when using `serve run`."
|
||||
)
|
||||
assert expected_warning_message not in logs
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-v", "-s", __file__]))
|
||||
Reference in New Issue
Block a user