1144 lines
36 KiB
Python
1144 lines
36 KiB
Python
import os
|
|
import platform
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
from pathlib import Path
|
|
from typing import List
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
import ray
|
|
from ray._common.test_utils import (
|
|
run_string_as_driver,
|
|
wait_for_condition,
|
|
)
|
|
from ray._common.utils import try_to_create_directory
|
|
from ray._private.runtime_env.conda import (
|
|
_current_py_version,
|
|
_inject_ray_to_conda_site,
|
|
_resolve_install_from_source_ray_dependencies,
|
|
inject_dependencies,
|
|
)
|
|
from ray._private.runtime_env.conda_utils import (
|
|
get_conda_env_list,
|
|
get_conda_envs,
|
|
get_conda_info_json,
|
|
)
|
|
from ray._private.test_utils import (
|
|
chdir,
|
|
run_string_as_driver_nonblocking,
|
|
)
|
|
from ray._private.utils import (
|
|
get_conda_bin_executable,
|
|
get_conda_env_dir,
|
|
)
|
|
from ray.runtime_env import RuntimeEnv
|
|
|
|
if not os.environ.get("CI"):
|
|
# This flags turns on the local development that link against current ray
|
|
# packages and fall back all the dependencies to current python's site.
|
|
os.environ["RAY_RUNTIME_ENV_LOCAL_DEV_MODE"] = "1"
|
|
|
|
EMOJI_VERSIONS = ["2.1.0", "2.2.0"]
|
|
|
|
_WIN32 = os.name == "nt"
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def conda_envs(tmp_path_factory):
|
|
"""Creates two conda env with different `emoji` package versions."""
|
|
conda_path = get_conda_bin_executable("conda")
|
|
init_cmd = f". {os.path.dirname(conda_path)}" f"/../etc/profile.d/conda.sh"
|
|
|
|
def delete_env(env_name):
|
|
subprocess.run(["conda", "remove", "--name", env_name, "--all", "-y"])
|
|
|
|
def create_package_env(env_name, package_version: str):
|
|
delete_env(env_name)
|
|
proc = subprocess.run(
|
|
[
|
|
"conda",
|
|
"create",
|
|
"-n",
|
|
env_name,
|
|
"-y",
|
|
f"python={_current_py_version()}",
|
|
],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
if proc.returncode != 0:
|
|
print("conda create failed, returned %d" % proc.returncode)
|
|
print(proc.stdout.decode())
|
|
print(proc.stderr.decode())
|
|
assert False
|
|
|
|
_inject_ray_to_conda_site(get_conda_env_dir(env_name))
|
|
ray_deps: List[str] = _resolve_install_from_source_ray_dependencies()
|
|
ray_deps.append(f"emoji=={package_version}")
|
|
|
|
reqs = tmp_path_factory.mktemp("reqs") / "requirements.txt"
|
|
with reqs.open("wt") as fid:
|
|
for line in ray_deps:
|
|
fid.write(line)
|
|
fid.write("\n")
|
|
|
|
commands = [
|
|
f"conda activate {env_name}",
|
|
f"python -m pip install -r {str(reqs)}",
|
|
"conda deactivate",
|
|
]
|
|
if _WIN32:
|
|
# as a string
|
|
command = " && ".join(commands)
|
|
else:
|
|
commands.insert(0, init_cmd)
|
|
# as a list
|
|
command = [" && ".join(commands)]
|
|
proc = subprocess.run(
|
|
command,
|
|
shell=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
)
|
|
if proc.returncode != 0:
|
|
print("conda/pip install failed, returned %d" % proc.returncode)
|
|
print("command", command)
|
|
print(proc.stdout.decode())
|
|
print(proc.stderr.decode())
|
|
assert False
|
|
|
|
for package_version in EMOJI_VERSIONS:
|
|
create_package_env(
|
|
env_name=f"package-{package_version}", package_version=package_version
|
|
)
|
|
|
|
yield
|
|
|
|
for package_version in EMOJI_VERSIONS:
|
|
delete_env(env_name=f"package-{package_version}")
|
|
|
|
|
|
@ray.remote
|
|
def get_emoji_version():
|
|
import emoji # noqa: E811
|
|
|
|
return emoji.__version__
|
|
|
|
|
|
@ray.remote
|
|
class VersionActor:
|
|
def get_emoji_version(self):
|
|
import emoji # noqa: E811
|
|
|
|
return emoji.__version__
|
|
|
|
|
|
check_remote_client_conda = """
|
|
import ray
|
|
context = (ray.client("localhost:24001")
|
|
.env({{"conda" : "package-{package_version}"}})
|
|
.connect())
|
|
@ray.remote
|
|
def get_package_version():
|
|
import emoji
|
|
return emoji.__version__
|
|
|
|
assert ray.get(get_package_version.remote()) == "{package_version}"
|
|
context.disconnect()
|
|
"""
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CONDA_DEFAULT_ENV") is None,
|
|
reason="must be run from within a conda environment",
|
|
)
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") and sys.platform != "linux",
|
|
reason="This test is only run on linux CI machines.",
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"call_ray_start",
|
|
["ray start --head --ray-client-server-port 24001 --port 0"],
|
|
indirect=True,
|
|
)
|
|
def test_client_tasks_and_actors_inherit_from_driver(conda_envs, call_ray_start):
|
|
for i, package_version in enumerate(EMOJI_VERSIONS):
|
|
runtime_env = {"conda": f"package-{package_version}"}
|
|
with ray.client("localhost:24001").env(runtime_env).connect():
|
|
assert ray.get(get_emoji_version.remote()) == package_version
|
|
actor_handle = VersionActor.remote()
|
|
assert ray.get(actor_handle.get_emoji_version.remote()) == package_version
|
|
|
|
# Ensure that we can have a second client connect using the other
|
|
# conda environment.
|
|
other_package_version = EMOJI_VERSIONS[(i + 1) % 2]
|
|
run_string_as_driver(
|
|
check_remote_client_conda.format(package_version=other_package_version)
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CONDA_DEFAULT_ENV") is None,
|
|
reason="must be run from within a conda environment",
|
|
)
|
|
def test_task_actor_conda_env(conda_envs, shutdown_only):
|
|
ray.init()
|
|
|
|
# Basic conda runtime env
|
|
for package_version in EMOJI_VERSIONS:
|
|
runtime_env = {"conda": f"package-{package_version}"}
|
|
|
|
task = get_emoji_version.options(runtime_env=runtime_env)
|
|
assert ray.get(task.remote()) == package_version
|
|
|
|
actor = VersionActor.options(runtime_env=runtime_env).remote()
|
|
assert ray.get(actor.get_emoji_version.remote()) == package_version
|
|
|
|
# Runtime env should inherit to nested task
|
|
@ray.remote
|
|
def wrapped_version():
|
|
return ray.get(get_emoji_version.remote())
|
|
|
|
@ray.remote
|
|
class Wrapper:
|
|
def wrapped_version(self):
|
|
return ray.get(get_emoji_version.remote())
|
|
|
|
for package_version in EMOJI_VERSIONS:
|
|
runtime_env = {"conda": f"package-{package_version}"}
|
|
|
|
task = wrapped_version.options(runtime_env=runtime_env)
|
|
assert ray.get(task.remote()) == package_version
|
|
|
|
actor = Wrapper.options(runtime_env=runtime_env).remote()
|
|
assert ray.get(actor.wrapped_version.remote()) == package_version
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CONDA_DEFAULT_ENV") is None,
|
|
reason="must be run from within a conda environment",
|
|
)
|
|
def test_base_full_path(conda_envs, shutdown_only):
|
|
"""
|
|
Test that `base` and its absolute path prefix can both work.
|
|
"""
|
|
ray.init()
|
|
|
|
conda_info = get_conda_info_json()
|
|
prefix = conda_info["conda_prefix"]
|
|
|
|
test_conda_envs = ["base", prefix]
|
|
|
|
@ray.remote
|
|
def get_conda_env_name():
|
|
return os.environ.get("CONDA_DEFAULT_ENV")
|
|
|
|
# Basic conda runtime env
|
|
for conda_env in test_conda_envs:
|
|
runtime_env = {"conda": conda_env}
|
|
|
|
task = get_conda_env_name.options(runtime_env=runtime_env)
|
|
assert ray.get(task.remote()) == "base"
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CONDA_DEFAULT_ENV") is None,
|
|
reason="must be run from within a conda environment",
|
|
)
|
|
def test_task_actor_conda_env_full_path(conda_envs, shutdown_only):
|
|
ray.init()
|
|
|
|
conda_info = get_conda_info_json()
|
|
prefix = conda_info["conda_prefix"]
|
|
|
|
test_conda_envs = {
|
|
package_version: f"{prefix}/envs/package-{package_version}"
|
|
for package_version in EMOJI_VERSIONS
|
|
}
|
|
|
|
# Basic conda runtime env
|
|
for package_version, conda_full_path in test_conda_envs.items():
|
|
runtime_env = {"conda": conda_full_path}
|
|
print(f"Testing {package_version}, runtime env: {runtime_env}")
|
|
|
|
task = get_emoji_version.options(runtime_env=runtime_env)
|
|
assert ray.get(task.remote()) == package_version
|
|
|
|
actor = VersionActor.options(runtime_env=runtime_env).remote()
|
|
assert ray.get(actor.get_emoji_version.remote()) == package_version
|
|
|
|
# Runtime env should inherit to nested task
|
|
@ray.remote
|
|
def wrapped_version():
|
|
return ray.get(get_emoji_version.remote())
|
|
|
|
@ray.remote
|
|
class Wrapper:
|
|
def wrapped_version(self):
|
|
return ray.get(get_emoji_version.remote())
|
|
|
|
for package_version, conda_full_path in test_conda_envs.items():
|
|
runtime_env = {"conda": conda_full_path}
|
|
|
|
task = wrapped_version.options(runtime_env=runtime_env)
|
|
assert ray.get(task.remote()) == package_version
|
|
|
|
actor = Wrapper.options(runtime_env=runtime_env).remote()
|
|
assert ray.get(actor.wrapped_version.remote()) == package_version
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CONDA_DEFAULT_ENV") is None,
|
|
reason="must be run from within a conda environment",
|
|
)
|
|
def test_task_conda_env_validation_cached(conda_envs, shutdown_only):
|
|
"""Verify that when a task is running with the same conda env
|
|
it doesn't validate if env exists.
|
|
"""
|
|
# The first run would be slower because we need to validate
|
|
# if the package exists.
|
|
ray.init()
|
|
version = EMOJI_VERSIONS[0]
|
|
runtime_env = {"conda": f"package-{version}"}
|
|
task = get_emoji_version.options(runtime_env=runtime_env)
|
|
s = time.time()
|
|
ray.get(task.remote())
|
|
first_run = time.time() - s
|
|
# Typically takes 1~2 seconds.
|
|
print("First run took", first_run)
|
|
|
|
# We should verify this doesn't happen
|
|
# from the second run.
|
|
s = time.time()
|
|
for _ in range(10):
|
|
ray.get(task.remote())
|
|
second_10_runs = time.time() - s
|
|
# Typicall takes less than 100ms.
|
|
print("second 10 runs took", second_10_runs)
|
|
assert second_10_runs < first_run
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CONDA_DEFAULT_ENV") is None,
|
|
reason="must be run from within a conda environment",
|
|
)
|
|
def test_job_config_conda_env(conda_envs, shutdown_only):
|
|
for package_version in EMOJI_VERSIONS:
|
|
runtime_env = {"conda": f"package-{package_version}"}
|
|
ray.init(runtime_env=runtime_env)
|
|
assert ray.get(get_emoji_version.remote()) == package_version
|
|
ray.shutdown()
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CONDA_DEFAULT_ENV") is None,
|
|
reason="must be run from within a conda environment",
|
|
)
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") and sys.platform != "linux",
|
|
reason="This test is only run on linux CI machines.",
|
|
)
|
|
@pytest.mark.parametrize("runtime_env_class", [dict, RuntimeEnv])
|
|
def test_job_eager_install(shutdown_only, runtime_env_class):
|
|
# Test enable eager install. This flag is set to True by default.
|
|
runtime_env = {"conda": {"dependencies": ["toolz"]}}
|
|
env_count = len(get_conda_env_list())
|
|
ray.init(runtime_env=runtime_env_class(**runtime_env))
|
|
wait_for_condition(lambda: len(get_conda_env_list()) == env_count + 1, timeout=60)
|
|
ray.shutdown()
|
|
# Test disable eager install
|
|
runtime_env = {
|
|
"conda": {"dependencies": ["toolz"]},
|
|
"config": {"eager_install": False},
|
|
}
|
|
ray.init(runtime_env=runtime_env_class(**runtime_env))
|
|
with pytest.raises(RuntimeError):
|
|
wait_for_condition(
|
|
lambda: len(get_conda_env_list()) == env_count + 2, timeout=5
|
|
)
|
|
ray.shutdown()
|
|
# Test unavailable type
|
|
runtime_env = {
|
|
"conda": {"dependencies": ["toolz"]},
|
|
"config": {"eager_install": 123},
|
|
}
|
|
with pytest.raises(TypeError):
|
|
ray.init(runtime_env=runtime_env_class(**runtime_env))
|
|
ray.shutdown()
|
|
|
|
|
|
def test_get_conda_env_dir(tmp_path):
|
|
"""
|
|
Typical output of `conda env list`, for context:
|
|
|
|
base /Users/scaly/anaconda3
|
|
my_env_1 /Users/scaly/anaconda3/envs/my_env_1
|
|
|
|
For this test, `tmp_path` is a stand-in for `Users/scaly/anaconda3`.
|
|
"""
|
|
|
|
# Simulate starting in an env named tf1.
|
|
d = tmp_path / "envs" / "tf1"
|
|
Path.mkdir(d, parents=True)
|
|
with mock.patch.dict(
|
|
os.environ, {"CONDA_PREFIX": str(d), "CONDA_DEFAULT_ENV": "tf1"}
|
|
):
|
|
with pytest.raises(ValueError):
|
|
# Env tf2 should not exist.
|
|
env_dir = get_conda_env_dir("tf2")
|
|
tf2_dir = tmp_path / "envs" / "tf2"
|
|
Path.mkdir(tf2_dir, parents=True)
|
|
env_dir = get_conda_env_dir("tf2")
|
|
assert env_dir == str(tmp_path / "envs" / "tf2")
|
|
|
|
# Simulate starting in (base) conda env.
|
|
with mock.patch.dict(
|
|
os.environ, {"CONDA_PREFIX": str(tmp_path), "CONDA_DEFAULT_ENV": "base"}
|
|
):
|
|
with pytest.raises(ValueError):
|
|
# Env tf3 should not exist.
|
|
env_dir = get_conda_env_dir("tf3")
|
|
# Env tf2 still should exist.
|
|
env_dir = get_conda_env_dir("tf2")
|
|
assert env_dir == str(tmp_path / "envs" / "tf2")
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CONDA_DEFAULT_ENV") is None,
|
|
reason="must be run from within a conda environment",
|
|
)
|
|
def test_get_conda_envs(conda_envs):
|
|
"""
|
|
Tests that we can at least find 3 conda envs: base, and two envs we created.
|
|
"""
|
|
conda_info = get_conda_info_json()
|
|
envs = get_conda_envs(conda_info)
|
|
prefix = conda_info["conda_prefix"]
|
|
assert ("base", prefix) in envs
|
|
assert ("package-2.1.0", prefix + "/envs/package-2.1.0") in envs
|
|
assert ("package-2.2.0", prefix + "/envs/package-2.2.0") in envs
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CONDA_EXE") is None,
|
|
reason="Requires properly set-up conda shell",
|
|
)
|
|
def test_conda_create_task(shutdown_only):
|
|
"""Tests dynamic creation of a conda env in a task's runtime env. Assumes
|
|
`conda init` has been successfully called."""
|
|
ray.init()
|
|
runtime_env = {
|
|
"conda": {"dependencies": ["pip", {"pip": ["pip-install-test==0.5"]}]}
|
|
}
|
|
|
|
@ray.remote
|
|
def f():
|
|
import pip_install_test # noqa
|
|
|
|
return True
|
|
|
|
with pytest.raises(ModuleNotFoundError):
|
|
# Ensure pip-install-test is not installed on the test machine
|
|
import pip_install_test # noqa
|
|
with pytest.raises(ray.exceptions.RayTaskError) as excinfo:
|
|
ray.get(f.remote())
|
|
assert "ModuleNotFoundError" in str(excinfo.value)
|
|
assert ray.get(f.options(runtime_env=runtime_env).remote())
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") and sys.platform != "linux",
|
|
reason="This test is only run on linux CI machines.",
|
|
)
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CONDA_EXE") is None,
|
|
reason="Requires properly set-up conda shell",
|
|
)
|
|
def test_conda_create_job_config(shutdown_only):
|
|
"""Tests dynamic conda env creation in a runtime env in the JobConfig."""
|
|
|
|
runtime_env = {
|
|
"conda": {"dependencies": ["pip", {"pip": ["pip-install-test==0.5"]}]}
|
|
}
|
|
ray.init(runtime_env=runtime_env)
|
|
|
|
@ray.remote
|
|
def f():
|
|
import pip_install_test # noqa
|
|
|
|
return True
|
|
|
|
with pytest.raises(ModuleNotFoundError):
|
|
# Ensure pip-install-test is not installed on the test machine
|
|
import pip_install_test # noqa
|
|
assert ray.get(f.remote())
|
|
|
|
|
|
def test_inject_dependencies():
|
|
num_tests = 4
|
|
conda_dicts = [None] * num_tests
|
|
outputs = [None] * num_tests
|
|
|
|
conda_dicts[0] = {}
|
|
outputs[0] = {"dependencies": ["python=7.8", "pip", {"pip": ["ray==1.2.3"]}]}
|
|
|
|
conda_dicts[1] = {"dependencies": ["blah"]}
|
|
outputs[1] = {
|
|
"dependencies": ["blah", "python=7.8", "pip", {"pip": ["ray==1.2.3"]}]
|
|
}
|
|
|
|
conda_dicts[2] = {"dependencies": ["blah", "pip"]}
|
|
outputs[2] = {
|
|
"dependencies": ["blah", "pip", "python=7.8", {"pip": ["ray==1.2.3"]}]
|
|
}
|
|
|
|
conda_dicts[3] = {"dependencies": ["blah", "pip", {"pip": ["some_pkg"]}]}
|
|
outputs[3] = {
|
|
"dependencies": [
|
|
"blah",
|
|
"pip",
|
|
{"pip": ["ray==1.2.3", "some_pkg"]},
|
|
"python=7.8",
|
|
]
|
|
}
|
|
|
|
for i in range(num_tests):
|
|
output = inject_dependencies(conda_dicts[i], "7.8", ["ray==1.2.3"])
|
|
error_msg = (
|
|
f"failed on input {i}."
|
|
f"Output: {output} \n"
|
|
f"Expected output: {outputs[i]}"
|
|
)
|
|
assert output == outputs[i], error_msg
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") and sys.platform != "linux",
|
|
reason="This test is only run on linux CI machines.",
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"call_ray_start",
|
|
["ray start --head --ray-client-server-port 24001 --port 0"],
|
|
indirect=True,
|
|
)
|
|
def test_conda_create_ray_client(call_ray_start):
|
|
"""Tests dynamic conda env creation in RayClient."""
|
|
|
|
runtime_env = {
|
|
"conda": {"dependencies": ["pip", {"pip": ["pip-install-test==0.5"]}]}
|
|
}
|
|
|
|
@ray.remote
|
|
def f():
|
|
import pip_install_test # noqa
|
|
|
|
return True
|
|
|
|
with ray.client("localhost:24001").env(runtime_env).connect():
|
|
with pytest.raises(ModuleNotFoundError):
|
|
# Ensure pip-install-test is not installed on the test machine
|
|
import pip_install_test # noqa
|
|
assert ray.get(f.remote())
|
|
|
|
with ray.client("localhost:24001").connect():
|
|
with pytest.raises(ModuleNotFoundError):
|
|
# Ensure pip-install-test is not installed in a client that doesn't
|
|
# use the runtime_env
|
|
ray.get(f.remote())
|
|
|
|
|
|
@pytest.mark.parametrize("pip_as_str", [True, False])
|
|
def test_pip_task(shutdown_only, pip_as_str, tmp_path):
|
|
"""Tests pip installs in the runtime env specified in f.options()."""
|
|
|
|
ray.init()
|
|
if pip_as_str:
|
|
d = tmp_path / "pip_requirements"
|
|
d.mkdir()
|
|
p = d / "requirements.txt"
|
|
requirements_txt = """
|
|
pip-install-test==0.5
|
|
"""
|
|
p.write_text(requirements_txt)
|
|
runtime_env = {"pip": str(p)}
|
|
else:
|
|
runtime_env = {"pip": ["pip-install-test==0.5"]}
|
|
|
|
@ray.remote
|
|
def f():
|
|
import pip_install_test # noqa
|
|
|
|
return True
|
|
|
|
with pytest.raises(ModuleNotFoundError):
|
|
# Ensure pip-install-test is not installed on the test machine
|
|
import pip_install_test # noqa
|
|
with pytest.raises(ray.exceptions.RayTaskError) as excinfo:
|
|
ray.get(f.remote())
|
|
assert "ModuleNotFoundError" in str(excinfo.value)
|
|
assert ray.get(f.options(runtime_env=runtime_env).remote())
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") and sys.platform != "linux",
|
|
reason="This test is only run on linux CI machines.",
|
|
)
|
|
@pytest.mark.parametrize("option", ["conda", "pip"])
|
|
def test_conda_pip_extras_ray_default(shutdown_only, option):
|
|
"""Tests that ray[extras] can be included as a conda/pip dependency."""
|
|
ray.init()
|
|
pip = ["pip-install-test==0.5", "ray[default]"]
|
|
if option == "conda":
|
|
runtime_env = {"conda": {"dependencies": ["pip", {"pip": pip}]}}
|
|
elif option == "pip":
|
|
runtime_env = {"pip": pip}
|
|
else:
|
|
assert False, f"Unknown option: {option}"
|
|
|
|
@ray.remote
|
|
def f():
|
|
import pip_install_test # noqa
|
|
|
|
return True
|
|
|
|
with pytest.raises(ModuleNotFoundError):
|
|
# Ensure pip-install-test is not installed on the test machine
|
|
import pip_install_test # noqa
|
|
with pytest.raises(ray.exceptions.RayTaskError) as excinfo:
|
|
ray.get(f.remote())
|
|
assert "ModuleNotFoundError" in str(excinfo.value)
|
|
assert ray.get(f.options(runtime_env=runtime_env).remote())
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") and sys.platform != "linux",
|
|
reason="This test is only run on linux CI machines.",
|
|
)
|
|
@pytest.mark.parametrize("pip_as_str", [True, False])
|
|
def test_pip_job_config(shutdown_only, pip_as_str, tmp_path):
|
|
"""Tests dynamic installation of pip packages in a task's runtime env."""
|
|
|
|
if pip_as_str:
|
|
d = tmp_path / "pip_requirements"
|
|
d.mkdir()
|
|
p = d / "requirements.txt"
|
|
requirements_txt = """
|
|
pip-install-test==0.5
|
|
"""
|
|
p.write_text(requirements_txt)
|
|
runtime_env = {"pip": str(p)}
|
|
else:
|
|
runtime_env = {"pip": ["pip-install-test==0.5"]}
|
|
|
|
ray.init(runtime_env=runtime_env)
|
|
|
|
@ray.remote
|
|
def f():
|
|
import pip_install_test # noqa
|
|
|
|
return True
|
|
|
|
with pytest.raises(ModuleNotFoundError):
|
|
# Ensure pip-install-test is not installed on the test machine
|
|
import pip_install_test # noqa
|
|
assert ray.get(f.remote())
|
|
|
|
|
|
@pytest.mark.skipif(_WIN32, reason="Fails on windows")
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") and sys.platform != "linux",
|
|
reason="This test is only run on linux CI machines.",
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"call_ray_start",
|
|
["ray start --head --ray-client-server-port 24001 --port 0"],
|
|
indirect=True,
|
|
)
|
|
def test_client_working_dir_filepath(call_ray_start, tmp_path):
|
|
"""Test that pip and conda filepaths work with working_dir."""
|
|
|
|
working_dir = tmp_path / "requirements"
|
|
working_dir.mkdir()
|
|
|
|
pip_file = working_dir / "requirements.txt"
|
|
requirements_txt = """
|
|
pip-install-test==0.5
|
|
"""
|
|
pip_file.write_text(requirements_txt)
|
|
runtime_env_pip = {"working_dir": str(working_dir), "pip": str(pip_file)}
|
|
|
|
conda_file = working_dir / "environment.yml"
|
|
conda_dict = {"dependencies": ["pip", {"pip": ["pip-install-test==0.5"]}]}
|
|
conda_str = yaml.dump(conda_dict)
|
|
conda_file.write_text(conda_str)
|
|
runtime_env_conda = {"working_dir": str(working_dir), "conda": str(conda_file)}
|
|
|
|
@ray.remote
|
|
def f():
|
|
import pip_install_test # noqa
|
|
|
|
return True
|
|
|
|
with ray.client("localhost:24001").connect():
|
|
with pytest.raises(ModuleNotFoundError):
|
|
# Ensure pip-install-test is not installed in a client that doesn't
|
|
# use the runtime_env
|
|
ray.get(f.remote())
|
|
|
|
for runtime_env in [runtime_env_pip, runtime_env_conda]:
|
|
with ray.client("localhost:24001").env(runtime_env).connect():
|
|
with pytest.raises(ModuleNotFoundError):
|
|
# Ensure pip-install-test is not installed on the test machine
|
|
import pip_install_test # noqa
|
|
assert ray.get(f.remote())
|
|
|
|
|
|
@pytest.mark.skipif(_WIN32, reason="Hangs on windows")
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") and sys.platform != "linux",
|
|
reason="This test is only run on linux CI machines.",
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"call_ray_start",
|
|
["ray start --head --ray-client-server-port 24001 --port 0"],
|
|
indirect=True,
|
|
)
|
|
def test_conda_pip_filepaths_remote(call_ray_start, tmp_path):
|
|
"""Test that pip and conda filepaths work, simulating a remote cluster."""
|
|
|
|
working_dir = tmp_path / "requirements"
|
|
working_dir.mkdir()
|
|
|
|
pip_file = working_dir / "requirements.txt"
|
|
requirements_txt = """
|
|
pip-install-test==0.5
|
|
"""
|
|
pip_file.write_text(requirements_txt)
|
|
runtime_env_pip = {"pip": str(pip_file)}
|
|
|
|
conda_file = working_dir / "environment.yml"
|
|
conda_dict = {"dependencies": ["pip", {"pip": ["pip-install-test==0.5"]}]}
|
|
conda_str = yaml.dump(conda_dict)
|
|
conda_file.write_text(conda_str)
|
|
runtime_env_conda = {"conda": str(conda_file)}
|
|
|
|
@ray.remote
|
|
def f():
|
|
import pip_install_test # noqa
|
|
|
|
return True
|
|
|
|
with ray.client("localhost:24001").connect():
|
|
with pytest.raises(ModuleNotFoundError):
|
|
# Ensure pip-install-test is not installed in a client that doesn't
|
|
# use the runtime_env
|
|
ray.get(f.remote())
|
|
|
|
# pip and conda files should be parsed when the function is declared.
|
|
f_pip = f.options(runtime_env=runtime_env_pip)
|
|
f_conda = f.options(runtime_env=runtime_env_conda)
|
|
|
|
# Remove the pip and conda files from the local filesystem. This is
|
|
# necessary to simulate the files not being present on the remote cluster,
|
|
# because in this single-machine test, the cluster has the same filesystem.
|
|
os.remove(pip_file)
|
|
os.remove(conda_file)
|
|
|
|
# Test with and without a working_dir.
|
|
client_envs = [{}, {"working_dir": str(working_dir)}]
|
|
for runtime_env in client_envs:
|
|
with ray.client("localhost:24001").env(runtime_env).connect():
|
|
with pytest.raises(ModuleNotFoundError):
|
|
# Ensure pip-install-test is not installed on the test machine
|
|
import pip_install_test # noqa
|
|
assert ray.get(f_pip.remote()), str(runtime_env)
|
|
assert ray.get(f_conda.remote()), str(runtime_env)
|
|
|
|
|
|
install_env_script = """
|
|
import ray
|
|
import time
|
|
ray.init(address="auto", runtime_env={env})
|
|
@ray.remote
|
|
def f():
|
|
return "hello"
|
|
f.remote()
|
|
# Give the env 5 seconds to begin installing in a new worker.
|
|
time.sleep(5)
|
|
"""
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") and sys.platform != "linux",
|
|
reason="This test is only run on linux CI machines.",
|
|
)
|
|
def test_env_installation_nonblocking(shutdown_only):
|
|
"""Test fix for https://github.com/ray-project/ray/issues/16226."""
|
|
env1 = {"pip": ["pip-install-test==0.5"]}
|
|
|
|
ray.init(runtime_env=env1)
|
|
|
|
@ray.remote
|
|
def f():
|
|
return "hello"
|
|
|
|
# Warm up a worker because it takes time to start.
|
|
ray.get(f.remote())
|
|
|
|
def assert_tasks_finish_quickly(total_sleep_s=0.1):
|
|
"""Call f every 0.01 seconds for total time total_sleep_s."""
|
|
gap_s = 0.01
|
|
for i in range(int(total_sleep_s / gap_s)):
|
|
start = time.time()
|
|
ray.get(f.remote())
|
|
# Env installation takes around 10 to 60 seconds. If we fail the
|
|
# below assert, we can be pretty sure an env installation blocked
|
|
# the task.
|
|
assert time.time() - start < 2.0
|
|
time.sleep(gap_s)
|
|
|
|
assert_tasks_finish_quickly()
|
|
|
|
env2 = {"pip": ["pip-install-test==0.5", "requests"]}
|
|
f.options(runtime_env=env2).remote()
|
|
# Check that installing env2 above does not block tasks using env1.
|
|
assert_tasks_finish_quickly()
|
|
|
|
proc = run_string_as_driver_nonblocking(install_env_script.format(env=env1))
|
|
# Check that installing env1 in a new worker in the script above does not
|
|
# block other tasks that use env1.
|
|
assert_tasks_finish_quickly(total_sleep_s=5)
|
|
proc.kill()
|
|
proc.wait()
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") and sys.platform != "linux",
|
|
reason="This test is only run on linux CI machines.",
|
|
)
|
|
def test_simultaneous_install(shutdown_only):
|
|
"""Test that two envs can be installed without affecting each other."""
|
|
ray.init()
|
|
|
|
@ray.remote
|
|
class VersionWorker:
|
|
def __init__(self, key):
|
|
self.key = key
|
|
|
|
def get(self):
|
|
import emoji
|
|
|
|
return (self.key, emoji.__version__)
|
|
|
|
# Before we used a global lock on conda installs, these two envs would be
|
|
# installed concurrently, leading to errors:
|
|
# https://github.com/ray-project/ray/issues/17086
|
|
# Now we use a global lock, so the envs are installed sequentially.
|
|
worker_1 = VersionWorker.options(
|
|
runtime_env={"pip": {"packages": ["emoji==2.1.0"], "pip_check": False}}
|
|
).remote(key=1)
|
|
worker_2 = VersionWorker.options(
|
|
runtime_env={"pip": {"packages": ["emoji==2.2.0"], "pip_check": False}}
|
|
).remote(key=2)
|
|
|
|
assert ray.get(worker_1.get.remote()) == (1, "2.1.0")
|
|
assert ray.get(worker_2.get.remote()) == (2, "2.2.0")
|
|
|
|
|
|
CLIENT_SERVER_PORT = 24001
|
|
|
|
|
|
@pytest.mark.skipif(_WIN32, reason="Fails on windows")
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") and sys.platform != "linux",
|
|
reason="This test is only run on linux CI machines.",
|
|
)
|
|
# Skip on Linux ARM64 as the test times out. This is probably because some
|
|
# dependencies may not be available for arm64 and must be compiled from source.
|
|
@pytest.mark.skipif(
|
|
sys.platform == "linux" and platform.processor() == "aarch64",
|
|
reason="This test is currently not supported on Linux ARM64",
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"call_ray_start",
|
|
[f"ray start --head --ray-client-server-port {CLIENT_SERVER_PORT} --port 0"],
|
|
indirect=True,
|
|
)
|
|
def test_e2e_complex(call_ray_start, tmp_path):
|
|
"""Test multiple runtime_env options across multiple client connections.
|
|
|
|
1. Run a Ray Client job with both working_dir and pip specified. Check the
|
|
environment using imports and file reads in tasks and actors.
|
|
2. On the same cluster, run another job with a requirements.txt file and
|
|
overriding per-actor and per-task pip requirements.
|
|
"""
|
|
# Create a file to use to test working_dir
|
|
specific_path = tmp_path / "test"
|
|
specific_path.write_text("Hello")
|
|
|
|
with ray.client(f"localhost:{CLIENT_SERVER_PORT}").env(
|
|
{"working_dir": str(tmp_path), "pip": ["pip-install-test"]}
|
|
).connect():
|
|
|
|
# Test that a task is started in the working_dir.
|
|
@ray.remote
|
|
def test_read():
|
|
return Path("./test").read_text()
|
|
|
|
assert ray.get(test_read.remote()) == "Hello"
|
|
|
|
# Check a task has the job's pip requirements and working_dir.
|
|
@ray.remote
|
|
def test_pip():
|
|
import pip_install_test # noqa
|
|
import ray # noqa
|
|
|
|
return Path("./test").read_text()
|
|
|
|
assert ray.get(test_pip.remote()) == "Hello"
|
|
|
|
# Check an actor has the job's pip requirements and working_dir.
|
|
@ray.remote
|
|
class TestActor:
|
|
def test(self):
|
|
import pip_install_test # noqa
|
|
|
|
return Path("./test").read_text()
|
|
|
|
a = TestActor.remote()
|
|
assert ray.get(a.test.remote()) == "Hello"
|
|
|
|
pandas_version = "1.5.3"
|
|
if sys.version_info.major >= 3 and sys.version_info.minor >= 11:
|
|
pandas_version = "2.2.3"
|
|
requirement_path = tmp_path / "requirements.txt"
|
|
requirement_path.write_text(
|
|
"\n".join(
|
|
[
|
|
f"pandas=={pandas_version}",
|
|
"typer",
|
|
"aiofiles",
|
|
]
|
|
)
|
|
)
|
|
|
|
# Start a new job on the same cluster using the requirements file.
|
|
with ray.client(f"localhost:{CLIENT_SERVER_PORT}").env(
|
|
{"working_dir": str(tmp_path), "pip": str(requirement_path)}
|
|
).connect():
|
|
|
|
@ray.remote
|
|
def test_read():
|
|
return Path("./test").read_text()
|
|
|
|
assert ray.get(test_read.remote()) == "Hello"
|
|
|
|
# Check that a task has the job's pip requirements and working_dir.
|
|
@ray.remote
|
|
def test_import():
|
|
import ray # noqa
|
|
import typer # noqa
|
|
|
|
return Path("./test").read_text()
|
|
|
|
assert ray.get(test_import.remote()) == "Hello"
|
|
|
|
# Check that an actor has the job's pip requirements and working_dir.
|
|
@ray.remote
|
|
class TestActor:
|
|
def test(self):
|
|
import ray # noqa
|
|
import typer # noqa
|
|
|
|
return Path("./test").read_text()
|
|
|
|
a = TestActor.options(runtime_env={"pip": str(requirement_path)}).remote()
|
|
assert ray.get(a.test.remote()) == "Hello"
|
|
|
|
# Check that per-task pip specification works and that the job's
|
|
# working_dir is still inherited.
|
|
@ray.remote
|
|
def test_pip():
|
|
import pip_install_test # noqa
|
|
|
|
return Path("./test").read_text()
|
|
|
|
assert (
|
|
ray.get(
|
|
test_pip.options(runtime_env={"pip": ["pip-install-test"]}).remote()
|
|
)
|
|
== "Hello"
|
|
)
|
|
|
|
# Check that pip_install_test is not in the job's pip requirements.
|
|
with pytest.raises(ray.exceptions.RayTaskError) as excinfo:
|
|
ray.get(test_pip.remote())
|
|
assert "ModuleNotFoundError" in str(excinfo.value)
|
|
|
|
# Check that per-actor pip specification works and that the job's
|
|
# working_dir is still inherited.
|
|
@ray.remote
|
|
class TestActor:
|
|
def test(self):
|
|
import pip_install_test # noqa
|
|
|
|
return Path("./test").read_text()
|
|
|
|
a = TestActor.options(runtime_env={"pip": ["pip-install-test"]}).remote()
|
|
assert ray.get(a.test.remote()) == "Hello"
|
|
|
|
|
|
@pytest.mark.skipif(_WIN32, reason="Fails on windows")
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") and sys.platform != "linux",
|
|
reason="This test is only run on linux CI machines.",
|
|
)
|
|
def test_runtime_env_override(call_ray_start):
|
|
# https://github.com/ray-project/ray/issues/16481
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir, chdir(tmpdir):
|
|
ray.init(address="auto", namespace="test")
|
|
|
|
@ray.remote
|
|
class Child:
|
|
def getcwd(self):
|
|
import os
|
|
|
|
return os.getcwd()
|
|
|
|
def read(self, path):
|
|
return open(path).read()
|
|
|
|
def ready(self):
|
|
pass
|
|
|
|
@ray.remote
|
|
class Parent:
|
|
def spawn_child(self, name, runtime_env):
|
|
child = Child.options(
|
|
lifetime="detached", name=name, runtime_env=runtime_env
|
|
).remote()
|
|
ray.get(child.ready.remote())
|
|
|
|
Parent.options(lifetime="detached", name="parent").remote()
|
|
ray.shutdown()
|
|
|
|
with open("hello", "w") as f:
|
|
f.write("world")
|
|
|
|
job_config = ray.job_config.JobConfig(runtime_env={"working_dir": "."})
|
|
ray.init(address="auto", namespace="test", job_config=job_config)
|
|
|
|
os.remove("hello")
|
|
|
|
parent = ray.get_actor("parent")
|
|
|
|
env = ray.get_runtime_context().runtime_env
|
|
print("Spawning with env:", env)
|
|
ray.get(parent.spawn_child.remote("child", env))
|
|
|
|
child = ray.get_actor("child")
|
|
child_cwd = ray.get(child.getcwd.remote())
|
|
# Child should be in tmp runtime resource dir.
|
|
assert child_cwd != os.getcwd(), (child_cwd, os.getcwd())
|
|
assert ray.get(child.read.remote("hello")) == "world"
|
|
|
|
ray.shutdown()
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") and sys.platform != "linux",
|
|
reason="This test is only run on linux CI machines.",
|
|
)
|
|
def test_pip_with_env_vars(start_cluster, tmp_path):
|
|
"""
|
|
The file structure:
|
|
$tmp_path/
|
|
│
|
|
├── setup.py
|
|
├── dist/ # the tar.gz file will be generated here
|
|
└── test_package/
|
|
└── test.py
|
|
"""
|
|
with chdir(tmp_path):
|
|
TEST_ENV_NAME = "TEST_ENV_VARS"
|
|
TEST_ENV_VALUE = "TEST"
|
|
package_name = "test_package"
|
|
package_version = "0.0.1"
|
|
package_dir = tmp_path
|
|
try_to_create_directory(os.path.join(package_dir, package_name))
|
|
|
|
setup_filename = os.path.join(package_dir, "setup.py")
|
|
setup_code = """import os
|
|
from setuptools import setup, find_packages
|
|
from setuptools.command.install import install
|
|
|
|
class InstallTestPackage(install):
|
|
# this function will be called when pip install this package
|
|
def run(self):
|
|
assert os.environ.get('{TEST_ENV_NAME}') == '{TEST_ENV_VALUE}'
|
|
super().run()
|
|
|
|
setup(
|
|
name='{package_name}',
|
|
version='{package_version}',
|
|
packages=find_packages(),
|
|
cmdclass=dict(install=InstallTestPackage),
|
|
license="MIT",
|
|
zip_safe=False,
|
|
)
|
|
""".format(
|
|
TEST_ENV_NAME=TEST_ENV_NAME,
|
|
TEST_ENV_VALUE=TEST_ENV_VALUE,
|
|
package_name=package_name,
|
|
package_version=package_version,
|
|
)
|
|
|
|
with open(setup_filename, "w+") as f:
|
|
f.writelines(setup_code)
|
|
|
|
python_filename = os.path.join(package_dir, package_name, "test.py")
|
|
python_code = "import os; print(os.environ)"
|
|
with open(python_filename, "w+") as f:
|
|
f.writelines(python_code)
|
|
|
|
gz_filename = os.path.join(
|
|
tmp_path,
|
|
"dist",
|
|
"{name}-{ver}.tar.gz".format(name=package_name, ver=package_version),
|
|
)
|
|
subprocess.check_call(["python", "setup.py", "sdist"])
|
|
|
|
with pytest.raises(ray.exceptions.RuntimeEnvSetupError):
|
|
|
|
@ray.remote(
|
|
runtime_env={
|
|
"env_vars": {TEST_ENV_NAME: "failed"},
|
|
"pip": [gz_filename],
|
|
}
|
|
)
|
|
def f1():
|
|
return True
|
|
|
|
ray.get(f1.remote())
|
|
|
|
@ray.remote(
|
|
runtime_env={
|
|
"env_vars": {TEST_ENV_NAME: TEST_ENV_VALUE},
|
|
"pip": [gz_filename],
|
|
}
|
|
)
|
|
def f2():
|
|
return True
|
|
|
|
assert ray.get(f2.remote())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-sv", __file__]))
|