chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
import os
|
||||
import random
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
from contextlib import contextmanager
|
||||
from copy import deepcopy
|
||||
from typing import Any, Dict, Generator
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
import ray
|
||||
from ray import serve
|
||||
from ray._common.test_utils import SignalActor, wait_for_condition
|
||||
from ray._common.usage import usage_lib
|
||||
from ray._common.utils import reset_ray_address
|
||||
from ray.cluster_utils import AutoscalingCluster, Cluster
|
||||
from ray.serve._private.test_utils import (
|
||||
TELEMETRY_ROUTE_PREFIX,
|
||||
TEST_METRICS_EXPORT_PORT,
|
||||
check_ray_started,
|
||||
check_ray_stopped,
|
||||
start_telemetry_app,
|
||||
)
|
||||
from ray.serve.config import HTTPOptions, ProxyLocation, gRPCOptions
|
||||
from ray.serve.context import _get_global_client
|
||||
from ray.tests.conftest import ( # noqa
|
||||
external_redis,
|
||||
propagate_logs,
|
||||
pytest_runtest_makereport,
|
||||
)
|
||||
|
||||
# https://tools.ietf.org/html/rfc6335#section-6
|
||||
MIN_DYNAMIC_PORT = 49152
|
||||
MAX_DYNAMIC_PORT = 65535
|
||||
|
||||
TEST_GRPC_SERVICER_FUNCTIONS = [
|
||||
"ray.serve.generated.serve_pb2_grpc.add_UserDefinedServiceServicer_to_server",
|
||||
"ray.serve.generated.serve_pb2_grpc.add_FruitServiceServicer_to_server",
|
||||
]
|
||||
|
||||
if os.environ.get("RAY_SERVE_INTENTIONALLY_CRASH", False) == 1:
|
||||
serve.controller._CRASH_AFTER_CHECKPOINT_PROBABILITY = 0.5
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _clear_stale_ray_address():
|
||||
# Serve CI runs several test targets per container sharing /tmp/ray; a target
|
||||
# killed mid-run can leave a ray_current_cluster pointing at a dead cluster.
|
||||
# Drop it before each test so an address-less ray.init() starts fresh.
|
||||
reset_ray_address()
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ray_shutdown():
|
||||
serve.shutdown()
|
||||
if ray.is_initialized():
|
||||
ray.shutdown()
|
||||
yield
|
||||
serve.shutdown()
|
||||
if ray.is_initialized():
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ray_cluster():
|
||||
cluster = Cluster()
|
||||
yield cluster
|
||||
serve.shutdown()
|
||||
ray.shutdown()
|
||||
cluster.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ray_autoscaling_cluster(request):
|
||||
# NOTE(zcin): We have to make a deepcopy here because AutoscalingCluster
|
||||
# modifies the dictionary that's passed in.
|
||||
params = deepcopy(request.param)
|
||||
cluster = AutoscalingCluster(**params)
|
||||
cluster.start()
|
||||
yield
|
||||
serve.shutdown()
|
||||
ray.shutdown()
|
||||
cluster.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ray_start(scope="module"):
|
||||
port = random.randint(MIN_DYNAMIC_PORT, MAX_DYNAMIC_PORT)
|
||||
subprocess.check_output(
|
||||
[
|
||||
"ray",
|
||||
"start",
|
||||
"--head",
|
||||
"--num-cpus",
|
||||
"16",
|
||||
"--ray-client-server-port",
|
||||
f"{port}",
|
||||
]
|
||||
)
|
||||
try:
|
||||
yield f"localhost:{port}"
|
||||
finally:
|
||||
subprocess.check_output(["ray", "stop", "--force"])
|
||||
|
||||
|
||||
def _check_ray_stop():
|
||||
try:
|
||||
httpx.get("http://localhost:8265/api/ray/version")
|
||||
return False
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
@contextmanager
|
||||
def start_and_shutdown_ray_cli():
|
||||
subprocess.check_output(["ray", "stop", "--force"])
|
||||
wait_for_condition(_check_ray_stop, timeout=15)
|
||||
subprocess.check_output(["ray", "start", "--head"])
|
||||
|
||||
yield
|
||||
|
||||
subprocess.check_output(["ray", "stop", "--force"])
|
||||
wait_for_condition(_check_ray_stop, timeout=15)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def start_and_shutdown_ray_cli_module():
|
||||
with start_and_shutdown_ray_cli():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_dir():
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
old_dir = os.getcwd()
|
||||
os.chdir(tmp_dir)
|
||||
yield tmp_dir
|
||||
os.chdir(old_dir)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def _shared_serve_instance():
|
||||
# Note(simon):
|
||||
# This line should be not turned on on master because it leads to very
|
||||
# spammy and not useful log in case of a failure in CI.
|
||||
# To run locally, please use this instead.
|
||||
# SERVE_DEBUG_LOG=1 pytest -v -s test_api.py
|
||||
# os.environ["SERVE_DEBUG_LOG"] = "1" <- Do not uncomment this.
|
||||
|
||||
# Overriding task_retry_delay_ms to relaunch actors more quickly
|
||||
ray.init(
|
||||
address="local",
|
||||
num_cpus=36,
|
||||
namespace="default_test_namespace",
|
||||
_metrics_export_port=9999,
|
||||
_system_config={"metrics_report_interval_ms": 1000, "task_retry_delay_ms": 50},
|
||||
)
|
||||
serve.start(
|
||||
proxy_location=ProxyLocation.HeadOnly,
|
||||
http_options={"host": "0.0.0.0"},
|
||||
grpc_options={
|
||||
"port": 9000,
|
||||
"grpc_servicer_functions": TEST_GRPC_SERVICER_FUNCTIONS,
|
||||
},
|
||||
)
|
||||
yield _get_global_client()
|
||||
# Shutdown Serve and Ray when the session ends so that proxy actors
|
||||
# (e.g. HAProxyManager) run their shutdown logic and stop subprocesses.
|
||||
serve.shutdown()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def serve_instance_async(_shared_serve_instance):
|
||||
yield _shared_serve_instance
|
||||
# Clear all state for 2.x applications and deployments.
|
||||
_shared_serve_instance.delete_all_apps()
|
||||
# Clear the ServeHandle cache between tests to avoid them piling up.
|
||||
await _shared_serve_instance.shutdown_cached_handles_async()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def serve_instance(_shared_serve_instance):
|
||||
yield _shared_serve_instance
|
||||
# Clear all state for 2.x applications and deployments.
|
||||
_shared_serve_instance.delete_all_apps()
|
||||
# Clear the ServeHandle cache between tests to avoid them piling up.
|
||||
_shared_serve_instance.shutdown_cached_handles()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def serve_instance_with_signal(serve_instance):
|
||||
client = serve_instance
|
||||
|
||||
signal = SignalActor.options(name="signal123").remote()
|
||||
yield client, signal
|
||||
|
||||
# Delete signal actor so there is no conflict between tests
|
||||
ray.kill(signal)
|
||||
|
||||
|
||||
def check_ray_stop():
|
||||
try:
|
||||
httpx.get("http://localhost:8265/api/ray/version")
|
||||
return False
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def ray_start_stop():
|
||||
subprocess.check_output(["ray", "stop", "--force"])
|
||||
ray.shutdown()
|
||||
wait_for_condition(
|
||||
check_ray_stop,
|
||||
timeout=15,
|
||||
)
|
||||
subprocess.check_output(["ray", "start", "--head"])
|
||||
wait_for_condition(
|
||||
lambda: httpx.get("http://localhost:8265/api/ray/version").status_code == 200,
|
||||
timeout=15,
|
||||
)
|
||||
ray.init("auto")
|
||||
yield
|
||||
serve.shutdown()
|
||||
ray.shutdown()
|
||||
subprocess.check_output(["ray", "stop", "--force"])
|
||||
wait_for_condition(
|
||||
check_ray_stop,
|
||||
timeout=15,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def ray_start_stop_in_specific_directory(request):
|
||||
original_working_dir = os.getcwd()
|
||||
|
||||
# Change working directory so Ray will start in the requested directory.
|
||||
new_working_dir = request.param
|
||||
os.chdir(new_working_dir)
|
||||
print(f"\nChanged working directory to {new_working_dir}\n")
|
||||
|
||||
subprocess.check_output(["ray", "start", "--head"])
|
||||
wait_for_condition(
|
||||
lambda: httpx.get("http://localhost:8265/api/ray/version").status_code == 200,
|
||||
timeout=15,
|
||||
)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
# Change the directory back to the original one.
|
||||
os.chdir(original_working_dir)
|
||||
print(f"\nChanged working directory back to {original_working_dir}\n")
|
||||
|
||||
subprocess.check_output(["ray", "stop", "--force"])
|
||||
wait_for_condition(
|
||||
check_ray_stop,
|
||||
timeout=15,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ray_instance(
|
||||
request: pytest.FixtureRequest,
|
||||
) -> Generator[Dict[str, Any], None, None]:
|
||||
"""Starts and stops a Ray instance for this test.
|
||||
|
||||
Args:
|
||||
request: request.param should contain a dictionary of env vars and
|
||||
their values. The Ray instance will be started with these env vars.
|
||||
|
||||
Yields:
|
||||
Dict[str, Any]: The dict returned by ``ray.init`` for the started cluster.
|
||||
"""
|
||||
|
||||
original_env_vars = os.environ.copy()
|
||||
|
||||
try:
|
||||
requested_env_vars = request.param
|
||||
except AttributeError:
|
||||
requested_env_vars = {}
|
||||
|
||||
os.environ.update(requested_env_vars)
|
||||
yield ray.init(
|
||||
address="local",
|
||||
_metrics_export_port=9999,
|
||||
_system_config={
|
||||
"metrics_report_interval_ms": 1000,
|
||||
"task_retry_delay_ms": 50,
|
||||
},
|
||||
)
|
||||
|
||||
serve.shutdown()
|
||||
ray.shutdown()
|
||||
|
||||
os.environ.clear()
|
||||
os.environ.update(original_env_vars)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def manage_ray_with_telemetry(monkeypatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("RAY_USAGE_STATS_ENABLED", "1")
|
||||
m.setenv(
|
||||
"RAY_USAGE_STATS_REPORT_URL",
|
||||
f"http://127.0.0.1:8000{TELEMETRY_ROUTE_PREFIX}",
|
||||
)
|
||||
m.setenv("RAY_USAGE_STATS_REPORT_INTERVAL_S", "1")
|
||||
subprocess.check_output(["ray", "stop", "--force"])
|
||||
wait_for_condition(check_ray_stopped, timeout=5)
|
||||
|
||||
subprocess.check_output(["ray", "start", "--head"])
|
||||
wait_for_condition(check_ray_started, timeout=5)
|
||||
|
||||
storage = start_telemetry_app()
|
||||
wait_for_condition(
|
||||
lambda: ray.get(storage.get_reports_received.remote()) > 1, timeout=15
|
||||
)
|
||||
|
||||
yield storage
|
||||
|
||||
# Call Python API shutdown() methods to clear global variable state
|
||||
serve.shutdown()
|
||||
ray.shutdown()
|
||||
|
||||
# Reset global state (any keys that may have been set and cached while the
|
||||
# workload was running).
|
||||
usage_lib.reset_global_state()
|
||||
|
||||
# Shut down Ray cluster with CLI
|
||||
subprocess.check_output(["ray", "stop", "--force"])
|
||||
wait_for_condition(check_ray_stopped, timeout=5)
|
||||
|
||||
|
||||
def wait_for_metrics_port_free(port=TEST_METRICS_EXPORT_PORT, timeout=30):
|
||||
"""
|
||||
Ensures the metrics export port is freed.
|
||||
"""
|
||||
|
||||
def port_free():
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
s.bind(("", port))
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
finally:
|
||||
s.close()
|
||||
|
||||
wait_for_condition(port_free, timeout=timeout, retry_interval_ms=200)
|
||||
|
||||
|
||||
def wait_for_metrics_endpoint(session_name, port=TEST_METRICS_EXPORT_PORT, timeout=30):
|
||||
"""
|
||||
Ensures the current dashboard agent is serving the metrics endpoint. A
|
||||
timeout indicates another agent is still running and holding the port.
|
||||
"""
|
||||
|
||||
def ready():
|
||||
try:
|
||||
resp = httpx.get(f"http://localhost:{port}/metrics", timeout=1.0)
|
||||
except Exception:
|
||||
return False
|
||||
return resp.status_code == 200 and f'SessionName="{session_name}"' in resp.text
|
||||
|
||||
wait_for_condition(ready, timeout=timeout, retry_interval_ms=500)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def metrics_start_shutdown(request):
|
||||
param = request.param if hasattr(request, "param") else None
|
||||
request_timeout_s = param if param else None
|
||||
"""Fixture provides a fresh Ray cluster to prevent metrics state sharing."""
|
||||
wait_for_metrics_port_free()
|
||||
ray.init(
|
||||
address="local",
|
||||
_metrics_export_port=TEST_METRICS_EXPORT_PORT,
|
||||
_system_config={
|
||||
"metrics_report_interval_ms": 100,
|
||||
"task_retry_delay_ms": 50,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
session_name = ray._private.worker._global_node.session_name
|
||||
wait_for_metrics_endpoint(session_name)
|
||||
|
||||
grpc_port = 9000
|
||||
grpc_servicer_functions = [
|
||||
"ray.serve.generated.serve_pb2_grpc.add_UserDefinedServiceServicer_to_server",
|
||||
"ray.serve.generated.serve_pb2_grpc.add_FruitServiceServicer_to_server",
|
||||
]
|
||||
yield serve.start(
|
||||
grpc_options=gRPCOptions(
|
||||
port=grpc_port,
|
||||
grpc_servicer_functions=grpc_servicer_functions,
|
||||
request_timeout_s=request_timeout_s,
|
||||
),
|
||||
http_options=HTTPOptions(
|
||||
host="0.0.0.0",
|
||||
request_timeout_s=request_timeout_s,
|
||||
),
|
||||
)
|
||||
finally:
|
||||
serve.shutdown()
|
||||
ray.shutdown()
|
||||
reset_ray_address()
|
||||
|
||||
|
||||
# Helper function to return the node ID of a remote worker.
|
||||
@ray.remote(num_cpus=0)
|
||||
def _get_node_id():
|
||||
return ray.get_runtime_context().get_node_id()
|
||||
|
||||
|
||||
# Test fixture to start a Serve instance in a RayCluster with two labeled nodes
|
||||
@pytest.fixture(scope="module")
|
||||
def serve_instance_with_labeled_nodes():
|
||||
cluster = Cluster()
|
||||
|
||||
# Unlabeled default node.
|
||||
cluster.add_node(num_cpus=3, resources={"worker0": 1})
|
||||
|
||||
# Node 1 - labeled A100 node in us-west.
|
||||
cluster.add_node(
|
||||
num_cpus=3,
|
||||
resources={"worker1": 1},
|
||||
labels={"region": "us-west", "gpu-type": "A100"},
|
||||
)
|
||||
|
||||
# Node 2 - labeled H100 node in us-east.
|
||||
cluster.add_node(
|
||||
num_cpus=3,
|
||||
resources={"worker2": 1},
|
||||
labels={"region": "us-east", "gpu-type": "H100"},
|
||||
)
|
||||
|
||||
cluster.wait_for_nodes()
|
||||
|
||||
if ray.is_initialized():
|
||||
ray.shutdown()
|
||||
|
||||
ray.init(address=cluster.address)
|
||||
|
||||
node_1_id = ray.get(_get_node_id.options(resources={"worker1": 1}).remote())
|
||||
node_2_id = ray.get(_get_node_id.options(resources={"worker2": 1}).remote())
|
||||
|
||||
serve.start()
|
||||
|
||||
yield _get_global_client(), node_1_id, node_2_id, cluster
|
||||
|
||||
serve.shutdown()
|
||||
ray.shutdown()
|
||||
cluster.shutdown()
|
||||
Reference in New Issue
Block a user