Files
2026-07-13 13:17:40 +08:00

378 lines
12 KiB
Python

import json
import os
import signal
import subprocess
import sys
import tempfile
import unittest.mock
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import grpc
import pytest
import ray
from ray._common.network_utils import build_address, parse_address
from ray._private import ray_constants
from ray._private.test_utils import persistent_gcs_test_enabled
from ray.client_builder import ClientContext
from ray.cluster_utils import Cluster
from ray.runtime_env.runtime_env import RuntimeEnv
from ray.util.client.common import ClientObjectRef
from ray.util.client.ray_client_helpers import ray_start_client_server
from ray.util.client.worker import Worker
@pytest.mark.skipif(
os.environ.get("CI") and sys.platform == "win32",
reason="Flaky when run on windows CI",
)
@pytest.mark.parametrize("input", [None, "auto"])
def test_ray_address(input, call_ray_start):
address = call_ray_start
with unittest.mock.patch.dict(os.environ, {"RAY_ADDRESS": address}):
res = ray.init(input)
# Ensure this is not a client.connect()
assert not isinstance(res, ClientContext)
assert res.address_info["gcs_address"] == address
ray.shutdown()
addr = f"localhost:{parse_address(address)[-1]}"
with unittest.mock.patch.dict(os.environ, {"RAY_ADDRESS": addr}):
res = ray.init(input)
# Ensure this is not a client.connect()
assert not isinstance(res, ClientContext)
assert res.address_info["gcs_address"] == address
ray.shutdown()
@pytest.mark.parametrize("address", [None, "auto"])
def test_ray_init_no_local_instance(shutdown_only, address):
# Starts a new Ray instance.
if address is None:
ray.init(address=address)
else:
# Throws an error if we explicitly want to connect to an existing
# instance and none exists.
with pytest.raises(ConnectionError):
ray.init(address=address)
@pytest.mark.skipif(
os.environ.get("CI") and sys.platform == "win32",
reason="Flaky when run on windows CI",
)
@pytest.mark.parametrize("address", [None, "auto"])
def test_ray_init_existing_instance(call_ray_start, address):
ray_address = call_ray_start
# If no address is specified, we will default to an existing cluster.
res = ray.init(address=address)
assert res.address_info["gcs_address"] == ray_address
ray.shutdown()
# Start a second local Ray instance.
try:
subprocess.check_output("ray start --head", shell=True)
# If there are multiple local instances, connect to the latest.
res = ray.init(address=address)
assert res.address_info["gcs_address"] != ray_address
ray.shutdown()
# If there are multiple local instances and we specify an address
# explicitly, it works.
with unittest.mock.patch.dict(os.environ, {"RAY_ADDRESS": ray_address}):
res = ray.init(address=address)
assert res.address_info["gcs_address"] == ray_address
finally:
ray.shutdown()
subprocess.check_output("ray stop --force", shell=True)
@pytest.mark.skipif(
os.environ.get("CI") and sys.platform == "win32",
reason="Flaky when run on windows CI",
)
def test_ray_init_existing_instance_via_blocked_ray_start():
"""Run a blocked ray start command and check that ray.init() connects to it."""
blocked_start_cmd = subprocess.Popen(
["ray", "start", "--head", "--block", "--num-cpus", "1999"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
def _wait_for_startup_msg():
for line in blocked_start_cmd.stdout:
l = line.decode("utf-8")
print(l)
if "Ray runtime started." in l:
return
try:
# Wait for the blocked start command's output to indicate that the local Ray
# instance has been started successfully. This is done in a background thread
# because there is no direct way to read the process' stdout with a timeout.
tp = ThreadPoolExecutor(max_workers=1)
fut = tp.submit(_wait_for_startup_msg)
fut.result(timeout=30)
# Verify that `ray.init()` connects to the existing cluster
# (verified by checking the resources specified to the `ray start` command).
ray.init()
assert ray.cluster_resources().get("CPU", 0) == 1999
finally:
ray.shutdown()
blocked_start_cmd.terminate()
blocked_start_cmd.wait()
tp.shutdown()
subprocess.check_output("ray stop --force", shell=True)
@pytest.mark.skipif(
os.environ.get("CI") and sys.platform == "win32",
reason="Flaky when run on windows CI",
)
@pytest.mark.parametrize("address", [None, "auto"])
def test_ray_init_existing_instance_crashed(address):
ray._private.utils.write_ray_address("localhost:6379")
try:
# If no address is specified, we will default to an existing cluster.
ray_constants.NUM_REDIS_GET_RETRIES = 1
with pytest.raises(ConnectionError):
ray.init(address=address)
finally:
ray._common.utils.reset_ray_address()
class Credentials(grpc.ChannelCredentials):
def __init__(self, name):
self.name = name
class Stop(Exception):
def __init__(self, credentials):
self.credentials = credentials
def test_ray_init_credentials_with_client(monkeypatch):
def mock_init(
self,
conn_str="",
secure=False,
metadata=None,
connection_retries=3,
_credentials=None,
):
raise (Stop(_credentials))
monkeypatch.setattr(Worker, "__init__", mock_init)
with pytest.raises(Stop) as stop:
with ray_start_client_server(_credentials=Credentials("test")):
pass
assert stop.value.credentials.name == "test"
def test_ray_init_credential(monkeypatch):
def mock_secure_channel(conn_str, credentials, options=None, compression=None):
raise (Stop(credentials))
monkeypatch.setattr(grpc, "secure_channel", mock_secure_channel)
with pytest.raises(Stop) as stop:
ray.init("ray://127.0.0.1", _credentials=Credentials("test"))
ray.util.disconnect()
assert stop.value.credentials.name == "test"
def test_auto_init_non_client(call_ray_start):
address = call_ray_start
with unittest.mock.patch.dict(os.environ, {"RAY_ADDRESS": address}):
res = ray.put(300)
# Ensure this is not a client.connect()
assert not isinstance(res, ClientObjectRef)
ray.shutdown()
addr = f"localhost:{parse_address(address)[-1]}"
with unittest.mock.patch.dict(os.environ, {"RAY_ADDRESS": addr}):
res = ray.put(300)
# Ensure this is not a client.connect()
assert not isinstance(res, ClientObjectRef)
@pytest.mark.parametrize(
"call_ray_start",
["ray start --head --ray-client-server-port 25036 --port 0"],
indirect=True,
)
@pytest.mark.parametrize(
"function", [lambda: ray.put(300), lambda: ray.remote(ray.nodes).remote()]
)
def test_auto_init_client(call_ray_start, function):
address = parse_address(call_ray_start)[0]
with unittest.mock.patch.dict(
os.environ, {"RAY_ADDRESS": f"ray://{build_address(address, 25036)}"}
):
res = function()
# Ensure this is a client connection.
assert isinstance(res, ClientObjectRef)
ray.shutdown()
with unittest.mock.patch.dict(os.environ, {"RAY_ADDRESS": "ray://localhost:25036"}):
res = function()
# Ensure this is a client connection.
assert isinstance(res, ClientObjectRef)
@pytest.mark.skipif(
os.environ.get("CI") and sys.platform != "linux",
reason="This test is only run on linux CI machines.",
)
def test_ray_init_using_hostname(ray_start_cluster):
import socket
hostname = socket.gethostname()
cluster = Cluster(
initialize_head=True,
head_node_args={
"node_ip_address": hostname,
},
)
# Use `ray.init` to test the connection.
ray.init(address=cluster.address, _node_ip_address=hostname)
node_table = cluster.global_state.node_table()
assert len(node_table) == 1
assert node_table[0].get("NodeManagerHostname", "") == hostname
def test_new_ray_instance_new_session_dir(shutdown_only):
ray.init()
session_dir = ray._private.worker._global_node.get_session_dir_path()
ray.shutdown()
ray.init()
if persistent_gcs_test_enabled():
assert ray._private.worker._global_node.get_session_dir_path() == session_dir
else:
assert ray._private.worker._global_node.get_session_dir_path() != session_dir
def test_new_cluster_new_session_dir(ray_start_cluster):
cluster = ray_start_cluster
cluster.add_node()
ray.init(address=cluster.address)
session_dir = ray._private.worker._global_node.get_session_dir_path()
ray.shutdown()
cluster.shutdown()
cluster.add_node()
ray.init(address=cluster.address)
if persistent_gcs_test_enabled():
assert ray._private.worker._global_node.get_session_dir_path() == session_dir
else:
assert ray._private.worker._global_node.get_session_dir_path() != session_dir
ray.shutdown()
cluster.shutdown()
@pytest.mark.skipif(sys.platform == "win32", reason="SIGTERM only on posix")
def test_ray_init_sigterm_handler():
TEST_FILENAME = "sigterm.txt"
def sigterm_handler_cmd(ray_init=False):
return f"""
import os
import sys
import signal
def sigterm_handler(signum, frame):
f = open("{TEST_FILENAME}", "w")
sys.exit(0)
signal.signal(signal.SIGTERM, sigterm_handler)
import ray
{"ray.init()" if ray_init else ""}
os.kill(os.getpid(), signal.SIGTERM)
"""
# test if sigterm handler is not overwritten by import ray
test_child = subprocess.run(["python", "-c", sigterm_handler_cmd()])
assert test_child.returncode == 0 and os.path.exists(TEST_FILENAME)
os.remove(TEST_FILENAME)
# test if sigterm handler is overwritten by ray.init
test_child = subprocess.run(["python", "-c", sigterm_handler_cmd(ray_init=True)])
assert test_child.returncode == signal.SIGTERM and not os.path.exists(TEST_FILENAME)
@pytest.fixture
def runtime_env_working_dir():
with tempfile.TemporaryDirectory() as tmp_dir:
path = Path(tmp_dir)
working_dir = path / "working_dir"
working_dir.mkdir(parents=True)
yield working_dir
@pytest.fixture
def py_module_whl():
f = tempfile.NamedTemporaryFile(suffix=".whl", delete=False)
f.close()
yield f.name
os.unlink(f.name)
@pytest.fixture
def ray_shutdown():
yield
ray.shutdown()
def test_ray_init_with_runtime_env_as_dict(
runtime_env_working_dir, py_module_whl, ray_shutdown
):
working_dir_path = runtime_env_working_dir
working_dir_str = str(working_dir_path)
ray.init(
runtime_env={"working_dir": working_dir_str, "py_modules": [py_module_whl]}
)
worker = ray._private.worker.global_worker.core_worker
parsed_runtime_env = json.loads(worker.get_current_runtime_env())
assert "gcs://" in parsed_runtime_env["working_dir"]
assert len(parsed_runtime_env["py_modules"]) == 1
assert "gcs://" in parsed_runtime_env["py_modules"][0]
def test_ray_init_with_runtime_env_as_object(
runtime_env_working_dir, py_module_whl, ray_shutdown
):
working_dir_path = runtime_env_working_dir
working_dir_str = str(working_dir_path)
ray.init(
runtime_env=RuntimeEnv(working_dir=working_dir_str, py_modules=[py_module_whl])
)
worker = ray._private.worker.global_worker.core_worker
parsed_runtime_env = json.loads(worker.get_current_runtime_env())
assert "gcs://" in parsed_runtime_env["working_dir"]
assert len(parsed_runtime_env["py_modules"]) == 1
assert "gcs://" in parsed_runtime_env["py_modules"][0]
def test_shutdown_wait_for_processes(shutdown_only):
ray.init()
node = ray._private.worker._global_node
procs = [proc for _, proc in node.live_processes()]
assert procs, "ray.init() should have started subprocesses"
with unittest.mock.patch.object(
node, "kill_all_processes", wraps=node.kill_all_processes
) as mock_kill:
ray.shutdown(wait_for_processes=True)
assert mock_kill.call_args.kwargs.get("wait") is True
assert all(proc.poll() is not None for proc in procs)
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))