chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,599 @@
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray._common.test_utils import SignalActor, wait_for_condition
|
||||
from ray._private.test_utils import (
|
||||
run_string_as_driver_nonblocking,
|
||||
wait_for_pid_to_exit,
|
||||
)
|
||||
|
||||
import psutil
|
||||
|
||||
SIGKILL = signal.SIGKILL if sys.platform != "win32" else signal.SIGTERM
|
||||
|
||||
|
||||
def test_worker_exit_after_parent_raylet_dies(ray_start_cluster):
|
||||
cluster = ray_start_cluster
|
||||
cluster.add_node(num_cpus=0)
|
||||
cluster.add_node(num_cpus=8, resources={"foo": 1})
|
||||
cluster.wait_for_nodes()
|
||||
|
||||
ray.init(address=cluster.address)
|
||||
|
||||
@ray.remote(resources={"foo": 1})
|
||||
class Actor:
|
||||
def get_worker_pid(self):
|
||||
return os.getpid()
|
||||
|
||||
def get_raylet_pid(self):
|
||||
return int(os.environ["RAY_RAYLET_PID"])
|
||||
|
||||
actor = Actor.remote()
|
||||
worker_pid = ray.get(actor.get_worker_pid.remote())
|
||||
raylet_pid = ray.get(actor.get_raylet_pid.remote())
|
||||
# Kill the parent raylet.
|
||||
os.kill(raylet_pid, SIGKILL)
|
||||
os.waitpid(raylet_pid, 0)
|
||||
wait_for_pid_to_exit(raylet_pid)
|
||||
# Make sure the worker process exits as well.
|
||||
wait_for_pid_to_exit(worker_pid)
|
||||
|
||||
|
||||
def test_plasma_store_operation_after_raylet_dies(ray_start_cluster):
|
||||
"""
|
||||
Test that the operation on the plasma store after the raylet dies will not fail the
|
||||
task with an application level error (RayTaskError) but a system level error
|
||||
(RayletDiedError).
|
||||
"""
|
||||
cluster = ray_start_cluster
|
||||
system_configs = {
|
||||
"health_check_initial_delay_ms": 0,
|
||||
"health_check_timeout_ms": 100,
|
||||
"health_check_failure_threshold": 1,
|
||||
}
|
||||
cluster.add_node(
|
||||
num_cpus=1,
|
||||
_system_config=system_configs,
|
||||
)
|
||||
cluster.wait_for_nodes()
|
||||
|
||||
ray.init(address=cluster.address)
|
||||
|
||||
@ray.remote
|
||||
def get_after_raylet_dies():
|
||||
raylet_pid = int(os.environ["RAY_RAYLET_PID"])
|
||||
os.kill(raylet_pid, SIGKILL)
|
||||
wait_for_pid_to_exit(raylet_pid)
|
||||
ray.put([0] * 100000)
|
||||
|
||||
try:
|
||||
ray.get(get_after_raylet_dies.remote(), timeout=10)
|
||||
except Exception as e:
|
||||
assert isinstance(e, ray.exceptions.LocalRayletDiedError)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ray_start_cluster_head",
|
||||
[
|
||||
{
|
||||
"num_cpus": 5,
|
||||
"object_store_memory": 10**8,
|
||||
}
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
def test_parallel_actor_fill_plasma_retry(ray_start_cluster_head):
|
||||
@ray.remote
|
||||
class LargeMemoryActor:
|
||||
def some_expensive_task(self):
|
||||
return np.zeros(10**8 // 2, dtype=np.uint8)
|
||||
|
||||
actors = [LargeMemoryActor.remote() for _ in range(5)]
|
||||
for _ in range(5):
|
||||
pending = [a.some_expensive_task.remote() for a in actors]
|
||||
while pending:
|
||||
[done], pending = ray.wait(pending, num_returns=1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ray_start_regular",
|
||||
[{"_system_config": {"task_retry_delay_ms": 500}}],
|
||||
indirect=True,
|
||||
)
|
||||
def test_async_actor_task_retries(ray_start_regular):
|
||||
# https://github.com/ray-project/ray/issues/11683
|
||||
|
||||
signal = SignalActor.remote()
|
||||
|
||||
@ray.remote
|
||||
class DyingActor:
|
||||
def __init__(self):
|
||||
print("DyingActor init called")
|
||||
self.should_exit = False
|
||||
|
||||
def set_should_exit(self):
|
||||
print("DyingActor.set_should_exit called")
|
||||
self.should_exit = True
|
||||
|
||||
async def get(self, x, wait=False):
|
||||
print(f"DyingActor.get called with x={x}, wait={wait}")
|
||||
if self.should_exit:
|
||||
os._exit(0)
|
||||
if wait:
|
||||
await signal.wait.remote()
|
||||
return x
|
||||
|
||||
# Normal in order actor task retries should work
|
||||
dying = DyingActor.options(
|
||||
max_restarts=-1,
|
||||
max_task_retries=-1,
|
||||
).remote()
|
||||
|
||||
assert ray.get(dying.get.remote(1)) == 1
|
||||
ray.get(dying.set_should_exit.remote())
|
||||
assert ray.get(dying.get.remote(42)) == 42
|
||||
|
||||
# Now let's try out of order retries:
|
||||
# Task seqno 0 will return
|
||||
# Task seqno 1 will be pending and retried later
|
||||
# Task seqno 2 will return
|
||||
# Task seqno 3 will crash the actor and retried later
|
||||
dying = DyingActor.options(
|
||||
max_restarts=-1,
|
||||
max_task_retries=-1,
|
||||
).remote()
|
||||
|
||||
# seqno 0
|
||||
ref_0 = dying.get.remote(0)
|
||||
assert ray.get(ref_0) == 0
|
||||
# seqno 1
|
||||
ref_1 = dying.get.remote(1, wait=True)
|
||||
# Need a barrier here to ensure ordering between the async and sync call.
|
||||
# Otherwise ref2 could be executed prior to ref1.
|
||||
for i in range(100):
|
||||
if ray.get(signal.cur_num_waiters.remote()) > 0:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
assert ray.get(signal.cur_num_waiters.remote()) > 0
|
||||
# seqno 2
|
||||
ref_2 = dying.set_should_exit.remote()
|
||||
assert ray.get(ref_2) is None
|
||||
# seqno 3, this will crash the actor because previous task set should exit
|
||||
# to true.
|
||||
ref_3 = dying.get.remote(3)
|
||||
|
||||
# At this point the actor should be restarted. The two pending tasks
|
||||
# [ref_1, ref_3] should be retried, but not the completed tasks [ref_0,
|
||||
# ref_2]. Critically, if ref_2 was retried, ref_3 can never return.
|
||||
ray.get(signal.send.remote())
|
||||
assert ray.get(ref_1) == 1
|
||||
assert ray.get(ref_3) == 3
|
||||
|
||||
|
||||
def test_actor_failure_async(ray_start_regular):
|
||||
@ray.remote
|
||||
class A:
|
||||
def echo(self):
|
||||
pass
|
||||
|
||||
def pid(self):
|
||||
return os.getpid()
|
||||
|
||||
a = A.remote()
|
||||
rs = []
|
||||
|
||||
def submit():
|
||||
for i in range(10000):
|
||||
r = a.echo.remote()
|
||||
r._on_completed(lambda x: 1)
|
||||
rs.append(r)
|
||||
|
||||
t = threading.Thread(target=submit)
|
||||
pid = ray.get(a.pid.remote())
|
||||
|
||||
t.start()
|
||||
from time import sleep
|
||||
|
||||
sleep(0.1)
|
||||
os.kill(pid, SIGKILL)
|
||||
|
||||
t.join()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ray_start_regular",
|
||||
[{"_system_config": {"timeout_ms_task_wait_for_death_info": 100000000}}],
|
||||
indirect=True,
|
||||
)
|
||||
def test_actor_failure_async_2(ray_start_regular, tmp_path):
|
||||
p = tmp_path / "a_pid"
|
||||
|
||||
@ray.remote(max_restarts=1)
|
||||
class A:
|
||||
def __init__(self):
|
||||
pid = os.getpid()
|
||||
# The second time start, it'll block,
|
||||
# so that we'll know the actor is restarting.
|
||||
if p.exists():
|
||||
p.write_text(str(pid))
|
||||
time.sleep(100000)
|
||||
else:
|
||||
p.write_text(str(pid))
|
||||
|
||||
def pid(self):
|
||||
return os.getpid()
|
||||
|
||||
a = A.remote()
|
||||
|
||||
pid = ray.get(a.pid.remote())
|
||||
|
||||
os.kill(int(pid), SIGKILL)
|
||||
|
||||
# kill will be in another thred.
|
||||
def kill():
|
||||
# sleep for 2s for the code to be setup
|
||||
time.sleep(2)
|
||||
new_pid = int(p.read_text())
|
||||
while new_pid == pid:
|
||||
new_pid = int(p.read_text())
|
||||
time.sleep(1)
|
||||
os.kill(new_pid, SIGKILL)
|
||||
|
||||
t = threading.Thread(target=kill)
|
||||
t.start()
|
||||
|
||||
try:
|
||||
o = a.pid.remote()
|
||||
|
||||
def new_task(_):
|
||||
print("new_task")
|
||||
# make sure there is no deadlock
|
||||
a.pid.remote()
|
||||
|
||||
o._on_completed(new_task)
|
||||
# When ray.get(o) failed,
|
||||
# new_task will be executed
|
||||
ray.get(o)
|
||||
except Exception:
|
||||
pass
|
||||
t.join()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ray_start_regular",
|
||||
[{"_system_config": {"timeout_ms_task_wait_for_death_info": 100000000}}],
|
||||
indirect=True,
|
||||
)
|
||||
def test_actor_failure_async_3(ray_start_regular):
|
||||
@ray.remote(max_restarts=1)
|
||||
class A:
|
||||
def pid(self):
|
||||
return os.getpid()
|
||||
|
||||
a = A.remote()
|
||||
|
||||
def new_task(_):
|
||||
print("new_task")
|
||||
# make sure there is no deadlock
|
||||
a.pid.remote()
|
||||
|
||||
t = a.pid.remote()
|
||||
# Make sure there is no deadlock when executing
|
||||
# the callback
|
||||
t._on_completed(new_task)
|
||||
|
||||
ray.kill(a)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
ray.get(t)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ray_start_regular",
|
||||
[{"_system_config": {"timeout_ms_task_wait_for_death_info": 100000000}}],
|
||||
indirect=True,
|
||||
)
|
||||
def test_actor_failure_async_4(ray_start_regular, tmp_path):
|
||||
from filelock import FileLock
|
||||
|
||||
l_file = tmp_path / "lock"
|
||||
|
||||
l_lock = FileLock(l_file)
|
||||
l_lock.acquire()
|
||||
|
||||
@ray.remote
|
||||
def f():
|
||||
with FileLock(l_file):
|
||||
os.kill(os.getpid(), SIGKILL)
|
||||
|
||||
@ray.remote(max_restarts=1)
|
||||
class A:
|
||||
def pid(self, x):
|
||||
return os.getpid()
|
||||
|
||||
a = A.remote()
|
||||
|
||||
def new_task(_):
|
||||
print("new_task")
|
||||
# make sure there is no deadlock
|
||||
a.pid.remote(None)
|
||||
|
||||
t = a.pid.remote(f.remote())
|
||||
# Make sure there is no deadlock when executing
|
||||
# the callback
|
||||
t._on_completed(new_task)
|
||||
|
||||
ray.kill(a)
|
||||
|
||||
# This will make the dependence failed
|
||||
l_lock.release()
|
||||
|
||||
with pytest.raises(Exception):
|
||||
ray.get(t)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ray_start_regular",
|
||||
[
|
||||
{
|
||||
"_system_config": {
|
||||
"timeout_ms_task_wait_for_death_info": 0,
|
||||
"core_worker_internal_heartbeat_ms": 1000000,
|
||||
}
|
||||
}
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
def test_actor_failure_no_wait(ray_start_regular, tmp_path):
|
||||
p = tmp_path / "a_pid"
|
||||
time.sleep(1)
|
||||
|
||||
# Make sure the request will fail immediately without waiting for the death info
|
||||
@ray.remote(max_restarts=1, max_task_retries=0)
|
||||
class A:
|
||||
def __init__(self):
|
||||
pid = os.getpid()
|
||||
# The second time start, it'll block,
|
||||
# so that we'll know the actor is restarting.
|
||||
if p.exists():
|
||||
p.write_text(str(pid))
|
||||
time.sleep(100000)
|
||||
else:
|
||||
p.write_text(str(pid))
|
||||
|
||||
def p(self):
|
||||
time.sleep(100000)
|
||||
|
||||
def pid(self):
|
||||
return os.getpid()
|
||||
|
||||
a = A.remote()
|
||||
pid = ray.get(a.pid.remote())
|
||||
t = a.p.remote()
|
||||
os.kill(int(pid), SIGKILL)
|
||||
with pytest.raises(ray.exceptions.RayActorError):
|
||||
# Make sure it'll return within 1s
|
||||
ray.get(t)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "linux", reason="Only works on linux.")
|
||||
def test_no_worker_child_process_leaks(ray_start_cluster, tmp_path):
|
||||
"""
|
||||
Verify that processes created by Ray tasks and actors are
|
||||
cleaned up after a Ctrl+C is sent to the driver. This is done by
|
||||
creating an actor and task that each spawn a number of child
|
||||
processes, sending a SIGINT to the driver process, and
|
||||
verifying that all child processes are killed.
|
||||
|
||||
The driver script uses a temporary JSON file to communicate
|
||||
the list of PIDs that are children of the Ray worker
|
||||
processes.
|
||||
"""
|
||||
|
||||
output_file_path = tmp_path / "leaked_pids.json"
|
||||
ray_start_cluster.add_node()
|
||||
driver_script = f"""
|
||||
import ray
|
||||
import json
|
||||
import multiprocessing
|
||||
import shutil
|
||||
import time
|
||||
import os
|
||||
|
||||
@ray.remote
|
||||
class Actor:
|
||||
def create_leaked_child_process(self, num_to_leak):
|
||||
print("Creating leaked process", os.getpid())
|
||||
|
||||
pids = []
|
||||
for _ in range(num_to_leak):
|
||||
proc = multiprocessing.Process(
|
||||
target=time.sleep,
|
||||
args=(1000,),
|
||||
daemon=True,
|
||||
)
|
||||
proc.start()
|
||||
pids.append(proc.pid)
|
||||
|
||||
return pids
|
||||
|
||||
@ray.remote
|
||||
def task():
|
||||
print("Creating leaked process", os.getpid())
|
||||
proc = multiprocessing.Process(
|
||||
target=time.sleep,
|
||||
args=(1000,),
|
||||
daemon=True,
|
||||
)
|
||||
proc.start()
|
||||
|
||||
return proc.pid
|
||||
|
||||
num_to_leak_per_type = 10
|
||||
|
||||
actor = Actor.remote()
|
||||
actor_leaked_pids = ray.get(actor.create_leaked_child_process.remote(
|
||||
num_to_leak=num_to_leak_per_type,
|
||||
))
|
||||
|
||||
task_leaked_pids = ray.get([task.remote() for _ in range(num_to_leak_per_type)])
|
||||
leaked_pids = actor_leaked_pids + task_leaked_pids
|
||||
|
||||
final_file = "{output_file_path}"
|
||||
tmp_file = final_file + ".tmp"
|
||||
with open(tmp_file, "w") as f:
|
||||
json.dump(leaked_pids, f)
|
||||
shutil.move(tmp_file, final_file)
|
||||
|
||||
while True:
|
||||
print(os.getpid())
|
||||
time.sleep(1)
|
||||
"""
|
||||
|
||||
driver_proc = run_string_as_driver_nonblocking(driver_script)
|
||||
|
||||
# Wait for the json file containing the child PIDS
|
||||
# to be present.
|
||||
wait_for_condition(
|
||||
condition_predictor=lambda: Path(output_file_path).exists(),
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
# Load the PIDs of the child processes.
|
||||
with open(output_file_path, "r") as f:
|
||||
pids = json.load(f)
|
||||
|
||||
# Validate all children of the worker processes are in a sleeping state.
|
||||
processes = [psutil.Process(pid) for pid in pids]
|
||||
assert all([proc.status() == psutil.STATUS_SLEEPING for proc in processes])
|
||||
|
||||
# Valdiate children of worker process die after SIGINT.
|
||||
driver_proc.send_signal(signal.SIGINT)
|
||||
wait_for_condition(
|
||||
condition_predictor=lambda: all([not proc.is_running() for proc in processes]),
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "linux", reason="Only works on linux.")
|
||||
def test_worker_cleans_up_child_procs_on_raylet_death(ray_start_cluster, tmp_path):
|
||||
"""
|
||||
CoreWorker kills its child processes if the raylet dies.
|
||||
This test creates 20 leaked processes; 10 from a single actor task, and
|
||||
10 from distinct non-actor tasks.
|
||||
|
||||
Once the raylet dies, the test verifies all leaked processes are cleaned up.
|
||||
"""
|
||||
|
||||
output_file_path = tmp_path / "leaked_pids.json"
|
||||
ray_start_cluster.add_node()
|
||||
driver_script = f"""
|
||||
import ray
|
||||
import json
|
||||
import multiprocessing
|
||||
import shutil
|
||||
import time
|
||||
import os
|
||||
|
||||
def change_name_and_sleep(label: str, index: int) -> None:
|
||||
proctitle = "child_proc_name_prefix_" + label + "_" + str(index)
|
||||
ray._raylet.setproctitle(proctitle)
|
||||
time.sleep(1000)
|
||||
|
||||
def create_child_proc(label, index):
|
||||
proc = multiprocessing.Process(
|
||||
target=change_name_and_sleep,
|
||||
args=(label, index,),
|
||||
daemon=True,
|
||||
)
|
||||
proc.start()
|
||||
return proc.pid
|
||||
|
||||
@ray.remote
|
||||
class LeakerActor:
|
||||
def create_leaked_child_process(self, num_to_leak):
|
||||
print("creating leaked process", os.getpid())
|
||||
|
||||
pids = []
|
||||
for index in range(num_to_leak):
|
||||
pid = create_child_proc("actor", index)
|
||||
pids.append(pid)
|
||||
|
||||
return pids
|
||||
|
||||
@ray.remote
|
||||
def leaker_task(index):
|
||||
print("Creating leaked process", os.getpid())
|
||||
return create_child_proc("task", index)
|
||||
|
||||
num_to_leak_per_type = 10
|
||||
print('starting actors')
|
||||
actor = LeakerActor.remote()
|
||||
actor_leaked_pids = ray.get(actor.create_leaked_child_process.remote(
|
||||
num_to_leak=num_to_leak_per_type,
|
||||
))
|
||||
|
||||
task_leaked_pids = ray.get([
|
||||
leaker_task.remote(index) for index in range(num_to_leak_per_type)
|
||||
])
|
||||
leaked_pids = actor_leaked_pids + task_leaked_pids
|
||||
|
||||
final_file = "{output_file_path}"
|
||||
tmp_file = final_file + ".tmp"
|
||||
with open(tmp_file, "w") as f:
|
||||
json.dump(leaked_pids, f)
|
||||
shutil.move(tmp_file, final_file)
|
||||
|
||||
while True:
|
||||
print(os.getpid())
|
||||
time.sleep(1)
|
||||
"""
|
||||
|
||||
print("Running string as driver")
|
||||
driver_proc = run_string_as_driver_nonblocking(driver_script)
|
||||
|
||||
# Wait for the json file containing the child PIDS
|
||||
# to be present.
|
||||
print("Waiting for child pids json")
|
||||
wait_for_condition(
|
||||
condition_predictor=lambda: Path(output_file_path).exists(),
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
# Load the PIDs of the child processes.
|
||||
with open(output_file_path, "r") as f:
|
||||
pids = json.load(f)
|
||||
|
||||
# Validate all children of the worker processes are in a sleeping state.
|
||||
processes = [psutil.Process(pid) for pid in pids]
|
||||
assert all([proc.status() == psutil.STATUS_SLEEPING for proc in processes])
|
||||
|
||||
# Obtain psutil handle for raylet process
|
||||
raylet_proc = [p for p in psutil.process_iter() if p.name() == "raylet"]
|
||||
assert len(raylet_proc) == 1
|
||||
raylet_proc = raylet_proc[0]
|
||||
|
||||
# Kill the raylet process and reap the zombie
|
||||
raylet_proc.kill()
|
||||
raylet_proc.wait()
|
||||
|
||||
print("Waiting for child procs to die")
|
||||
wait_for_condition(
|
||||
condition_predictor=lambda: all([not proc.is_running() for proc in processes]),
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
driver_proc.kill()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main(["-sv", __file__]))
|
||||
Reference in New Issue
Block a user