1298 lines
39 KiB
Python
1298 lines
39 KiB
Python
import os
|
|
import sys
|
|
import time
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from typing import Optional
|
|
|
|
import pytest
|
|
|
|
import ray
|
|
import ray._private.gcs_utils as gcs_utils
|
|
import ray.cluster_utils
|
|
from ray._common.test_utils import (
|
|
SignalActor,
|
|
run_string_as_driver,
|
|
wait_for_condition,
|
|
)
|
|
from ray._private.test_utils import (
|
|
kill_actor_and_wait_for_failure,
|
|
make_global_state_accessor,
|
|
wait_for_pid_to_exit,
|
|
)
|
|
from ray.experimental.internal_kv import _internal_kv_get, _internal_kv_put
|
|
from ray.util.state import list_actors
|
|
|
|
|
|
def test_actors_on_nodes_with_no_cpus(ray_start_no_cpu):
|
|
@ray.remote
|
|
class Foo:
|
|
def method(self):
|
|
pass
|
|
|
|
f = Foo.remote()
|
|
ready_ids, _ = ray.wait([f.method.remote()], timeout=0.1)
|
|
assert ready_ids == []
|
|
|
|
|
|
def test_actor_load_balancing(ray_start_cluster):
|
|
"""Check that actor scheduling is load balanced across worker nodes."""
|
|
cluster = ray_start_cluster
|
|
worker_node_ids = set()
|
|
for i in range(2):
|
|
worker_node_ids.add(cluster.add_node(num_cpus=1).node_id)
|
|
|
|
ray.init(address=cluster.address)
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def get_node_id(self) -> str:
|
|
return ray.get_runtime_context().get_node_id()
|
|
|
|
# Schedule a group of actors, ensure that the actors are spread between all nodes.
|
|
node_ids = ray.get([Actor.remote().get_node_id.remote() for _ in range(10)])
|
|
assert set(node_ids) == worker_node_ids
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_regular",
|
|
[
|
|
{
|
|
"resources": {"actor": 1},
|
|
"num_cpus": 2,
|
|
}
|
|
],
|
|
indirect=True,
|
|
)
|
|
def test_deleted_actor_no_restart(ray_start_regular):
|
|
@ray.remote(resources={"actor": 1}, max_restarts=3)
|
|
class Actor:
|
|
def method(self):
|
|
return 1
|
|
|
|
def getpid(self):
|
|
return os.getpid()
|
|
|
|
@ray.remote
|
|
def f(actor, signal):
|
|
ray.get(signal.wait.remote())
|
|
return ray.get(actor.method.remote())
|
|
|
|
signal = SignalActor.remote()
|
|
a = Actor.remote()
|
|
pid = ray.get(a.getpid.remote())
|
|
# Pass the handle to another task that cannot run yet.
|
|
x_id = f.remote(a, signal)
|
|
# Delete the original handle. The actor should not get killed yet.
|
|
del a
|
|
|
|
# Once the task finishes, the actor process should get killed.
|
|
ray.get(signal.send.remote())
|
|
assert ray.get(x_id) == 1
|
|
wait_for_pid_to_exit(pid)
|
|
|
|
# Create another actor with the same resource requirement to make sure the
|
|
# old one was not restarted.
|
|
a = Actor.remote()
|
|
pid = ray.get(a.getpid.remote())
|
|
|
|
|
|
def test_exception_raised_when_actor_node_dies(ray_start_cluster_head):
|
|
cluster = ray_start_cluster_head
|
|
remote_node = cluster.add_node()
|
|
|
|
@ray.remote(max_restarts=0, scheduling_strategy="SPREAD")
|
|
class Counter:
|
|
def __init__(self):
|
|
self.x = 0
|
|
|
|
def node_id(self):
|
|
return ray._private.worker.global_worker.node.unique_id
|
|
|
|
def inc(self):
|
|
self.x += 1
|
|
return self.x
|
|
|
|
# Create an actor that is not on the raylet.
|
|
actor = Counter.remote()
|
|
while ray.get(actor.node_id.remote()) != remote_node.unique_id:
|
|
actor = Counter.remote()
|
|
|
|
# Kill the second node.
|
|
cluster.remove_node(remote_node)
|
|
|
|
# Submit some new actor tasks both before and after the node failure is
|
|
# detected. Make sure that getting the result raises an exception.
|
|
for _ in range(10):
|
|
# Submit some new actor tasks.
|
|
x_ids = [actor.inc.remote() for _ in range(5)]
|
|
for x_id in x_ids:
|
|
with pytest.raises(ray.exceptions.RayActorError):
|
|
# There is some small chance that ray.get will actually
|
|
# succeed (if the object is transferred before the raylet
|
|
# dies).
|
|
ray.get(x_id)
|
|
|
|
|
|
def test_actor_fail_during_constructor_restart(ray_start_cluster_head):
|
|
cluster = ray_start_cluster_head
|
|
worker_nodes = {
|
|
node.node_id: node for node in [cluster.add_node() for _ in range(2)]
|
|
}
|
|
|
|
@ray.remote
|
|
class ReportNodeIDActor:
|
|
def __init__(self):
|
|
self._reported_node_id = None
|
|
|
|
def report(self, node_id: str):
|
|
self._reported_node_id = node_id
|
|
|
|
def get(self) -> Optional[str]:
|
|
return self._reported_node_id
|
|
|
|
# Pin these actors to the head node so they don't crash.
|
|
# Occupy the 1 CPU on the head node so the actor below is forced to a worker node.
|
|
pin_head_resources = {"node:__internal_head__": 0.1}
|
|
report_node_id_actor = ReportNodeIDActor.options(
|
|
num_cpus=0.5, resources=pin_head_resources
|
|
).remote()
|
|
signal = SignalActor.options(
|
|
num_cpus=0.5,
|
|
resources=pin_head_resources,
|
|
).remote()
|
|
|
|
@ray.remote(max_restarts=1, max_task_retries=-1)
|
|
class Actor:
|
|
def __init__(self):
|
|
ray.get(
|
|
report_node_id_actor.report.remote(
|
|
ray.get_runtime_context().get_node_id()
|
|
)
|
|
)
|
|
ray.get(signal.wait.remote())
|
|
|
|
# Create the actor and wait for it to start initializing.
|
|
actor = Actor.remote()
|
|
wait_for_condition(lambda: ray.get(signal.cur_num_waiters.remote()) == 1)
|
|
actor_node_id = ray.get(report_node_id_actor.get.remote())
|
|
assert actor_node_id is not None
|
|
|
|
# Kill the worker node.
|
|
cluster.remove_node(worker_nodes[actor_node_id])
|
|
|
|
# Verify that the actor was restarted on the other node.
|
|
ray.get(signal.send.remote())
|
|
ray.get(actor.__ray_ready__.remote())
|
|
assert ray.get(report_node_id_actor.get.remote()) != actor_node_id
|
|
|
|
|
|
def test_actor_restart_multiple_callers(ray_start_cluster):
|
|
cluster = ray_start_cluster
|
|
_ = cluster.add_node(num_cpus=4)
|
|
ray.init(address=cluster.address)
|
|
|
|
_ = cluster.add_node(num_cpus=4)
|
|
actor_worker_node = cluster.add_node(num_cpus=0, resources={"actor": 1})
|
|
cluster.wait_for_nodes()
|
|
|
|
@ray.remote(
|
|
num_cpus=0,
|
|
# Only one of the callers should successfully restart the actor.
|
|
max_restarts=1,
|
|
# Retry transient ActorUnavailableErrors.
|
|
max_task_retries=-1,
|
|
# Schedule the actor on actor_worker_node.
|
|
resources={"actor": 1},
|
|
)
|
|
class A:
|
|
def get_node_id(self) -> str:
|
|
return ray.get_runtime_context().get_node_id()
|
|
|
|
a = A.remote()
|
|
|
|
@ray.remote
|
|
def call_a() -> str:
|
|
return ray.get(a.get_node_id.remote())
|
|
|
|
# Run caller tasks in parallel across the other two nodes.
|
|
results = ray.get([call_a.remote() for _ in range(8)])
|
|
assert all(r == actor_worker_node.node_id for r in results), results
|
|
|
|
# Kill the node that the actor is running on.
|
|
cluster.remove_node(actor_worker_node)
|
|
|
|
# Run caller tasks in parallel again.
|
|
refs = [call_a.remote() for _ in range(8)]
|
|
ready, _ = ray.wait(refs, timeout=0.1)
|
|
assert len(ready) == 0
|
|
|
|
# The actor should be restarted once the node becomes available.
|
|
new_actor_worker_node = cluster.add_node(num_cpus=0, resources={"actor": 1})
|
|
results = ray.get(refs)
|
|
assert all(r == new_actor_worker_node.node_id for r in results), results
|
|
|
|
|
|
@pytest.fixture
|
|
def setup_queue_actor():
|
|
ray.init(num_cpus=1, object_store_memory=int(150 * 1024 * 1024))
|
|
|
|
@ray.remote
|
|
class Queue:
|
|
def __init__(self):
|
|
self.queue = []
|
|
|
|
def enqueue(self, key, item):
|
|
self.queue.append((key, item))
|
|
|
|
def read(self):
|
|
return self.queue
|
|
|
|
queue = Queue.remote()
|
|
# Make sure queue actor is initialized.
|
|
ray.get(queue.read.remote())
|
|
|
|
yield queue
|
|
|
|
# The code after the yield will run as teardown code.
|
|
ray.shutdown()
|
|
|
|
|
|
def test_fork(setup_queue_actor):
|
|
queue = setup_queue_actor
|
|
|
|
@ray.remote
|
|
def fork(queue, key, item):
|
|
# ray.get here could be blocked and cause ray to start
|
|
# a lot of python workers.
|
|
return ray.get(queue.enqueue.remote(key, item))
|
|
|
|
# Fork num_iters times.
|
|
num_iters = 100
|
|
ray.get([fork.remote(queue, i, 0) for i in range(num_iters)])
|
|
items = ray.get(queue.read.remote())
|
|
for i in range(num_iters):
|
|
filtered_items = [item[1] for item in items if item[0] == i]
|
|
assert filtered_items == list(range(1))
|
|
|
|
|
|
def test_fork_consistency(setup_queue_actor):
|
|
queue = setup_queue_actor
|
|
|
|
@ray.remote
|
|
def fork(queue, key, num_items):
|
|
x = None
|
|
for item in range(num_items):
|
|
x = queue.enqueue.remote(key, item)
|
|
return ray.get(x)
|
|
|
|
# Fork num_iters times.
|
|
num_forks = 5
|
|
num_items_per_fork = 100
|
|
|
|
# Submit some tasks on new actor handles.
|
|
forks = [fork.remote(queue, i, num_items_per_fork) for i in range(num_forks)]
|
|
# Submit some more tasks on the original actor handle.
|
|
for item in range(num_items_per_fork):
|
|
local_fork = queue.enqueue.remote(num_forks, item)
|
|
forks.append(local_fork)
|
|
# Wait for tasks from all handles to complete.
|
|
ray.get(forks)
|
|
# Check that all tasks from all handles have completed.
|
|
items = ray.get(queue.read.remote())
|
|
for i in range(num_forks + 1):
|
|
filtered_items = [item[1] for item in items if item[0] == i]
|
|
assert filtered_items == list(range(num_items_per_fork))
|
|
|
|
|
|
def test_pickled_handle_consistency(setup_queue_actor):
|
|
queue = setup_queue_actor
|
|
|
|
@ray.remote
|
|
def fork(pickled_queue, key, num_items):
|
|
queue = ray._private.worker.pickle.loads(pickled_queue)
|
|
x = None
|
|
for item in range(num_items):
|
|
x = queue.enqueue.remote(key, item)
|
|
return ray.get(x)
|
|
|
|
# Fork num_iters times.
|
|
num_forks = 10
|
|
num_items_per_fork = 100
|
|
|
|
# Submit some tasks on the pickled actor handle.
|
|
new_queue = ray._private.worker.pickle.dumps(queue)
|
|
forks = [fork.remote(new_queue, i, num_items_per_fork) for i in range(num_forks)]
|
|
# Submit some more tasks on the original actor handle.
|
|
for item in range(num_items_per_fork):
|
|
local_fork = queue.enqueue.remote(num_forks, item)
|
|
forks.append(local_fork)
|
|
# Wait for tasks from all handles to complete.
|
|
ray.get(forks)
|
|
# Check that all tasks from all handles have completed.
|
|
items = ray.get(queue.read.remote())
|
|
for i in range(num_forks + 1):
|
|
filtered_items = [item[1] for item in items if item[0] == i]
|
|
assert filtered_items == list(range(num_items_per_fork))
|
|
|
|
|
|
def test_nested_fork(setup_queue_actor):
|
|
queue = setup_queue_actor
|
|
|
|
@ray.remote
|
|
def fork(queue, key, num_items):
|
|
x = None
|
|
for item in range(num_items):
|
|
x = queue.enqueue.remote(key, item)
|
|
return ray.get(x)
|
|
|
|
@ray.remote
|
|
def nested_fork(queue, key, num_items):
|
|
# Pass the actor into a nested task.
|
|
ray.get(fork.remote(queue, key + 1, num_items))
|
|
x = None
|
|
for item in range(num_items):
|
|
x = queue.enqueue.remote(key, item)
|
|
return ray.get(x)
|
|
|
|
# Fork num_iters times.
|
|
num_forks = 10
|
|
num_items_per_fork = 100
|
|
|
|
# Submit some tasks on new actor handles.
|
|
forks = [
|
|
nested_fork.remote(queue, i, num_items_per_fork) for i in range(0, num_forks, 2)
|
|
]
|
|
ray.get(forks)
|
|
# Check that all tasks from all handles have completed.
|
|
items = ray.get(queue.read.remote())
|
|
for i in range(num_forks):
|
|
filtered_items = [item[1] for item in items if item[0] == i]
|
|
assert filtered_items == list(range(num_items_per_fork))
|
|
|
|
|
|
def test_calling_put_on_actor_handle(ray_start_regular):
|
|
@ray.remote
|
|
class Counter:
|
|
def __init__(self):
|
|
self.x = 0
|
|
|
|
def inc(self):
|
|
self.x += 1
|
|
return self.x
|
|
|
|
@ray.remote
|
|
def f():
|
|
return Counter.remote()
|
|
|
|
# Currently, calling ray.put on an actor handle is allowed, but is
|
|
# there a good use case?
|
|
counter = Counter.remote()
|
|
counter_id = ray.put(counter)
|
|
new_counter = ray.get(counter_id)
|
|
assert ray.get(new_counter.inc.remote()) == 1
|
|
assert ray.get(counter.inc.remote()) == 2
|
|
assert ray.get(new_counter.inc.remote()) == 3
|
|
|
|
ray.get(f.remote())
|
|
|
|
|
|
def test_named_but_not_detached(ray_start_regular):
|
|
address = ray_start_regular["address"]
|
|
|
|
driver_script = """
|
|
import ray
|
|
ray.init(address="{}")
|
|
|
|
@ray.remote
|
|
class NotDetached:
|
|
def ping(self):
|
|
return "pong"
|
|
|
|
actor = NotDetached.options(name="actor").remote()
|
|
assert ray.get(actor.ping.remote()) == "pong"
|
|
handle = ray.get_actor("actor")
|
|
assert ray.util.list_named_actors() == ["actor"]
|
|
assert ray.get(handle.ping.remote()) == "pong"
|
|
""".format(
|
|
address
|
|
)
|
|
|
|
# Creates and kills actor once the driver exits.
|
|
run_string_as_driver(driver_script)
|
|
|
|
# Must raise an exception since lifetime is not detached.
|
|
with pytest.raises(Exception):
|
|
assert not ray.util.list_named_actors()
|
|
detached_actor = ray.get_actor("actor")
|
|
ray.get(detached_actor.ping.remote())
|
|
|
|
# Check that the names are reclaimed after actors die.
|
|
|
|
def check_name_available(name):
|
|
try:
|
|
ray.get_actor(name)
|
|
return False
|
|
except ValueError:
|
|
return True
|
|
|
|
@ray.remote
|
|
class A:
|
|
pass
|
|
|
|
a = A.options(name="my_actor_1").remote()
|
|
ray.kill(a, no_restart=True)
|
|
wait_for_condition(lambda: check_name_available("my_actor_1"))
|
|
|
|
b = A.options(name="my_actor_2").remote()
|
|
del b
|
|
wait_for_condition(lambda: check_name_available("my_actor_2"))
|
|
|
|
|
|
def test_detached_actor(ray_start_regular):
|
|
@ray.remote
|
|
class DetachedActor:
|
|
def ping(self):
|
|
return "pong"
|
|
|
|
with pytest.raises(TypeError):
|
|
DetachedActor._remote(lifetime="detached", name=1)
|
|
|
|
with pytest.raises(ValueError, match="Actor name cannot be an empty string"):
|
|
DetachedActor._remote(lifetime="detached", name="")
|
|
|
|
with pytest.raises(ValueError):
|
|
DetachedActor._remote(lifetime="detached", name="hi", namespace="")
|
|
|
|
with pytest.raises(TypeError):
|
|
DetachedActor._remote(lifetime="detached", name="hi", namespace=2)
|
|
|
|
d = DetachedActor._remote(lifetime="detached", name="d_actor")
|
|
assert ray.get(d.ping.remote()) == "pong"
|
|
|
|
with pytest.raises(ValueError, match="Please use a different name"):
|
|
DetachedActor._remote(lifetime="detached", name="d_actor")
|
|
|
|
address = ray_start_regular["address"]
|
|
|
|
get_actor_name = "d_actor"
|
|
create_actor_name = "DetachedActor"
|
|
driver_script = """
|
|
import ray
|
|
ray.init(address="{}", namespace="default_test_namespace")
|
|
|
|
name = "{}"
|
|
assert ray.util.list_named_actors() == [name]
|
|
existing_actor = ray.get_actor(name)
|
|
assert ray.get(existing_actor.ping.remote()) == "pong"
|
|
|
|
@ray.remote
|
|
def foo():
|
|
return "bar"
|
|
|
|
@ray.remote
|
|
class NonDetachedActor:
|
|
def foo(self):
|
|
return "bar"
|
|
|
|
@ray.remote
|
|
class DetachedActor:
|
|
def ping(self):
|
|
return "pong"
|
|
|
|
def foobar(self):
|
|
actor = NonDetachedActor.remote()
|
|
return ray.get([foo.remote(), actor.foo.remote()])
|
|
|
|
actor = DetachedActor._remote(lifetime="detached", name="{}")
|
|
ray.get(actor.ping.remote())
|
|
""".format(
|
|
address, get_actor_name, create_actor_name
|
|
)
|
|
|
|
run_string_as_driver(driver_script)
|
|
assert len(ray.util.list_named_actors()) == 2
|
|
assert get_actor_name in ray.util.list_named_actors()
|
|
assert create_actor_name in ray.util.list_named_actors()
|
|
detached_actor = ray.get_actor(create_actor_name)
|
|
assert ray.get(detached_actor.ping.remote()) == "pong"
|
|
# Verify that a detached actor is able to create tasks/actors
|
|
# even if the driver of the detached actor has exited.
|
|
assert ray.get(detached_actor.foobar.remote()) == ["bar", "bar"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_regular",
|
|
[{"include_dashboard": True}],
|
|
indirect=True,
|
|
)
|
|
def test_detached_actor_cleanup(ray_start_regular):
|
|
@ray.remote
|
|
class DetachedActor:
|
|
def ping(self):
|
|
return "pong"
|
|
|
|
dup_actor_name = "actor"
|
|
|
|
def create_and_kill_actor(actor_name):
|
|
# Make sure same name is creatable after killing it.
|
|
detached_actor = DetachedActor.options(
|
|
lifetime="detached", name=actor_name
|
|
).remote()
|
|
# Wait for detached actor creation.
|
|
assert ray.get(detached_actor.ping.remote()) == "pong"
|
|
del detached_actor
|
|
assert ray.util.list_named_actors() == [dup_actor_name]
|
|
detached_actor = ray.get_actor(dup_actor_name)
|
|
ray.kill(detached_actor)
|
|
# Wait until actor dies.
|
|
actor_status = ray.util.state.get_actor(id=detached_actor._actor_id.hex())
|
|
max_wait_time = 10
|
|
wait_time = 0
|
|
while actor_status.state != "DEAD":
|
|
actor_status = ray.util.state.get_actor(id=detached_actor._actor_id.hex())
|
|
print(f"actor status is {actor_status}")
|
|
time.sleep(1.0)
|
|
wait_time += 1
|
|
if wait_time >= max_wait_time:
|
|
assert None, "It took too much time to kill an actor: {}".format(
|
|
detached_actor._actor_id
|
|
)
|
|
|
|
create_and_kill_actor(dup_actor_name)
|
|
|
|
# This shouldn't be broken because actor
|
|
# name should have been cleaned up from GCS.
|
|
create_and_kill_actor(dup_actor_name)
|
|
|
|
address = ray_start_regular["address"]
|
|
driver_script = """
|
|
import ray
|
|
import ray._private.gcs_utils as gcs_utils
|
|
import time
|
|
from ray._private.test_utils import convert_actor_state
|
|
import traceback
|
|
|
|
|
|
try:
|
|
|
|
def _load_state_api():
|
|
try:
|
|
from ray.util import state as state_api
|
|
return state_api
|
|
except Exception:
|
|
pass
|
|
|
|
raise ImportError("No usable Ray State API found")
|
|
|
|
ray.init(address="{}", namespace="default_test_namespace")
|
|
|
|
@ray.remote
|
|
class DetachedActor:
|
|
def ping(self):
|
|
return "pong"
|
|
|
|
# Make sure same name is creatable after killing it.
|
|
detached_actor = DetachedActor.options(lifetime="detached", name="{}").remote()
|
|
assert ray.get(detached_actor.ping.remote()) == "pong"
|
|
ray.kill(detached_actor)
|
|
# Wait until actor dies.
|
|
actor_status = _load_state_api().get_actor(id=detached_actor._actor_id.hex())
|
|
max_wait_time = 10
|
|
wait_time = 0
|
|
while actor_status.state != "DEAD": # noqa
|
|
actor_status = _load_state_api().get_actor(id=detached_actor._actor_id.hex())
|
|
time.sleep(1.0)
|
|
wait_time += 1
|
|
if wait_time >= max_wait_time:
|
|
assert None, (
|
|
"It took too much time to kill an actor")
|
|
except Exception:
|
|
traceback.print_exc()
|
|
raise
|
|
""".format(
|
|
address, dup_actor_name
|
|
)
|
|
|
|
run_string_as_driver(driver_script)
|
|
# Make sure we can create a detached actor created/killed
|
|
# at other scripts.
|
|
create_and_kill_actor(dup_actor_name)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_cluster",
|
|
[
|
|
{
|
|
"num_cpus": 3,
|
|
"num_nodes": 1,
|
|
"resources": {"first_node": 5},
|
|
"include_dashboard": True,
|
|
}
|
|
],
|
|
indirect=True,
|
|
)
|
|
def test_detached_actor_cleanup_due_to_failure(ray_start_cluster):
|
|
cluster = ray_start_cluster
|
|
node = cluster.add_node(resources={"second_node": 1})
|
|
cluster.wait_for_nodes()
|
|
|
|
@ray.remote
|
|
class DetachedActor:
|
|
def ping(self):
|
|
return "pong"
|
|
|
|
def kill_itself(self):
|
|
# kill itself.
|
|
os._exit(0)
|
|
|
|
worker_failure_actor_name = "worker_failure_actor_name"
|
|
node_failure_actor_name = "node_failure_actor_name"
|
|
|
|
def wait_until_actor_dead(handle):
|
|
actor_status = ray.util.state.get_actor(id=handle._actor_id.hex())
|
|
max_wait_time = 10
|
|
wait_time = 0
|
|
while actor_status.state != "DEAD":
|
|
actor_status = ray.util.state.get_actor(id=handle._actor_id.hex())
|
|
time.sleep(1.0)
|
|
wait_time += 1
|
|
if wait_time >= max_wait_time:
|
|
assert None, "It took too much time to kill an actor: {}".format(
|
|
handle._actor_id
|
|
)
|
|
|
|
def create_detached_actor_blocking(actor_name, schedule_in_second_node=False):
|
|
resources = {"second_node": 1} if schedule_in_second_node else {"first_node": 1}
|
|
actor_handle = DetachedActor.options(
|
|
lifetime="detached", name=actor_name, resources=resources
|
|
).remote()
|
|
# Wait for detached actor creation.
|
|
assert ray.get(actor_handle.ping.remote()) == "pong"
|
|
return actor_handle
|
|
|
|
# Name should be cleaned when workers fail
|
|
deatched_actor = create_detached_actor_blocking(worker_failure_actor_name)
|
|
deatched_actor.kill_itself.remote()
|
|
wait_until_actor_dead(deatched_actor)
|
|
# Name should be available now.
|
|
deatched_actor = create_detached_actor_blocking(worker_failure_actor_name)
|
|
assert ray.get(deatched_actor.ping.remote()) == "pong"
|
|
|
|
# Name should be cleaned when nodes fail.
|
|
deatched_actor = create_detached_actor_blocking(
|
|
node_failure_actor_name, schedule_in_second_node=True
|
|
)
|
|
cluster.remove_node(node)
|
|
wait_until_actor_dead(deatched_actor)
|
|
# Name should be available now.
|
|
deatched_actor = create_detached_actor_blocking(node_failure_actor_name)
|
|
assert ray.get(deatched_actor.ping.remote()) == "pong"
|
|
|
|
|
|
# This test verifies actor creation task failure will not
|
|
# hang the caller.
|
|
def test_actor_creation_task_crash(ray_start_regular):
|
|
# Test actor death in constructor.
|
|
@ray.remote(max_restarts=0)
|
|
class Actor:
|
|
def __init__(self):
|
|
print("crash")
|
|
os._exit(0)
|
|
|
|
def f(self):
|
|
return "ACTOR OK"
|
|
|
|
# Verify an exception is thrown.
|
|
a = Actor.remote()
|
|
with pytest.raises(ray.exceptions.RayActorError) as excinfo:
|
|
ray.get(a.f.remote())
|
|
assert excinfo.value.actor_id == a._actor_id.hex()
|
|
|
|
# Test an actor can be restarted successfully
|
|
# afte it dies in its constructor.
|
|
@ray.remote(max_restarts=3)
|
|
class RestartableActor:
|
|
def __init__(self):
|
|
count = self.get_count()
|
|
count += 1
|
|
# Make it die for the first 2 times.
|
|
if count < 3:
|
|
self.set_count(count)
|
|
print("crash: " + str(count))
|
|
os._exit(0)
|
|
else:
|
|
print("no crash")
|
|
|
|
def f(self):
|
|
return "ACTOR OK"
|
|
|
|
def get_count(self):
|
|
value = _internal_kv_get("count")
|
|
if value is None:
|
|
count = 0
|
|
else:
|
|
count = int(value)
|
|
return count
|
|
|
|
def set_count(self, count):
|
|
_internal_kv_put("count", str(count), True)
|
|
|
|
# Verify we can get the object successfully.
|
|
ra = RestartableActor.remote()
|
|
ray.get(ra.f.remote())
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"ray_start_regular", [{"num_cpus": 2, "resources": {"a": 1}}], indirect=True
|
|
)
|
|
def test_pending_actor_removed_by_owner(ray_start_regular):
|
|
# Verify when an owner of pending actors is killed, the actor resources
|
|
# are correctly returned.
|
|
|
|
@ray.remote(num_cpus=1, resources={"a": 1})
|
|
class A:
|
|
def __init__(self):
|
|
self.actors = []
|
|
|
|
def create_actors(self):
|
|
self.actors = [B.remote() for _ in range(2)]
|
|
|
|
@ray.remote(resources={"a": 1})
|
|
class B:
|
|
def ping(self):
|
|
return True
|
|
|
|
@ray.remote(resources={"a": 1})
|
|
def f():
|
|
return True
|
|
|
|
a = A.remote()
|
|
# Create pending actors
|
|
ray.get(a.create_actors.remote())
|
|
|
|
# Owner is dead. pending actors should be killed
|
|
# and raylet should return workers correctly.
|
|
del a
|
|
a = B.remote()
|
|
assert ray.get(a.ping.remote())
|
|
ray.kill(a)
|
|
assert ray.get(f.remote())
|
|
|
|
|
|
def test_pickling_actor_handle(ray_start_regular_shared):
|
|
@ray.remote
|
|
class Foo:
|
|
def method(self):
|
|
pass
|
|
|
|
f = Foo.remote()
|
|
new_f = ray._private.worker.pickle.loads(ray._private.worker.pickle.dumps(f))
|
|
# Verify that we can call a method on the unpickled handle. TODO(rkn):
|
|
# we should also test this from a different driver.
|
|
ray.get(new_f.method.remote())
|
|
|
|
|
|
def test_pickled_actor_handle_call_in_method_twice(ray_start_regular_shared):
|
|
@ray.remote
|
|
class Actor1:
|
|
def f(self):
|
|
return 1
|
|
|
|
@ray.remote
|
|
class Actor2:
|
|
def __init__(self, constructor):
|
|
self.actor = constructor()
|
|
|
|
def step(self):
|
|
ray.get(self.actor.f.remote())
|
|
|
|
a = Actor1.remote()
|
|
|
|
b = Actor2.remote(lambda: a)
|
|
|
|
ray.get(b.step.remote())
|
|
ray.get(b.step.remote())
|
|
|
|
|
|
def test_kill(ray_start_regular_shared):
|
|
@ray.remote
|
|
class Actor:
|
|
def hang(self):
|
|
while True:
|
|
time.sleep(1)
|
|
|
|
actor = Actor.remote()
|
|
result = actor.hang.remote()
|
|
ready, _ = ray.wait([result], timeout=0.5)
|
|
assert len(ready) == 0
|
|
kill_actor_and_wait_for_failure(actor)
|
|
|
|
with pytest.raises(ray.exceptions.RayActorError):
|
|
ray.get(result)
|
|
|
|
with pytest.raises(ValueError):
|
|
ray.kill("not_an_actor_handle")
|
|
|
|
|
|
def test_get_actor_no_input(ray_start_regular_shared):
|
|
for bad_name in [None, "", " "]:
|
|
with pytest.raises(ValueError):
|
|
ray.get_actor(bad_name)
|
|
|
|
|
|
def test_actor_resource_demand(shutdown_only):
|
|
ray.shutdown()
|
|
cluster = ray.init(num_cpus=3)
|
|
global_state_accessor = make_global_state_accessor(cluster)
|
|
|
|
@ray.remote(num_cpus=2)
|
|
class Actor:
|
|
def foo(self):
|
|
return "ok"
|
|
|
|
a = Actor.remote()
|
|
ray.get(a.foo.remote())
|
|
time.sleep(1)
|
|
|
|
message = global_state_accessor.get_all_resource_usage()
|
|
resource_usages = gcs_utils.ResourceUsageBatchData.FromString(message)
|
|
|
|
# The actor is scheduled so there should be no more demands left.
|
|
assert len(resource_usages.resource_load_by_shape.resource_demands) == 0
|
|
|
|
@ray.remote(num_cpus=80)
|
|
class Actor2:
|
|
pass
|
|
|
|
actors = []
|
|
actors.append(Actor2.remote())
|
|
time.sleep(1)
|
|
|
|
# This actor cannot be scheduled.
|
|
message = global_state_accessor.get_all_resource_usage()
|
|
resource_usages = gcs_utils.ResourceUsageBatchData.FromString(message)
|
|
assert len(resource_usages.resource_load_by_shape.resource_demands) == 1
|
|
assert resource_usages.resource_load_by_shape.resource_demands[0].shape == {
|
|
"CPU": 80.0
|
|
}
|
|
assert (
|
|
resource_usages.resource_load_by_shape.resource_demands[
|
|
0
|
|
].num_infeasible_requests_queued
|
|
== 1
|
|
)
|
|
|
|
actors.append(Actor2.remote())
|
|
time.sleep(1)
|
|
|
|
# Two actors cannot be scheduled.
|
|
message = global_state_accessor.get_all_resource_usage()
|
|
resource_usages = gcs_utils.ResourceUsageBatchData.FromString(message)
|
|
assert len(resource_usages.resource_load_by_shape.resource_demands) == 1
|
|
assert (
|
|
resource_usages.resource_load_by_shape.resource_demands[
|
|
0
|
|
].num_infeasible_requests_queued
|
|
== 2
|
|
)
|
|
|
|
|
|
def test_kill_pending_actor_with_no_restart_true():
|
|
cluster = ray.init()
|
|
global_state_accessor = make_global_state_accessor(cluster)
|
|
|
|
@ray.remote(resources={"WORKER": 1.0})
|
|
class PendingActor:
|
|
pass
|
|
|
|
# Kill actor with `no_restart=True`.
|
|
actor = PendingActor.remote()
|
|
# TODO(ffbin): The raylet doesn't guarantee the order when dealing with
|
|
# RequestWorkerLease and CancelWorkerLease. If we kill the actor
|
|
# immediately after creating the actor, we may not be able to clean up
|
|
# the request cached by the raylet.
|
|
# See https://github.com/ray-project/ray/issues/13545 for details.
|
|
time.sleep(1)
|
|
ray.kill(actor, no_restart=True)
|
|
|
|
def condition1():
|
|
message = global_state_accessor.get_all_resource_usage()
|
|
resource_usages = gcs_utils.ResourceUsageBatchData.FromString(message)
|
|
if len(resource_usages.resource_load_by_shape.resource_demands) == 0:
|
|
return True
|
|
return False
|
|
|
|
# Actor is dead, so the infeasible task queue length is 0.
|
|
wait_for_condition(condition1, timeout=10)
|
|
|
|
ray.shutdown()
|
|
|
|
|
|
def test_actor_timestamps(ray_start_regular):
|
|
@ray.remote
|
|
class Foo:
|
|
def get_id(self):
|
|
return ray.get_runtime_context().get_actor_id()
|
|
|
|
def kill_self(self):
|
|
sys.exit(1)
|
|
|
|
def graceful_exit():
|
|
actor = Foo.remote()
|
|
actor_id = ray.get(actor.get_id.remote())
|
|
|
|
state_after_starting = ray._private.state.actors()[actor_id]
|
|
time.sleep(1)
|
|
del actor
|
|
time.sleep(1)
|
|
state_after_ending = ray._private.state.actors()[actor_id]
|
|
|
|
assert state_after_starting["StartTime"] == state_after_ending["StartTime"]
|
|
start_time = state_after_ending["StartTime"]
|
|
end_time = state_after_ending["EndTime"]
|
|
assert end_time > start_time > 0, f"Start: {start_time}, End: {end_time}"
|
|
|
|
def not_graceful_exit():
|
|
actor = Foo.remote()
|
|
actor_id = ray.get(actor.get_id.remote())
|
|
|
|
state_after_starting = ray._private.state.actors()[actor_id]
|
|
time.sleep(1)
|
|
actor.kill_self.remote()
|
|
time.sleep(1)
|
|
state_after_ending = ray._private.state.actors()[actor_id]
|
|
|
|
assert state_after_starting["StartTime"] == state_after_ending["StartTime"]
|
|
|
|
start_time = state_after_ending["StartTime"]
|
|
end_time = state_after_ending["EndTime"]
|
|
assert end_time > start_time > 0, f"Start: {start_time}, End: {end_time}"
|
|
|
|
def restarted():
|
|
actor = Foo.options(max_restarts=1, max_task_retries=-1).remote()
|
|
actor_id = ray.get(actor.get_id.remote())
|
|
|
|
state_after_starting = ray._private.state.actors()[actor_id]
|
|
time.sleep(1)
|
|
actor.kill_self.remote()
|
|
time.sleep(1)
|
|
actor.kill_self.remote()
|
|
time.sleep(1)
|
|
state_after_ending = ray._private.state.actors()[actor_id]
|
|
|
|
assert state_after_starting["StartTime"] == state_after_ending["StartTime"]
|
|
|
|
start_time = state_after_ending["StartTime"]
|
|
end_time = state_after_ending["EndTime"]
|
|
assert end_time > start_time > 0, f"Start: {start_time}, End: {end_time}"
|
|
|
|
graceful_exit()
|
|
not_graceful_exit()
|
|
restarted()
|
|
|
|
|
|
def test_kill_pending_actor_with_no_restart_false():
|
|
cluster = ray.init()
|
|
global_state_accessor = make_global_state_accessor(cluster)
|
|
|
|
@ray.remote(resources={"WORKER": 1.0}, max_restarts=1)
|
|
class PendingActor:
|
|
pass
|
|
|
|
# Kill actor with `no_restart=False`.
|
|
actor = PendingActor.remote()
|
|
# TODO(ffbin): The raylet doesn't guarantee the order when dealing with
|
|
# RequestWorkerLease and CancelWorkerLease. If we kill the actor
|
|
# immediately after creating the actor, we may not be able to clean up
|
|
# the request cached by the raylet.
|
|
# See https://github.com/ray-project/ray/issues/13545 for details.
|
|
time.sleep(1)
|
|
ray.kill(actor, no_restart=False)
|
|
|
|
def condition1():
|
|
message = global_state_accessor.get_all_resource_usage()
|
|
resource_usages = gcs_utils.ResourceUsageBatchData.FromString(message)
|
|
if len(resource_usages.resource_load_by_shape.resource_demands) == 0:
|
|
return False
|
|
return True
|
|
|
|
# Actor restarts, so the infeasible task queue length is 1.
|
|
wait_for_condition(condition1, timeout=10)
|
|
|
|
# Kill actor again and actor is dead,
|
|
# so the infeasible task queue length is 0.
|
|
ray.kill(actor, no_restart=False)
|
|
|
|
def condition2():
|
|
message = global_state_accessor.get_all_resource_usage()
|
|
resource_usages = gcs_utils.ResourceUsageBatchData.FromString(message)
|
|
if len(resource_usages.resource_load_by_shape.resource_demands) == 0:
|
|
return True
|
|
return False
|
|
|
|
wait_for_condition(condition2, timeout=10)
|
|
|
|
ray.shutdown()
|
|
|
|
|
|
def test_actor_namespace_access(ray_start_regular):
|
|
@ray.remote
|
|
class A:
|
|
def hi(self):
|
|
return "hi"
|
|
|
|
A.options(name="actor_in_current_namespace", lifetime="detached").remote()
|
|
A.options(name="actor_name", namespace="namespace", lifetime="detached").remote()
|
|
ray.get_actor("actor_in_current_namespace") # => works
|
|
ray.get_actor("actor_name", namespace="namespace") # => works
|
|
match_str = r"Failed to look up actor with name.*"
|
|
with pytest.raises(ValueError, match=match_str):
|
|
ray.get_actor("actor_name") # => errors
|
|
|
|
|
|
def test_get_actor_after_killed(shutdown_only):
|
|
ray.init(num_cpus=2, include_dashboard=True)
|
|
|
|
@ray.remote
|
|
class A:
|
|
def ready(self):
|
|
return True
|
|
|
|
actor = A.options(name="actor", namespace="namespace").remote()
|
|
ray.kill(actor)
|
|
with pytest.raises(ValueError):
|
|
ray.get_actor("actor", namespace="namespace")
|
|
|
|
actor = A.options(
|
|
name="actor_2",
|
|
namespace="namespace",
|
|
max_restarts=1,
|
|
max_task_retries=-1,
|
|
).remote()
|
|
ray.kill(actor, no_restart=False)
|
|
assert ray.get(ray.get_actor("actor_2", namespace="namespace").ready.remote())
|
|
|
|
|
|
def test_get_actor_from_concurrent_tasks(shutdown_only):
|
|
@ray.remote
|
|
class Actor:
|
|
def get_actor_id(self) -> str:
|
|
return ray.get_runtime_context().get_actor_id()
|
|
|
|
actor_name = "test_actor"
|
|
|
|
@ray.remote(num_cpus=0)
|
|
def get_or_create_actor():
|
|
try:
|
|
# The first task will try to get the actor but fail (doesn't exist).
|
|
try:
|
|
actor = ray.get_actor(actor_name)
|
|
except Exception:
|
|
print("Get failed, trying to create")
|
|
# Actor must be detached so it outlives this task and other tasks can
|
|
# get a handle to it.
|
|
actor = Actor.options(name=actor_name, lifetime="detached").remote()
|
|
except Exception:
|
|
# Multiple tasks may have reached the creation block above.
|
|
# Only one will succeed and the others will get an error, in which case
|
|
# they fall here and should be able to get the actor handle.
|
|
print("Someone else created it, trying to get")
|
|
actor = ray.get_actor(actor_name)
|
|
|
|
return ray.get(actor.get_actor_id.remote())
|
|
|
|
# Run 10 concurrent tasks to get or create the same actor.
|
|
# Only one task should succeed at creating it, and all the others should get it.
|
|
assert len(set(ray.get([get_or_create_actor.remote() for _ in range(10)]))) == 1
|
|
|
|
|
|
def test_get_or_create_actor_from_multiple_threads(shutdown_only):
|
|
"""Make sure we can create actors in multiple threads without
|
|
race conditions.
|
|
|
|
Check https://github.com/ray-project/ray/issues/41324
|
|
"""
|
|
|
|
@ray.remote
|
|
class Counter:
|
|
def __init__(self):
|
|
self._count = 0
|
|
|
|
def inc(self):
|
|
self._count += 1
|
|
|
|
def get(self) -> int:
|
|
return self._count
|
|
|
|
counter = Counter.remote()
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def __init__(self):
|
|
ray.get(counter.inc.remote())
|
|
|
|
def get_actor_id(self) -> str:
|
|
return ray.get_runtime_context().get_actor_id()
|
|
|
|
def _create_or_get_actor(*args):
|
|
a = Actor.options(
|
|
name="test_actor",
|
|
get_if_exists=True,
|
|
# Actor must be detached so it outlives this function and other threads
|
|
# can get a handle to it.
|
|
lifetime="detached",
|
|
).remote()
|
|
|
|
return ray.get(a.get_actor_id.remote())
|
|
|
|
# Concurrently submit 100 calls to create or get the actor from 10 threads.
|
|
# Ensure that exactly one call actually creates the actor and the other 99 get it.
|
|
with ThreadPoolExecutor(max_workers=10) as tp:
|
|
assert len(set(tp.map(_create_or_get_actor, range(100)))) == 1
|
|
assert ray.get(counter.get.remote()) == 1
|
|
|
|
|
|
def test_get_actor_in_remote_workers(ray_start_cluster):
|
|
"""Make sure we can get and create actors without
|
|
race condition in a remote worker.
|
|
|
|
Check https://github.com/ray-project/ray/issues/20092. # noqa
|
|
"""
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=0)
|
|
cluster.add_node(num_cpus=1)
|
|
ray.init(address=cluster.address, namespace="xxx")
|
|
|
|
@ray.remote(num_cpus=0)
|
|
class RemoteProc:
|
|
def __init__(self):
|
|
pass
|
|
|
|
def procTask(self, a, b):
|
|
print("[%s]-> %s" % (a, b))
|
|
return a, b
|
|
|
|
@ray.remote
|
|
def submit_named_actors():
|
|
RemoteProc.options(
|
|
name="test", lifetime="detached", max_concurrency=10, namespace="xxx"
|
|
).remote()
|
|
proc = ray.get_actor("test", namespace="xxx")
|
|
ray.get(proc.procTask.remote(1, 2))
|
|
# Should be able to create an actor with the same name
|
|
# immediately after killing it.
|
|
ray.kill(proc)
|
|
RemoteProc.options(
|
|
name="test", lifetime="detached", max_concurrency=10, namespace="xxx"
|
|
).remote()
|
|
proc = ray.get_actor("test", namespace="xxx")
|
|
return ray.get(proc.procTask.remote(1, 2))
|
|
|
|
assert (1, 2) == ray.get(submit_named_actors.remote())
|
|
|
|
|
|
def test_resource_leak_when_cancel_actor_in_phase_of_creating(ray_start_cluster):
|
|
"""Make sure there is no resource leak when cancel an actor in phase of
|
|
creating.
|
|
|
|
Check https://github.com/ray-project/ray/issues/27743. # noqa
|
|
"""
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=2)
|
|
ray.init(address=cluster.address)
|
|
cluster.wait_for_nodes()
|
|
|
|
@ray.remote(num_cpus=1)
|
|
class Actor:
|
|
def __init__(self, signal_1, signal_2):
|
|
signal_1.send.remote()
|
|
ray.get(signal_2.wait.remote())
|
|
pass
|
|
|
|
signal_1 = SignalActor.remote()
|
|
signal_2 = SignalActor.remote()
|
|
actor = Actor.remote(signal_1, signal_2)
|
|
|
|
wait_for_condition(lambda: ray.available_resources()["CPU"] != 2)
|
|
|
|
# Checking that the constructor of `Actor`` is invoked.
|
|
ready_ids, _ = ray.wait([signal_1.wait.remote()], timeout=3.0)
|
|
assert len(ready_ids) == 1
|
|
|
|
# Kill the actor which is in the phase of creating.
|
|
ray.kill(actor)
|
|
|
|
# Ensure there is no resource leak.
|
|
wait_for_condition(lambda: ray.available_resources()["CPU"] == 2)
|
|
|
|
|
|
def test_actor_gc(monkeypatch, shutdown_only):
|
|
MAX_DEAD_ACTOR_CNT = 5
|
|
with monkeypatch.context() as m:
|
|
m.setenv("RAY_maximum_gcs_destroyed_actor_cached_count", MAX_DEAD_ACTOR_CNT)
|
|
ray.init()
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def ready(self):
|
|
pass
|
|
|
|
actors = [Actor.remote() for _ in range(10)]
|
|
ray.get([actor.ready.remote() for actor in actors])
|
|
alive_actors = 0
|
|
for a in list_actors():
|
|
if a["state"] == "ALIVE":
|
|
alive_actors += 1
|
|
assert alive_actors == 10
|
|
# Kill actors
|
|
del actors
|
|
|
|
def verify_cached_dead_actor_cleaned():
|
|
return len(list_actors()) == MAX_DEAD_ACTOR_CNT # noqa
|
|
|
|
wait_for_condition(verify_cached_dead_actor_cleaned)
|
|
|
|
# Test detached actors
|
|
actors = [Actor.options(lifetime="detached").remote() for _ in range(10)]
|
|
ray.get([actor.ready.remote() for actor in actors])
|
|
alive_actors = 0
|
|
for a in list_actors():
|
|
if a["state"] == "ALIVE":
|
|
alive_actors += 1
|
|
assert alive_actors == 10
|
|
# Kill actors
|
|
for actor in actors:
|
|
ray.kill(actor)
|
|
|
|
wait_for_condition(verify_cached_dead_actor_cleaned)
|
|
|
|
# Test actors created by a driver.
|
|
|
|
driver = """
|
|
import ray
|
|
from ray.util.state import list_actors
|
|
ray.init("auto")
|
|
|
|
@ray.remote
|
|
class A:
|
|
def ready(self):
|
|
pass
|
|
|
|
actors = [A.remote() for _ in range(10)]
|
|
ray.get([actor.ready.remote() for actor in actors])
|
|
alive_actors = 0
|
|
for a in list_actors():
|
|
if a.state == "ALIVE":
|
|
alive_actors += 1
|
|
assert alive_actors == 10
|
|
"""
|
|
|
|
run_string_as_driver(driver)
|
|
# Driver exits, so dead actors must be cleaned.
|
|
wait_for_condition(verify_cached_dead_actor_cleaned)
|
|
print(list_actors())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-sv", __file__]))
|