292 lines
8.8 KiB
Python
292 lines
8.8 KiB
Python
"""Client tests that run their own init (as with init_and_serve) live here"""
|
|
|
|
import json
|
|
import random
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
import ray
|
|
import ray.core.generated.ray_client_pb2 as ray_client_pb2
|
|
import ray.util.client.server.server as ray_client_server
|
|
from ray._private.client_mode_hook import _explicitly_disable_client_mode
|
|
from ray._private.ray_logging.logging_config import LoggingConfig
|
|
from ray.cloudpickle.compat import pickle
|
|
from ray.cluster_utils import cluster_not_supported
|
|
from ray.job_config import JobConfig
|
|
from ray.util.client import _ClientContext, ray as ray_client
|
|
from ray.util.client.worker import prepare_init_request_args
|
|
|
|
|
|
@ray.remote
|
|
def hello_world():
|
|
c1 = complex_task.remote(random.randint(1, 10))
|
|
c2 = complex_task.remote(random.randint(1, 10))
|
|
return sum(ray.get([c1, c2]))
|
|
|
|
|
|
@ray.remote
|
|
def complex_task(value):
|
|
time.sleep(1)
|
|
return value * 10
|
|
|
|
|
|
@ray.remote
|
|
class C:
|
|
def __init__(self, x):
|
|
self.val = x
|
|
|
|
def double(self):
|
|
self.val += self.val
|
|
|
|
def get(self):
|
|
return self.val
|
|
|
|
|
|
@pytest.mark.xfail(cluster_not_supported, reason="cluster not supported")
|
|
@pytest.fixture
|
|
def init_and_serve_lazy(ray_start_cluster):
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=1, num_gpus=0)
|
|
cluster.wait_for_nodes(1)
|
|
address = cluster.address
|
|
|
|
def connect(job_config=None, **ray_init_kwargs):
|
|
ray.init(address=address, job_config=job_config, **ray_init_kwargs)
|
|
|
|
server_handle = ray_client_server.serve("localhost", 50051, connect)
|
|
yield server_handle
|
|
ray_client_server.shutdown_with_server(server_handle.grpc_server)
|
|
|
|
|
|
def test_validate_port():
|
|
"""Check that ports outside of 1024-65535 are rejected."""
|
|
for port in [1000, 1023, 65536, 700000]:
|
|
with pytest.raises(subprocess.CalledProcessError) as excinfo:
|
|
subprocess.check_output(
|
|
[
|
|
"ray",
|
|
"start",
|
|
"--head",
|
|
"--num-cpus",
|
|
"8",
|
|
"--ray-client-server-port",
|
|
f"{port}",
|
|
]
|
|
)
|
|
assert "ValueError" in str(excinfo.traceback)
|
|
assert "65535" in str(excinfo.traceback)
|
|
|
|
|
|
def test_basic_preregister(init_and_serve):
|
|
"""Tests conversion of Ray actors and remote functions to client actors
|
|
and client remote functions.
|
|
|
|
Checks that the conversion works when disconnecting and reconnecting client
|
|
sessions.
|
|
"""
|
|
from ray.util.client import ray
|
|
|
|
for _ in range(2):
|
|
ray.connect("localhost:50051")
|
|
val = ray.get(hello_world.remote())
|
|
print(val)
|
|
assert val >= 20
|
|
assert val <= 200
|
|
c = C.remote(3)
|
|
x = c.double.remote()
|
|
y = c.double.remote()
|
|
ray.wait([x, y])
|
|
val = ray.get(c.get.remote())
|
|
assert val == 12
|
|
ray.disconnect()
|
|
|
|
|
|
def test_idempotent_disconnect(init_and_serve):
|
|
from ray.util.client import ray
|
|
|
|
ray.disconnect()
|
|
ray.disconnect()
|
|
ray.connect("localhost:50051")
|
|
ray.disconnect()
|
|
ray.disconnect()
|
|
|
|
|
|
def test_public_shutdown_wait_for_processes_in_client_mode(init_and_serve):
|
|
ray_client.connect("localhost:50051")
|
|
try:
|
|
assert ray_client.is_connected()
|
|
# In client mode, wait_for_processes is a no-op
|
|
ray.shutdown(wait_for_processes=True)
|
|
assert not ray_client.is_connected()
|
|
finally:
|
|
_explicitly_disable_client_mode()
|
|
|
|
|
|
def test_num_clients(init_and_serve_lazy):
|
|
# Tests num clients reporting; useful if you want to build an app that
|
|
# load balances clients between Ray client servers.
|
|
|
|
def get_job_id(api):
|
|
return api.get_runtime_context().worker.current_job_id
|
|
|
|
api1 = _ClientContext()
|
|
info1 = api1.connect("localhost:50051")
|
|
job_id_1 = get_job_id(api1)
|
|
assert info1["num_clients"] == 1, info1
|
|
api2 = _ClientContext()
|
|
info2 = api2.connect("localhost:50051")
|
|
job_id_2 = get_job_id(api2)
|
|
assert info2["num_clients"] == 2, info2
|
|
|
|
assert job_id_1 == job_id_2
|
|
|
|
# Disconnect the first two clients.
|
|
api1.disconnect()
|
|
api2.disconnect()
|
|
time.sleep(1)
|
|
|
|
api3 = _ClientContext()
|
|
info3 = api3.connect("localhost:50051")
|
|
job_id_3 = get_job_id(api3)
|
|
assert info3["num_clients"] == 1, info3
|
|
assert job_id_1 != job_id_3
|
|
|
|
# Check info contains ray and python version.
|
|
assert isinstance(info3["ray_version"], str), info3
|
|
assert isinstance(info3["ray_commit"], str), info3
|
|
assert isinstance(info3["python_version"], str), info3
|
|
api3.disconnect()
|
|
|
|
|
|
def test_python_version(init_and_serve):
|
|
server_handle = init_and_serve
|
|
ray = _ClientContext()
|
|
info1 = ray.connect("localhost:50051")
|
|
assert info1["python_version"] == ".".join(
|
|
[str(x) for x in list(sys.version_info)[:3]]
|
|
)
|
|
ray.disconnect()
|
|
time.sleep(1)
|
|
|
|
def mock_connection_response():
|
|
return ray_client_pb2.ConnectionInfoResponse(
|
|
num_clients=1,
|
|
python_version="2.7.12",
|
|
ray_version="",
|
|
ray_commit="",
|
|
)
|
|
|
|
# inject mock connection function
|
|
server_handle.data_servicer._build_connection_response = mock_connection_response
|
|
|
|
ray = _ClientContext()
|
|
with pytest.raises(RuntimeError):
|
|
_ = ray.connect("localhost:50051")
|
|
|
|
ray = _ClientContext()
|
|
info3 = ray.connect("localhost:50051", ignore_version=True)
|
|
assert info3["num_clients"] == 1, info3
|
|
ray.disconnect()
|
|
|
|
|
|
@patch("ray.util.client.server.dataservicer.CLIENT_SERVER_MAX_THREADS", 6)
|
|
@patch("ray.util.client.server.logservicer.CLIENT_SERVER_MAX_THREADS", 6)
|
|
def test_max_clients(init_and_serve):
|
|
# Tests max clients. Should support up to CLIENT_SERVER_MAX_THREADS / 2.
|
|
def get_job_id(api):
|
|
return api.get_runtime_context().worker.current_job_id
|
|
|
|
for i in range(3):
|
|
api1 = _ClientContext()
|
|
info1 = api1.connect("localhost:50051")
|
|
|
|
assert info1["num_clients"] == i + 1, info1
|
|
|
|
with pytest.raises(ConnectionError):
|
|
api = _ClientContext()
|
|
_ = api.connect("localhost:50051")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"call_ray_start",
|
|
["ray start --head --ray-client-server-port=50056"],
|
|
indirect=True,
|
|
)
|
|
def test_logging_config_in_client_mode(call_ray_start):
|
|
"""logging_config reaches task workers (JobConfig) when using Ray Client."""
|
|
|
|
@ray.remote
|
|
def job_logging_encoding():
|
|
w = ray.get_runtime_context().worker
|
|
jlc = w.job_logging_config
|
|
return None if jlc is None else jlc.encoding
|
|
|
|
ray.init(
|
|
"ray://localhost:50056",
|
|
logging_config=LoggingConfig(encoding="TEXT", log_level="INFO"),
|
|
log_to_driver=False,
|
|
configure_logging=False,
|
|
)
|
|
|
|
assert ray.get(job_logging_encoding.remote()) == "TEXT"
|
|
|
|
|
|
def test_prepare_init_request_args():
|
|
"""``prepare_init_request_args`` copies kwargs, JSON-serializes logging config,
|
|
propagates it onto ``JobConfig``, and pickles the job for ``InitRequest``."""
|
|
# no job config, no ray init kwargs
|
|
serialized, kwargs = prepare_init_request_args(None, None)
|
|
assert serialized is None and kwargs == {}
|
|
|
|
# invalid logging config type
|
|
with pytest.raises(
|
|
TypeError, match="logging_config must be a dict or LoggingConfig"
|
|
):
|
|
prepare_init_request_args(JobConfig(), {"logging_config": 42})
|
|
|
|
# ray init kwargs contains serialized logging config
|
|
lc = LoggingConfig(encoding="JSON", log_level="DEBUG")
|
|
orig = {"logging_config": lc, "num_cpus": 1}
|
|
job = JobConfig()
|
|
_, out = prepare_init_request_args(job, orig)
|
|
assert isinstance(orig["logging_config"], LoggingConfig)
|
|
assert isinstance(out["logging_config"], dict)
|
|
assert out["logging_config"]["encoding"] == "JSON"
|
|
assert orig["num_cpus"] == 1
|
|
json.dumps(out)
|
|
assert job.py_logging_config is not None
|
|
assert job.py_logging_config.encoding == "JSON"
|
|
|
|
# write logging config to JobConfig
|
|
job_dict = JobConfig()
|
|
prepare_init_request_args(
|
|
job_dict,
|
|
{"logging_config": {"encoding": "TEXT", "log_level": "ERROR"}},
|
|
)
|
|
assert job_dict.py_logging_config.encoding == "TEXT"
|
|
assert job_dict.py_logging_config.log_level == "ERROR"
|
|
|
|
# don't overwrite existing logging config on JobConfig
|
|
existing = LoggingConfig(encoding="TEXT")
|
|
job_keep = JobConfig()
|
|
job_keep.set_py_logging_config(existing)
|
|
prepare_init_request_args(
|
|
job_keep,
|
|
{"logging_config": LoggingConfig(encoding="JSON")},
|
|
)
|
|
assert job_keep.py_logging_config is existing
|
|
|
|
# pickle the job for InitRequest
|
|
job_pickle = JobConfig()
|
|
blob, _ = prepare_init_request_args(job_pickle, {})
|
|
assert isinstance(blob, bytes) and len(blob) > 0
|
|
assert isinstance(pickle.loads(blob), JobConfig)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-sv", __file__]))
|