459 lines
13 KiB
Python
459 lines
13 KiB
Python
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
from subprocess import PIPE, STDOUT, Popen, list2cmdline
|
|
from typing import List
|
|
|
|
import pytest
|
|
|
|
import ray
|
|
import ray.cloudpickle as pickle
|
|
from ray._common.test_utils import (
|
|
run_string_as_driver,
|
|
wait_for_condition,
|
|
)
|
|
from ray._private.test_utils import (
|
|
format_web_url,
|
|
run_string_as_driver_nonblocking,
|
|
wait_for_pid_to_exit,
|
|
)
|
|
from ray.dashboard.modules.job.pydantic_models import JobDetails
|
|
from ray.job_config import JobConfig, LoggingConfig
|
|
from ray.job_submission import JobStatus, JobSubmissionClient
|
|
|
|
|
|
def execute_driver(commands: List[str], input: bytes = None):
|
|
p = None
|
|
outs = []
|
|
try:
|
|
p = Popen(commands, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
|
|
if isinstance(input, str):
|
|
input = input.encode()
|
|
|
|
stdout, stderr = p.communicate(input=input)
|
|
outs = stdout.decode().split("\n")
|
|
return outs
|
|
finally:
|
|
if p:
|
|
try:
|
|
p.kill()
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def test_invalid_gcs_address():
|
|
with pytest.raises(ValueError):
|
|
JobSubmissionClient("foobar")
|
|
|
|
with pytest.raises(ValueError):
|
|
JobSubmissionClient("")
|
|
|
|
with pytest.raises(ValueError):
|
|
JobSubmissionClient("abc:abc")
|
|
|
|
|
|
def test_job_isolation(ray_start_regular):
|
|
# Make sure two jobs with same module name
|
|
# don't interfere with each other
|
|
# (https://github.com/ray-project/ray/issues/19358).
|
|
|
|
gcs_address = ray_start_regular.address_info["gcs_address"]
|
|
|
|
lib_template = """
|
|
import ray
|
|
|
|
@ray.remote
|
|
def task():
|
|
return subtask()
|
|
|
|
def subtask():
|
|
return {}
|
|
"""
|
|
driver_template = """
|
|
import ray
|
|
import os
|
|
import sys
|
|
|
|
# Add current directory to Python path so we can import lib.py
|
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, current_dir)
|
|
|
|
import lib
|
|
|
|
ray.init(address="{}", include_dashboard=True)
|
|
assert ray.get(lib.task.remote()) == {}
|
|
"""
|
|
|
|
def setup_driver_files(base_path: str, version: int) -> str:
|
|
version_path = os.path.join(base_path, f"v{version}")
|
|
os.makedirs(version_path)
|
|
|
|
lib_path = os.path.join(version_path, "lib.py")
|
|
driver_path = os.path.join(version_path, "driver.py")
|
|
|
|
with open(lib_path, "w") as f:
|
|
f.write(lib_template.format(version))
|
|
with open(driver_path, "w") as f:
|
|
f.write(driver_template.format(gcs_address, version))
|
|
|
|
return driver_path
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
v1_driver = setup_driver_files(tmpdir, version=1)
|
|
v2_driver = setup_driver_files(tmpdir, version=2)
|
|
|
|
subprocess.check_call([sys.executable, v1_driver])
|
|
subprocess.check_call([sys.executable, v2_driver])
|
|
|
|
dashboard_url = ray_start_regular.dashboard_url
|
|
client = JobSubmissionClient(f"http://{dashboard_url}")
|
|
jobs = client.list_jobs()
|
|
assert len(jobs) == 3 # ray_start_regular, v1, v2
|
|
|
|
num_succeeded = 0
|
|
num_running = 0
|
|
for job in jobs:
|
|
if job.status == "SUCCEEDED":
|
|
num_succeeded += 1
|
|
elif job.status == "RUNNING":
|
|
num_running += 1
|
|
assert num_succeeded == 2
|
|
assert num_running == 1
|
|
|
|
|
|
def test_job_observability(ray_start_regular):
|
|
driver_template = """
|
|
import ray
|
|
from time import sleep
|
|
|
|
ray.init(address="{}", include_dashboard=True)
|
|
open("{}", "w+").close()
|
|
|
|
print("My job id: ", str(ray.get_runtime_context().get_job_id()))
|
|
|
|
{}
|
|
ray.shutdown()
|
|
"""
|
|
tmpfile1 = tempfile.NamedTemporaryFile("w+", suffix=".tmp", prefix="_")
|
|
tmpfile2 = tempfile.NamedTemporaryFile("w+", suffix=".tmp", prefix="_")
|
|
tmpfiles = [tmpfile1.name, tmpfile2.name]
|
|
tmpfile1.close()
|
|
tmpfile2.close()
|
|
for tmpfile in tmpfiles:
|
|
if os.path.exists(tmpfile):
|
|
os.unlink(tmpfile)
|
|
|
|
non_hanging = driver_template.format(
|
|
ray_start_regular["address"], tmpfiles[0], "sleep(1)"
|
|
)
|
|
hanging_driver = driver_template.format(
|
|
ray_start_regular["address"], tmpfiles[1], "sleep(60)"
|
|
)
|
|
|
|
out = run_string_as_driver(non_hanging)
|
|
p = run_string_as_driver_nonblocking(hanging_driver)
|
|
# The nonblocking process needs time to connect.
|
|
while not os.path.exists(tmpfiles[1]):
|
|
time.sleep(1)
|
|
|
|
jobs = list(ray._private.state.jobs())
|
|
jobs.sort(key=lambda x: x["JobID"])
|
|
|
|
driver = jobs[0]
|
|
finished = jobs[1]
|
|
running = jobs[2]
|
|
|
|
# The initial driver timestamp/start time go down a different code path.
|
|
assert driver["Timestamp"] == driver["StartTime"]
|
|
assert finished["Timestamp"] == finished["EndTime"]
|
|
assert running["Timestamp"] == running["StartTime"]
|
|
|
|
assert finished["EndTime"] > finished["StartTime"] > 0, out
|
|
lapsed = finished["EndTime"] - finished["StartTime"]
|
|
assert 0 < lapsed < 5000, f"Job should've taken ~1s, {finished}"
|
|
|
|
assert running["StartTime"] > 0
|
|
assert running["EndTime"] == 0
|
|
|
|
assert len(running["DriverIPAddress"]) > 0
|
|
assert running["DriverPid"] > 0
|
|
assert len(finished["DriverIPAddress"]) > 0
|
|
assert finished["DriverPid"] > 0
|
|
|
|
p.kill()
|
|
# Give the second job time to clean itself up.
|
|
time.sleep(1)
|
|
|
|
jobs = list(ray._private.state.jobs())
|
|
jobs.sort(key=lambda x: x["JobID"])
|
|
|
|
# jobs[0] is the test case driver.
|
|
finished = jobs[1]
|
|
prev_running = jobs[2]
|
|
|
|
assert finished["EndTime"] > finished["StartTime"] > 0, f"{finished}"
|
|
assert finished["EndTime"] == finished["Timestamp"]
|
|
lapsed = finished["EndTime"] - finished["StartTime"]
|
|
assert 0 < lapsed < 5000, f"Job should've taken ~1s {finished}"
|
|
|
|
assert prev_running["EndTime"] > prev_running["StartTime"] > 0
|
|
|
|
assert len(prev_running["DriverIPAddress"]) > 0
|
|
assert prev_running["DriverPid"] > 0
|
|
|
|
|
|
def test_config_metadata(shutdown_only):
|
|
job_config = JobConfig(metadata={"abc": "xyz"})
|
|
job_config.set_metadata("xyz", "abc")
|
|
|
|
ray.init(job_config=job_config, include_dashboard=True)
|
|
|
|
from_worker = ray._private.worker.global_worker.core_worker.get_job_config()
|
|
|
|
assert dict(from_worker.metadata) == job_config.metadata
|
|
|
|
|
|
def test_logging_config_serialization():
|
|
logging_config = LoggingConfig(encoding="TEXT")
|
|
serialized_py_logging_config = pickle.dumps(logging_config)
|
|
job_config = JobConfig()
|
|
job_config.set_py_logging_config(logging_config)
|
|
pb = job_config._get_proto_job_config()
|
|
assert pb.serialized_py_logging_config == serialized_py_logging_config
|
|
|
|
|
|
def test_get_entrypoint(tmp_path):
|
|
get_entrypoint = """
|
|
from ray._private.utils import get_entrypoint_name
|
|
print("result:", get_entrypoint_name())
|
|
"""
|
|
|
|
def line_exists(lines: List[str], regex_target: str):
|
|
p = re.compile(regex_target)
|
|
for line in lines:
|
|
m = p.match(line.strip(" \n"))
|
|
if m:
|
|
return True
|
|
print(f"No target {regex_target} in lines {lines}")
|
|
return False
|
|
|
|
# Test a regular script.
|
|
fp = tmp_path / "test.py"
|
|
fp.write_text(get_entrypoint)
|
|
outputs = execute_driver([sys.executable, str(fp), "--flag"])
|
|
assert line_exists(outputs, f"result: {sys.executable} {fp} --flag")
|
|
|
|
# Test python shell
|
|
outputs = execute_driver([sys.executable, "-i"], input=get_entrypoint)
|
|
assert line_exists(
|
|
outputs, rf".*result: \(interactive_shell\) {re.escape(sys.executable)} -i.*"
|
|
)
|
|
|
|
# Test IPython shell
|
|
outputs = execute_driver(["ipython"], input=get_entrypoint)
|
|
assert line_exists(outputs, r".*result: \(interactive_shell\).*ipython")
|
|
|
|
|
|
def test_removed_internal_flags(shutdown_only):
|
|
ray.init(include_dashboard=True)
|
|
address = ray._private.worker._global_node.webui_url
|
|
address = format_web_url(address)
|
|
client = JobSubmissionClient(address)
|
|
|
|
# Tests this env var is not set.
|
|
job_submission_id = client.submit_job(
|
|
entrypoint='[ -z "${RAY_JOB_ID+x}" ] && '
|
|
'echo "RAY_JOB_ID is not set" || '
|
|
'{ echo "RAY_JOB_ID is set to $RAY_JOB_ID"; return 1; }'
|
|
)
|
|
|
|
def job_finished():
|
|
status = client.get_job_status(job_submission_id)
|
|
assert status != JobStatus.FAILED
|
|
return status == JobStatus.SUCCEEDED
|
|
|
|
wait_for_condition(job_finished)
|
|
|
|
all_logs = client.get_job_logs(job_submission_id)
|
|
assert "RAY_JOB_ID is not set" in all_logs
|
|
|
|
|
|
def test_entrypoint_field(shutdown_only, tmp_path):
|
|
"""Make sure the entrypoint field is correctly set for jobs."""
|
|
driver = """
|
|
import ray
|
|
ray.init("auto", include_dashboard=True)
|
|
|
|
@ray.remote
|
|
def f():
|
|
pass
|
|
|
|
ray.get(f.remote())
|
|
"""
|
|
ray.init(include_dashboard=True)
|
|
address = ray._private.worker._global_node.webui_url
|
|
address = format_web_url(address)
|
|
client = JobSubmissionClient(address)
|
|
|
|
# Test a regular script.
|
|
fp = tmp_path / "driver.py"
|
|
fp.write_text(driver)
|
|
|
|
"""
|
|
Test driver.
|
|
"""
|
|
commands = [sys.executable, str(fp), "--flag"]
|
|
print(execute_driver(commands))
|
|
|
|
jobs = ray.state.jobs()
|
|
assert len(jobs) == 2
|
|
jobs = list(jobs)
|
|
jobs.sort(key=lambda j: j["JobID"])
|
|
|
|
# The first job is the test job.
|
|
|
|
driver_job = jobs[1]
|
|
assert driver_job["Entrypoint"] == list2cmdline(commands)
|
|
|
|
# Make sure the Dashboard endpoint works
|
|
r = client._do_request(
|
|
"GET",
|
|
"/api/jobs/",
|
|
)
|
|
|
|
assert r.status_code == 200, r.text
|
|
jobs_info_json = json.loads(r.text)
|
|
jobs_info_json.sort(key=lambda j: j["job_id"])
|
|
info_json = jobs_info_json[1]
|
|
info = JobDetails(**info_json)
|
|
assert info.entrypoint == list2cmdline(commands)
|
|
|
|
"""
|
|
Test job submission
|
|
"""
|
|
client.submit_job(entrypoint=list2cmdline(commands))
|
|
|
|
def verify():
|
|
jobs = ray.state.jobs()
|
|
# Test, first job, agent, submission job
|
|
assert len(jobs) == 4
|
|
jobs = list(jobs)
|
|
jobs.sort(key=lambda j: j["JobID"])
|
|
|
|
# The first job is the test job.
|
|
|
|
submission_job = jobs[3]
|
|
assert submission_job["Entrypoint"] == list2cmdline(commands)
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
# Test client
|
|
# TODO(sang): Client entrypoint not supported yet.
|
|
|
|
|
|
def test_task_spec_root_detached_actor_id(shutdown_only):
|
|
"""Test to make sure root detached actor id is set correctly
|
|
for task spec of submitted task or actor.
|
|
"""
|
|
|
|
ray.init(include_dashboard=True)
|
|
|
|
@ray.remote
|
|
def get_task_root_detached_actor_id():
|
|
core_worker = ray._private.worker.global_worker.core_worker
|
|
return core_worker.get_current_root_detached_actor_id().hex()
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def get_root_detached_actor_id(self):
|
|
core_worker = ray._private.worker.global_worker.core_worker
|
|
return core_worker.get_current_root_detached_actor_id().hex()
|
|
|
|
@ray.remote(lifetime="detached")
|
|
class DetachedActor:
|
|
def check(self):
|
|
core_worker = ray._private.worker.global_worker.core_worker
|
|
assert (
|
|
ray.get_runtime_context().get_actor_id()
|
|
== core_worker.get_current_root_detached_actor_id().hex()
|
|
)
|
|
assert ray.get_runtime_context().get_actor_id() == ray.get(
|
|
get_task_root_detached_actor_id.remote()
|
|
)
|
|
actor = Actor.remote()
|
|
assert ray.get_runtime_context().get_actor_id() == ray.get(
|
|
actor.get_root_detached_actor_id.remote()
|
|
)
|
|
|
|
assert (
|
|
ray.get(get_task_root_detached_actor_id.remote())
|
|
== ray._raylet.ActorID.nil().hex()
|
|
)
|
|
actor = Actor.remote()
|
|
assert (
|
|
ray.get(actor.get_root_detached_actor_id.remote())
|
|
== ray._raylet.ActorID.nil().hex()
|
|
)
|
|
detached_actor = DetachedActor.remote()
|
|
ray.get(detached_actor.check.remote())
|
|
|
|
|
|
def test_no_process_leak_after_job_finishes(ray_start_cluster):
|
|
"""Test to make sure when a job finishes,
|
|
all the worker processes belonging to it exit.
|
|
"""
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=8)
|
|
ray.init(address=cluster.address, include_dashboard=True)
|
|
|
|
@ray.remote(num_cpus=0)
|
|
class PidActor:
|
|
def __init__(self):
|
|
# List (not set): parent and child may run on the same worker PID, but we
|
|
# still get one entry per registration (actor + parent + child == len 3).
|
|
self.pids = [os.getpid()]
|
|
|
|
def add_pid(self, pid):
|
|
self.pids.append(pid)
|
|
|
|
def get_pids(self):
|
|
return self.pids
|
|
|
|
@ray.remote
|
|
def child(pid_actor):
|
|
# child worker process should be forcibly killed
|
|
# when the job finishes.
|
|
ray.get(pid_actor.add_pid.remote(os.getpid()))
|
|
time.sleep(1000000)
|
|
|
|
@ray.remote
|
|
def parent(pid_actor):
|
|
ray.get(pid_actor.add_pid.remote(os.getpid()))
|
|
child.remote(pid_actor)
|
|
|
|
pid_actor = PidActor.remote()
|
|
ray.get(parent.remote(pid_actor))
|
|
|
|
wait_for_condition(lambda: len(ray.get(pid_actor.get_pids.remote())) == 3)
|
|
|
|
pids = ray.get(pid_actor.get_pids.remote())
|
|
|
|
ray.shutdown()
|
|
# Job finishes at this point
|
|
|
|
for pid in set(pids):
|
|
wait_for_pid_to_exit(pid)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
# Make subprocess happy in bazel.
|
|
os.environ["LC_ALL"] = "en_US.UTF-8"
|
|
os.environ["LANG"] = "en_US.UTF-8"
|
|
sys.exit(pytest.main(["-sv", __file__]))
|