1908 lines
52 KiB
Python
1908 lines
52 KiB
Python
import os
|
||
import random
|
||
import sys
|
||
import tempfile
|
||
|
||
import numpy as np
|
||
import pytest
|
||
|
||
import ray
|
||
from ray import cloudpickle as pickle
|
||
from ray._common.test_utils import SignalActor, wait_for_condition
|
||
from ray._common.utils import hex_to_binary
|
||
from ray._private import ray_constants
|
||
from ray._private.state_api_test_utils import invoke_state_api, invoke_state_api_n
|
||
from ray._private.test_utils import (
|
||
client_test_enabled,
|
||
wait_for_pid_to_exit,
|
||
)
|
||
from ray.actor import ActorClassInheritanceException
|
||
from ray.core.generated import gcs_pb2
|
||
from ray.tests.client_test_utils import create_remote_signal_actor
|
||
from ray.util.state import list_actors
|
||
|
||
import psutil
|
||
|
||
|
||
@pytest.mark.parametrize("set_enable_auto_connect", [True, False], indirect=True)
|
||
def test_caching_actors(shutdown_only, set_enable_auto_connect):
|
||
# Test defining actors before ray.init() has been called.
|
||
|
||
@ray.remote
|
||
class Foo:
|
||
def __init__(self):
|
||
pass
|
||
|
||
def get_val(self):
|
||
return 3
|
||
|
||
if not set_enable_auto_connect:
|
||
# Check that we can't actually create actors before ray.init() has
|
||
# been called.
|
||
with pytest.raises(Exception):
|
||
f = Foo.remote()
|
||
|
||
ray.init(num_cpus=1)
|
||
else:
|
||
# Actor creation should succeed here because ray.init() auto connection
|
||
# is (by default) enabled.
|
||
f = Foo.remote()
|
||
|
||
f = Foo.remote()
|
||
|
||
assert ray.get(f.get_val.remote()) == 3
|
||
|
||
|
||
# https://github.com/ray-project/ray/issues/20554
|
||
def test_not_reusing_task_workers(shutdown_only):
|
||
@ray.remote
|
||
def create_ref():
|
||
ref = ray.put(np.zeros(10_000_000))
|
||
return ref
|
||
|
||
@ray.remote
|
||
class Actor:
|
||
def __init__(self):
|
||
return
|
||
|
||
def foo(self):
|
||
return
|
||
|
||
ray.init(num_cpus=1, object_store_memory=100_000_000)
|
||
wrapped_ref = create_ref.remote()
|
||
print(ray.get(ray.get(wrapped_ref)))
|
||
|
||
# create_ref worker gets reused as an actor.
|
||
a = Actor.remote()
|
||
ray.get(a.foo.remote())
|
||
# Actor will get force-killed.
|
||
del a
|
||
|
||
# Flush the object store.
|
||
for _ in range(10):
|
||
ray.put(np.zeros(10_000_000))
|
||
|
||
# Object has been evicted and owner has died. Throws OwnerDiedError.
|
||
print(ray.get(ray.get(wrapped_ref)))
|
||
|
||
|
||
def test_remote_function_within_actor(ray_start_10_cpus):
|
||
# Make sure we can use remote functions within actors.
|
||
|
||
# Create some values to close over.
|
||
val1 = 1
|
||
val2 = 2
|
||
|
||
@ray.remote
|
||
def f(x):
|
||
return val1 + x
|
||
|
||
@ray.remote
|
||
def g(x):
|
||
return ray.get(f.remote(x))
|
||
|
||
@ray.remote
|
||
class Actor:
|
||
def __init__(self, x):
|
||
self.x = x
|
||
self.y = val2
|
||
self.object_refs = [f.remote(i) for i in range(5)]
|
||
self.values2 = ray.get([f.remote(i) for i in range(5)])
|
||
|
||
def get_values(self):
|
||
return self.x, self.y, self.object_refs, self.values2
|
||
|
||
def f(self):
|
||
return [f.remote(i) for i in range(5)]
|
||
|
||
def g(self):
|
||
return ray.get([g.remote(i) for i in range(5)])
|
||
|
||
def h(self, object_refs):
|
||
return ray.get(object_refs)
|
||
|
||
actor = Actor.remote(1)
|
||
values = ray.get(actor.get_values.remote())
|
||
assert values[0] == 1
|
||
assert values[1] == val2
|
||
assert ray.get(values[2]) == list(range(1, 6))
|
||
assert values[3] == list(range(1, 6))
|
||
|
||
assert ray.get(ray.get(actor.f.remote())) == list(range(1, 6))
|
||
assert ray.get(actor.g.remote()) == list(range(1, 6))
|
||
assert ray.get(actor.h.remote([f.remote(i) for i in range(5)])) == list(range(1, 6))
|
||
|
||
|
||
def test_define_actor_within_actor(ray_start_10_cpus):
|
||
# Make sure we can use remote functions within actors.
|
||
|
||
@ray.remote
|
||
class Actor1:
|
||
def __init__(self, x):
|
||
self.x = x
|
||
|
||
def new_actor(self, z):
|
||
@ray.remote
|
||
class Actor2:
|
||
def __init__(self, x):
|
||
self.x = x
|
||
|
||
def get_value(self):
|
||
return self.x
|
||
|
||
self.actor2 = Actor2.remote(z)
|
||
|
||
def get_values(self, z):
|
||
self.new_actor(z)
|
||
return self.x, ray.get(self.actor2.get_value.remote())
|
||
|
||
actor1 = Actor1.remote(3)
|
||
assert ray.get(actor1.get_values.remote(5)) == (3, 5)
|
||
|
||
|
||
def test_use_actor_within_actor(ray_start_10_cpus):
|
||
# Make sure we can use actors within actors.
|
||
|
||
@ray.remote
|
||
class Actor1:
|
||
def __init__(self, x):
|
||
self.x = x
|
||
|
||
def get_val(self):
|
||
return self.x
|
||
|
||
@ray.remote
|
||
class Actor2:
|
||
def __init__(self, x, y):
|
||
self.x = x
|
||
self.actor1 = Actor1.remote(y)
|
||
|
||
def get_values(self, z):
|
||
return self.x, ray.get(self.actor1.get_val.remote())
|
||
|
||
actor2 = Actor2.remote(3, 4)
|
||
assert ray.get(actor2.get_values.remote(5)) == (3, 4)
|
||
|
||
|
||
def test_use_actor_twice(ray_start_10_cpus):
|
||
# Make sure we can call the same actor using different refs.
|
||
|
||
@ray.remote
|
||
class Actor1:
|
||
def __init__(self):
|
||
self.count = 0
|
||
|
||
def inc(self):
|
||
self.count += 1
|
||
return self.count
|
||
|
||
@ray.remote
|
||
class Actor2:
|
||
def __init__(self):
|
||
pass
|
||
|
||
def inc(self, handle):
|
||
return ray.get(handle.inc.remote())
|
||
|
||
a = Actor1.remote()
|
||
a2 = Actor2.remote()
|
||
assert ray.get(a2.inc.remote(a)) == 1
|
||
assert ray.get(a2.inc.remote(a)) == 2
|
||
|
||
|
||
def test_define_actor_within_remote_function(ray_start_10_cpus):
|
||
# Make sure we can define and actors within remote functions.
|
||
|
||
@ray.remote
|
||
def f(x, n):
|
||
@ray.remote
|
||
class Actor1:
|
||
def __init__(self, x):
|
||
self.x = x
|
||
|
||
def get_value(self):
|
||
return self.x
|
||
|
||
actor = Actor1.remote(x)
|
||
return ray.get([actor.get_value.remote() for _ in range(n)])
|
||
|
||
assert ray.get(f.remote(3, 1)) == [3]
|
||
assert ray.get([f.remote(i, 20) for i in range(10)]) == [
|
||
20 * [i] for i in range(10)
|
||
]
|
||
|
||
|
||
def test_use_actor_within_remote_function(ray_start_10_cpus):
|
||
# Make sure we can create and use actors within remote functions.
|
||
|
||
@ray.remote
|
||
class Actor1:
|
||
def __init__(self, x):
|
||
self.x = x
|
||
|
||
def get_values(self):
|
||
return self.x
|
||
|
||
@ray.remote
|
||
def f(x):
|
||
actor = Actor1.remote(x)
|
||
return ray.get(actor.get_values.remote())
|
||
|
||
assert ray.get(f.remote(3)) == 3
|
||
|
||
|
||
def test_actor_import_counter(ray_start_10_cpus):
|
||
# This is mostly a test of the export counters to make sure that when
|
||
# an actor is imported, all of the necessary remote functions have been
|
||
# imported.
|
||
|
||
# Export a bunch of remote functions.
|
||
num_remote_functions = 50
|
||
for i in range(num_remote_functions):
|
||
|
||
@ray.remote
|
||
def f():
|
||
return i
|
||
|
||
@ray.remote
|
||
def g():
|
||
@ray.remote
|
||
class Actor:
|
||
def __init__(self):
|
||
# This should use the last version of f.
|
||
self.x = ray.get(f.remote())
|
||
|
||
def get_val(self):
|
||
return self.x
|
||
|
||
actor = Actor.remote()
|
||
return ray.get(actor.get_val.remote())
|
||
|
||
assert ray.get(g.remote()) == num_remote_functions - 1
|
||
|
||
|
||
@pytest.mark.parametrize("enable_concurrency_group", [False, True])
|
||
def test_exit_actor(ray_start_regular, enable_concurrency_group):
|
||
concurrency_groups = {"io": 1} if enable_concurrency_group else None
|
||
|
||
@ray.remote(concurrency_groups=concurrency_groups)
|
||
class TestActor:
|
||
def exit(self):
|
||
ray.actor.exit_actor()
|
||
|
||
num_actors = 30
|
||
actor_class_name = TestActor.__ray_metadata__.class_name
|
||
|
||
actors = [TestActor.remote() for _ in range(num_actors)]
|
||
ray.get([actor.__ray_ready__.remote() for actor in actors])
|
||
invoke_state_api(
|
||
lambda res: len(res) == num_actors,
|
||
list_actors,
|
||
filters=[("state", "=", "ALIVE"), ("class_name", "=", actor_class_name)],
|
||
limit=1000,
|
||
)
|
||
|
||
ray.wait([actor.exit.remote() for actor in actors], timeout=10.0)
|
||
|
||
invoke_state_api_n(
|
||
lambda res: len(res) == 0,
|
||
list_actors,
|
||
filters=[("state", "=", "ALIVE"), ("class_name", "=", actor_class_name)],
|
||
limit=1000,
|
||
)
|
||
|
||
invoke_state_api(
|
||
lambda res: len(res) == num_actors,
|
||
list_actors,
|
||
filters=[("state", "=", "DEAD"), ("class_name", "=", actor_class_name)],
|
||
limit=1000,
|
||
)
|
||
|
||
|
||
@pytest.mark.skipif(client_test_enabled(), reason="internal api")
|
||
def test_actor_method_metadata_cache(ray_start_regular):
|
||
class Actor(object):
|
||
pass
|
||
|
||
# The cache of _ActorClassMethodMetadata.
|
||
cache = ray.actor._ActorClassMethodMetadata._cache
|
||
cache.clear()
|
||
|
||
# Check cache hit during ActorHandle deserialization.
|
||
A1 = ray.remote(Actor)
|
||
a = A1.remote()
|
||
assert len(cache) == 1
|
||
cached_data_id = [id(x) for x in list(cache.items())[0]]
|
||
for x in range(10):
|
||
a = pickle.loads(pickle.dumps(a))
|
||
assert len(ray.actor._ActorClassMethodMetadata._cache) == 1
|
||
assert [id(x) for x in list(cache.items())[0]] == cached_data_id
|
||
|
||
|
||
@pytest.mark.skipif(client_test_enabled(), reason="internal api")
|
||
def test_actor_class_name(ray_start_regular):
|
||
@ray.remote
|
||
class Foo:
|
||
def __init__(self):
|
||
pass
|
||
|
||
Foo.remote()
|
||
g = ray._private.worker.global_worker.gcs_client
|
||
actor_keys = g.internal_kv_keys(
|
||
b"ActorClass", ray_constants.KV_NAMESPACE_FUNCTION_TABLE
|
||
)
|
||
assert len(actor_keys) == 1
|
||
actor_class_info = pickle.loads(
|
||
g.internal_kv_get(actor_keys[0], ray_constants.KV_NAMESPACE_FUNCTION_TABLE)
|
||
)
|
||
assert actor_class_info["class_name"] == "Foo"
|
||
assert "test_actor" in actor_class_info["module"]
|
||
|
||
|
||
def test_actor_exit_from_task(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Actor:
|
||
def __init__(self):
|
||
print("Actor created")
|
||
|
||
def f(self):
|
||
return 0
|
||
|
||
@ray.remote
|
||
def f():
|
||
a = Actor.remote()
|
||
x_id = a.f.remote()
|
||
return [x_id]
|
||
|
||
x_id = ray.get(f.remote())[0]
|
||
print(ray.get(x_id)) # This should not hang.
|
||
|
||
|
||
def test_actor_init_error_propagated(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Actor:
|
||
def __init__(self, error=False):
|
||
if error:
|
||
raise Exception("oops")
|
||
|
||
def foo(self):
|
||
return "OK"
|
||
|
||
actor = Actor.remote(error=False)
|
||
ray.get(actor.foo.remote())
|
||
|
||
actor = Actor.remote(error=True)
|
||
with pytest.raises(Exception, match=".*oops.*"):
|
||
ray.get(actor.foo.remote())
|
||
|
||
|
||
def test_keyword_args(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Actor:
|
||
def __init__(self, arg0, arg1=1, arg2="a"):
|
||
self.arg0 = arg0
|
||
self.arg1 = arg1
|
||
self.arg2 = arg2
|
||
|
||
def get_values(self, arg0, arg1=2, arg2="b"):
|
||
return self.arg0 + arg0, self.arg1 + arg1, self.arg2 + arg2
|
||
|
||
actor = Actor.remote(0)
|
||
assert ray.get(actor.get_values.remote(1)) == (1, 3, "ab")
|
||
|
||
actor = Actor.remote(1, 2)
|
||
assert ray.get(actor.get_values.remote(2, 3)) == (3, 5, "ab")
|
||
|
||
actor = Actor.remote(1, 2, "c")
|
||
assert ray.get(actor.get_values.remote(2, 3, "d")) == (3, 5, "cd")
|
||
|
||
actor = Actor.remote(1, arg2="c")
|
||
assert ray.get(actor.get_values.remote(0, arg2="d")) == (1, 3, "cd")
|
||
assert ray.get(actor.get_values.remote(0, arg2="d", arg1=0)) == (1, 1, "cd")
|
||
|
||
actor = Actor.remote(1, arg2="c", arg1=2)
|
||
assert ray.get(actor.get_values.remote(0, arg2="d")) == (1, 4, "cd")
|
||
assert ray.get(actor.get_values.remote(0, arg2="d", arg1=0)) == (1, 2, "cd")
|
||
assert ray.get(actor.get_values.remote(arg2="d", arg1=0, arg0=2)) == (3, 2, "cd")
|
||
|
||
# Make sure we get an exception if the constructor is called
|
||
# incorrectly.
|
||
with pytest.raises(TypeError):
|
||
actor = Actor.remote()
|
||
|
||
with pytest.raises(TypeError):
|
||
actor = Actor.remote(0, 1, 2, arg3=3)
|
||
|
||
with pytest.raises(TypeError):
|
||
actor = Actor.remote(0, arg0=1)
|
||
|
||
# Make sure we get an exception if the method is called incorrectly.
|
||
actor = Actor.remote(1)
|
||
with pytest.raises(Exception):
|
||
ray.get(actor.get_values.remote())
|
||
|
||
|
||
def test_actor_name_conflict(ray_start_regular_shared):
|
||
@ray.remote
|
||
class A(object):
|
||
def foo(self):
|
||
return 100000
|
||
|
||
a = A.remote()
|
||
r = a.foo.remote()
|
||
|
||
results = [r]
|
||
for x in range(10):
|
||
|
||
@ray.remote
|
||
class A(object):
|
||
def foo(self):
|
||
return x
|
||
|
||
a = A.remote()
|
||
r = a.foo.remote()
|
||
results.append(r)
|
||
|
||
assert ray.get(results) == [100000, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||
|
||
|
||
def test_variable_number_of_args(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Actor:
|
||
def __init__(self, arg0, arg1=1, *args):
|
||
self.arg0 = arg0
|
||
self.arg1 = arg1
|
||
self.args = args
|
||
|
||
def get_values(self, arg0, arg1=2, *args):
|
||
return self.arg0 + arg0, self.arg1 + arg1, self.args, args
|
||
|
||
actor = Actor.remote(0)
|
||
assert ray.get(actor.get_values.remote(1)) == (1, 3, (), ())
|
||
|
||
actor = Actor.remote(1, 2)
|
||
assert ray.get(actor.get_values.remote(2, 3)) == (3, 5, (), ())
|
||
|
||
actor = Actor.remote(1, 2, "c")
|
||
assert ray.get(actor.get_values.remote(2, 3, "d")) == (3, 5, ("c",), ("d",))
|
||
|
||
actor = Actor.remote(1, 2, "a", "b", "c", "d")
|
||
assert ray.get(actor.get_values.remote(2, 3, 1, 2, 3, 4)) == (
|
||
3,
|
||
5,
|
||
("a", "b", "c", "d"),
|
||
(1, 2, 3, 4),
|
||
)
|
||
|
||
@ray.remote
|
||
class Actor:
|
||
def __init__(self, *args):
|
||
self.args = args
|
||
|
||
def get_values(self, *args):
|
||
return self.args, args
|
||
|
||
a = Actor.remote()
|
||
assert ray.get(a.get_values.remote()) == ((), ())
|
||
a = Actor.remote(1)
|
||
assert ray.get(a.get_values.remote(2)) == ((1,), (2,))
|
||
a = Actor.remote(1, 2)
|
||
assert ray.get(a.get_values.remote(3, 4)) == ((1, 2), (3, 4))
|
||
|
||
|
||
def test_no_args(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Actor:
|
||
def __init__(self):
|
||
pass
|
||
|
||
def get_values(self):
|
||
pass
|
||
|
||
actor = Actor.remote()
|
||
assert ray.get(actor.get_values.remote()) is None
|
||
|
||
|
||
def test_no_constructor(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Actor:
|
||
def get_values(self):
|
||
pass
|
||
|
||
actor = Actor.remote()
|
||
assert ray.get(actor.get_values.remote()) is None
|
||
|
||
|
||
def test_custom_classes(ray_start_regular_shared):
|
||
class Foo:
|
||
def __init__(self, x):
|
||
self.x = x
|
||
|
||
@ray.remote
|
||
class Actor:
|
||
def __init__(self, f2):
|
||
self.f1 = Foo(1)
|
||
self.f2 = f2
|
||
|
||
def get_values1(self):
|
||
return self.f1, self.f2
|
||
|
||
def get_values2(self, f3):
|
||
return self.f1, self.f2, f3
|
||
|
||
actor = Actor.remote(Foo(2))
|
||
results1 = ray.get(actor.get_values1.remote())
|
||
assert results1[0].x == 1
|
||
assert results1[1].x == 2
|
||
results2 = ray.get(actor.get_values2.remote(Foo(3)))
|
||
assert results2[0].x == 1
|
||
assert results2[1].x == 2
|
||
assert results2[2].x == 3
|
||
|
||
|
||
def test_actor_class_attributes(ray_start_regular_shared):
|
||
class Grandparent:
|
||
GRANDPARENT = 2
|
||
|
||
class Parent1(Grandparent):
|
||
PARENT1 = 6
|
||
|
||
class Parent2:
|
||
PARENT2 = 7
|
||
|
||
@ray.remote
|
||
class TestActor(Parent1, Parent2):
|
||
X = 3
|
||
|
||
@classmethod
|
||
def f(cls):
|
||
assert TestActor.GRANDPARENT == 2
|
||
assert TestActor.PARENT1 == 6
|
||
assert TestActor.PARENT2 == 7
|
||
assert TestActor.X == 3
|
||
return 4
|
||
|
||
def g(self):
|
||
assert TestActor.GRANDPARENT == 2
|
||
assert TestActor.PARENT1 == 6
|
||
assert TestActor.PARENT2 == 7
|
||
assert TestActor.f() == 4
|
||
return TestActor.X
|
||
|
||
t = TestActor.remote()
|
||
assert ray.get(t.g.remote()) == 3
|
||
|
||
|
||
def test_actor_static_attributes(ray_start_regular_shared):
|
||
class Grandparent:
|
||
GRANDPARENT = 2
|
||
|
||
@staticmethod
|
||
def grandparent_static():
|
||
assert Grandparent.GRANDPARENT == 2
|
||
return 1
|
||
|
||
class Parent1(Grandparent):
|
||
PARENT1 = 6
|
||
|
||
@staticmethod
|
||
def parent1_static():
|
||
assert Parent1.PARENT1 == 6
|
||
return 2
|
||
|
||
def parent1(self):
|
||
assert Parent1.PARENT1 == 6
|
||
|
||
class Parent2:
|
||
PARENT2 = 7
|
||
|
||
def parent2(self):
|
||
assert Parent2.PARENT2 == 7
|
||
|
||
@ray.remote
|
||
class TestActor(Parent1, Parent2):
|
||
X = 3
|
||
|
||
@staticmethod
|
||
def f():
|
||
assert TestActor.GRANDPARENT == 2
|
||
assert TestActor.PARENT1 == 6
|
||
assert TestActor.PARENT2 == 7
|
||
assert TestActor.X == 3
|
||
return 4
|
||
|
||
def g(self):
|
||
assert TestActor.GRANDPARENT == 2
|
||
assert TestActor.PARENT1 == 6
|
||
assert TestActor.PARENT2 == 7
|
||
assert TestActor.f() == 4
|
||
return TestActor.X
|
||
|
||
t = TestActor.remote()
|
||
assert ray.get(t.g.remote()) == 3
|
||
|
||
|
||
def test_decorator_args(ray_start_regular_shared):
|
||
# This is an invalid way of using the actor decorator.
|
||
with pytest.raises(Exception):
|
||
|
||
@ray.remote()
|
||
class Actor:
|
||
def __init__(self):
|
||
pass
|
||
|
||
# This is an invalid way of using the actor decorator.
|
||
with pytest.raises(Exception):
|
||
|
||
@ray.remote(invalid_kwarg=0) # noqa: F811
|
||
class Actor: # noqa: F811
|
||
def __init__(self):
|
||
pass
|
||
|
||
# This is an invalid way of using the actor decorator.
|
||
with pytest.raises(Exception):
|
||
|
||
@ray.remote(num_cpus=0, invalid_kwarg=0) # noqa: F811
|
||
class Actor: # noqa: F811
|
||
def __init__(self):
|
||
pass
|
||
|
||
# This is a valid way of using the decorator.
|
||
@ray.remote(num_cpus=1) # noqa: F811
|
||
class Actor: # noqa: F811
|
||
def __init__(self):
|
||
pass
|
||
|
||
# This is a valid way of using the decorator.
|
||
@ray.remote(num_gpus=1) # noqa: F811
|
||
class Actor: # noqa: F811
|
||
def __init__(self):
|
||
pass
|
||
|
||
# This is a valid way of using the decorator.
|
||
@ray.remote(num_cpus=1, num_gpus=1) # noqa: F811
|
||
class Actor: # noqa: F811
|
||
def __init__(self):
|
||
pass
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"label_selector, expected_error",
|
||
[
|
||
( # Valid: multiple labels with implicit 'equals' condition
|
||
{"ray.io/market-type": "spot", "ray.io/accelerator-type": "H100"},
|
||
None,
|
||
),
|
||
( # Valid: not equals condition
|
||
{"ray.io/market-type": "!spot"},
|
||
None,
|
||
),
|
||
( # Valid: in condition
|
||
{"ray.io/accelerator-type": "in(H100, B200, TPU)"},
|
||
None,
|
||
),
|
||
( # Valid: not in condition
|
||
{"ray.io/accelerator-type": "!in(H100, B200)"},
|
||
None,
|
||
),
|
||
( # Invalid: label_selector expects a dict
|
||
"",
|
||
TypeError,
|
||
),
|
||
( # Invalid: Invalid label prefix
|
||
{"r!a!y.io/market_type": "spot"},
|
||
ValueError,
|
||
),
|
||
( # Invalid: Invalid label name
|
||
{"??==ags!": "true"},
|
||
ValueError,
|
||
),
|
||
( # Invalid: Invalid label value
|
||
{"ray.io/accelerator_type": "??==ags!"},
|
||
ValueError,
|
||
),
|
||
( # Invalid: non-supported label selector condition
|
||
{"ray.io/accelerator_type": "matches(TPU)"},
|
||
ValueError,
|
||
),
|
||
( # Invalid: in condition with incorrectly formatted string
|
||
{"ray.io/accelerator_type": "in(H100,, B200)"},
|
||
ValueError,
|
||
),
|
||
( # Invalid: unclosed parentheses in condition
|
||
{"ray.io/accelerator_type": "in(TPU, H100, B200"},
|
||
ValueError,
|
||
),
|
||
],
|
||
)
|
||
def test_decorator_label_selector_args(
|
||
ray_start_regular_shared, label_selector, expected_error
|
||
):
|
||
if expected_error:
|
||
with pytest.raises(expected_error):
|
||
|
||
@ray.remote(label_selector=label_selector) # noqa: F811
|
||
class Actor: # noqa: F811
|
||
def __init__(self):
|
||
pass
|
||
|
||
else:
|
||
|
||
@ray.remote(label_selector=label_selector) # noqa: F811
|
||
class Actor: # noqa: F811
|
||
def __init__(self):
|
||
pass
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
"fallback_strategy, expected_error",
|
||
[
|
||
( # Valid: single selector in the list
|
||
[{"label_selector": {"ray.io/accelerator-type": "H100"}}],
|
||
None,
|
||
),
|
||
( # Valid: multiple selectors in the list
|
||
[
|
||
{"label_selector": {"market-type": "spot"}},
|
||
{"label_selector": {"region": "in(us-west-1, us-east-1)"}},
|
||
],
|
||
None,
|
||
),
|
||
( # Invalid: unsupported `fallback_strategy` option.
|
||
[
|
||
{"memory": "1Gi"},
|
||
],
|
||
ValueError,
|
||
),
|
||
( # Invalid: not a list
|
||
{"label_selector": {"market-type": "spot"}},
|
||
TypeError,
|
||
),
|
||
( # Invalid: `fallback_strategy`` contains a non-dict element
|
||
["not-a-dict"],
|
||
ValueError,
|
||
),
|
||
( # Invalid: `label_selector` contains a dict with a bad key
|
||
[{"label_selector": {"-bad-key-": "value"}}],
|
||
ValueError,
|
||
),
|
||
( # Invalid: `label_selector` contains a dict with a bad value
|
||
[{"label_selector": {"key": "-bad-value-"}}],
|
||
ValueError,
|
||
),
|
||
],
|
||
)
|
||
def test_decorator_fallback_strategy_args(
|
||
ray_start_regular_shared, fallback_strategy, expected_error
|
||
):
|
||
"""
|
||
Tests that the fallback_strategy actor option is validated correctly.
|
||
"""
|
||
if expected_error:
|
||
with pytest.raises(expected_error):
|
||
|
||
@ray.remote(fallback_strategy=fallback_strategy)
|
||
class Actor:
|
||
def __init__(self):
|
||
pass
|
||
|
||
else:
|
||
|
||
@ray.remote(fallback_strategy=fallback_strategy)
|
||
class Actor:
|
||
def __init__(self):
|
||
pass
|
||
|
||
|
||
def test_random_id_generation(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Foo:
|
||
def __init__(self):
|
||
pass
|
||
|
||
# Make sure that seeding numpy does not interfere with the generation
|
||
# of actor IDs.
|
||
np.random.seed(1234)
|
||
random.seed(1234)
|
||
f1 = Foo.remote()
|
||
np.random.seed(1234)
|
||
random.seed(1234)
|
||
f2 = Foo.remote()
|
||
|
||
assert f1._actor_id != f2._actor_id
|
||
|
||
|
||
@pytest.mark.skipif(client_test_enabled(), reason="differing inheritence structure")
|
||
def test_actor_inheritance(ray_start_regular_shared):
|
||
class NonActorBase:
|
||
def __init__(self):
|
||
pass
|
||
|
||
# Test that an actor class can inherit from a non-actor class.
|
||
@ray.remote
|
||
class ActorBase(NonActorBase):
|
||
def __init__(self):
|
||
pass
|
||
|
||
# Test that you can't instantiate an actor class directly.
|
||
with pytest.raises(Exception, match="cannot be instantiated directly"):
|
||
ActorBase()
|
||
|
||
# Test that you can't inherit from an actor class.
|
||
with pytest.raises(
|
||
ActorClassInheritanceException,
|
||
match="Inheriting from actor classes is not currently supported.",
|
||
):
|
||
|
||
class Derived(ActorBase):
|
||
def __init__(self):
|
||
pass
|
||
|
||
|
||
def test_multiple_return_values(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Foo:
|
||
def method0(self):
|
||
return 1
|
||
|
||
@ray.method(num_returns=1)
|
||
def method1(self):
|
||
return 1
|
||
|
||
@ray.method(num_returns=2)
|
||
def method2(self):
|
||
return 1, 2
|
||
|
||
@ray.method(num_returns=3)
|
||
def method3(self):
|
||
return 1, 2, 3
|
||
|
||
f = Foo.remote()
|
||
|
||
id0 = f.method0.remote()
|
||
assert ray.get(id0) == 1
|
||
|
||
id1 = f.method1.remote()
|
||
assert ray.get(id1) == 1
|
||
|
||
id2a, id2b = f.method2.remote()
|
||
assert ray.get([id2a, id2b]) == [1, 2]
|
||
|
||
id3a, id3b, id3c = f.method3.remote()
|
||
assert ray.get([id3a, id3b, id3c]) == [1, 2, 3]
|
||
|
||
|
||
def test_options_num_returns(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Foo:
|
||
def method(self):
|
||
return 1, 2
|
||
|
||
f = Foo.remote()
|
||
|
||
obj = f.method.remote()
|
||
assert ray.get(obj) == (1, 2)
|
||
|
||
obj1, obj2 = f.method.options(num_returns=2).remote()
|
||
assert ray.get([obj1, obj2]) == [1, 2]
|
||
|
||
|
||
def test_options_no_ref_cycle(ray_start_regular_shared):
|
||
"""ActorMethod.options() must not prevent the ActorHandle from being freed
|
||
by reference counting alone. Regression test for #61922."""
|
||
import gc
|
||
import weakref
|
||
|
||
@ray.remote
|
||
class MyActor:
|
||
def task(self, x):
|
||
return x
|
||
|
||
actor = MyActor.remote()
|
||
weak = weakref.ref(actor)
|
||
|
||
ref = actor.task.options(num_returns=1).remote(42)
|
||
assert ray.get(ref) == 42
|
||
del ref
|
||
|
||
# With gc disabled, only reference counting can free the handle.
|
||
gc.disable()
|
||
try:
|
||
del actor
|
||
assert weak() is None, "ActorHandle leaked via reference cycle"
|
||
finally:
|
||
gc.enable()
|
||
|
||
|
||
@pytest.mark.skipif(
|
||
sys.platform == "win32", reason="Windows doesn't support changing process title."
|
||
)
|
||
def test_options_name(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Foo:
|
||
def method(self, name):
|
||
assert psutil.Process().cmdline()[0] == f"ray::{name}"
|
||
|
||
f = Foo.remote()
|
||
|
||
ray.get(f.method.options(name="foo").remote("foo"))
|
||
ray.get(f.method.options(name="bar").remote("bar"))
|
||
|
||
|
||
def test_define_actor(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Test:
|
||
def __init__(self, x):
|
||
self.x = x
|
||
|
||
def f(self, y):
|
||
return self.x + y
|
||
|
||
t = Test.remote(2)
|
||
assert ray.get(t.f.remote(1)) == 3
|
||
|
||
# Make sure that calling an actor method directly raises an exception.
|
||
with pytest.raises(Exception):
|
||
t.f(1)
|
||
|
||
|
||
def test_actor_deletion(ray_start_regular_shared):
|
||
# Make sure that when an actor handles goes out of scope, the actor
|
||
# destructor is called.
|
||
|
||
@ray.remote
|
||
class Actor:
|
||
def getpid(self):
|
||
return os.getpid()
|
||
|
||
a = Actor.remote()
|
||
pid = ray.get(a.getpid.remote())
|
||
a = None
|
||
wait_for_pid_to_exit(pid)
|
||
|
||
actors = [Actor.remote() for _ in range(10)]
|
||
pids = ray.get([a.getpid.remote() for a in actors])
|
||
a = None
|
||
actors = None
|
||
[wait_for_pid_to_exit(pid) for pid in pids]
|
||
|
||
|
||
def test_actor_method_deletion(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Actor:
|
||
def method(self):
|
||
return 1
|
||
|
||
# Make sure that if we create an actor and call a method on it
|
||
# immediately, the actor doesn't get killed before the method is
|
||
# called.
|
||
assert ray.get(Actor.remote().method.remote()) == 1
|
||
|
||
|
||
def test_distributed_actor_handle_deletion(ray_start_regular_shared):
|
||
@ray.remote
|
||
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())
|
||
|
||
SignalActor = create_remote_signal_actor(ray)
|
||
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)
|
||
|
||
|
||
def test_multiple_actors(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Counter:
|
||
def __init__(self, value):
|
||
self.value = value
|
||
|
||
def increase(self):
|
||
self.value += 1
|
||
return self.value
|
||
|
||
def reset(self):
|
||
self.value = 0
|
||
|
||
num_actors = 5
|
||
num_increases = 50
|
||
# Create multiple actors.
|
||
actors = [Counter.remote(i) for i in range(num_actors)]
|
||
results = []
|
||
# Call each actor's method a bunch of times.
|
||
for i in range(num_actors):
|
||
results += [actors[i].increase.remote() for _ in range(num_increases)]
|
||
result_values = ray.get(results)
|
||
for i in range(num_actors):
|
||
v = result_values[(num_increases * i) : (num_increases * (i + 1))]
|
||
assert v == list(range(i + 1, num_increases + i + 1))
|
||
|
||
# Reset the actor values.
|
||
[actor.reset.remote() for actor in actors]
|
||
|
||
# Interweave the method calls on the different actors.
|
||
results = []
|
||
for j in range(num_increases):
|
||
results += [actor.increase.remote() for actor in actors]
|
||
result_values = ray.get(results)
|
||
for j in range(num_increases):
|
||
v = result_values[(num_actors * j) : (num_actors * (j + 1))]
|
||
assert v == num_actors * [j + 1]
|
||
|
||
|
||
def test_inherit_actor_from_class(ray_start_regular_shared):
|
||
# Make sure we can define an actor by inheriting from a regular class.
|
||
# Note that actors cannot inherit from other actors.
|
||
|
||
class Foo:
|
||
def __init__(self, x):
|
||
self.x = x
|
||
|
||
def f(self):
|
||
return self.x
|
||
|
||
def g(self, y):
|
||
return self.x + y
|
||
|
||
@ray.remote
|
||
class Actor(Foo):
|
||
def __init__(self, x):
|
||
Foo.__init__(self, x)
|
||
|
||
def get_value(self):
|
||
return self.f()
|
||
|
||
actor = Actor.remote(1)
|
||
assert ray.get(actor.get_value.remote()) == 1
|
||
assert ray.get(actor.g.remote(5)) == 6
|
||
|
||
|
||
def test_get_non_existing_named_actor(ray_start_regular_shared):
|
||
with pytest.raises(ValueError):
|
||
_ = ray.get_actor("non_existing_actor")
|
||
|
||
|
||
# https://github.com/ray-project/ray/issues/17843
|
||
def test_actor_namespace(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Actor:
|
||
def f(self):
|
||
return "ok"
|
||
|
||
a = Actor.options(name="foo", namespace="f1").remote()
|
||
|
||
with pytest.raises(ValueError):
|
||
ray.get_actor(name="foo", namespace="f2")
|
||
|
||
a1 = ray.get_actor(name="foo", namespace="f1")
|
||
assert ray.get(a1.f.remote()) == "ok"
|
||
del a
|
||
|
||
|
||
def test_named_actor_cache(ray_start_regular_shared):
|
||
"""Verify that named actor cache works well."""
|
||
|
||
@ray.remote(max_restarts=-1)
|
||
class Counter:
|
||
def __init__(self):
|
||
self.count = 0
|
||
|
||
def inc_and_get(self):
|
||
self.count += 1
|
||
return self.count
|
||
|
||
a = Counter.options(name="hi").remote()
|
||
first_get = ray.get_actor("hi")
|
||
assert ray.get(first_get.inc_and_get.remote()) == 1
|
||
|
||
second_get = ray.get_actor("hi")
|
||
assert ray.get(second_get.inc_and_get.remote()) == 2
|
||
ray.kill(a, no_restart=True)
|
||
|
||
def actor_removed():
|
||
try:
|
||
ray.get_actor("hi")
|
||
return False
|
||
except ValueError:
|
||
return True
|
||
|
||
wait_for_condition(actor_removed)
|
||
|
||
get_after_restart = Counter.options(name="hi").remote()
|
||
assert ray.get(get_after_restart.inc_and_get.remote()) == 1
|
||
get_by_name = ray.get_actor("hi")
|
||
assert ray.get(get_by_name.inc_and_get.remote()) == 2
|
||
|
||
|
||
def test_named_actor_cache_via_another_actor(ray_start_regular_shared):
|
||
"""Verify that named actor cache works well with another actor."""
|
||
|
||
@ray.remote(max_restarts=0)
|
||
class Counter:
|
||
def __init__(self):
|
||
self.count = 0
|
||
|
||
def inc_and_get(self):
|
||
self.count += 1
|
||
return self.count
|
||
|
||
# The third actor to get named actor. To indicates this cache doesn't
|
||
# break getting from the third party.
|
||
@ray.remote(max_restarts=0)
|
||
class ActorGetter:
|
||
def get_actor_count(self, name):
|
||
actor = ray.get_actor(name)
|
||
return ray.get(actor.inc_and_get.remote())
|
||
|
||
# Start a actor and get it by name in driver.
|
||
a = Counter.options(name="foo").remote()
|
||
first_get = ray.get_actor("foo")
|
||
assert ray.get(first_get.inc_and_get.remote()) == 1
|
||
|
||
# Start another actor as the third actor to get named actor.
|
||
actor_getter = ActorGetter.remote()
|
||
assert ray.get(actor_getter.get_actor_count.remote("foo")) == 2
|
||
ray.kill(a, no_restart=True)
|
||
|
||
def actor_removed():
|
||
try:
|
||
ray.get_actor("foo")
|
||
return False
|
||
except ValueError:
|
||
return True
|
||
|
||
wait_for_condition(actor_removed)
|
||
|
||
# Restart the named actor.
|
||
get_after_restart = Counter.options(name="foo").remote()
|
||
assert ray.get(get_after_restart.inc_and_get.remote()) == 1
|
||
# Get the named actor from the third actor again.
|
||
assert ray.get(actor_getter.get_actor_count.remote("foo")) == 2
|
||
# Get the named actor by name in driver again.
|
||
get_by_name = ray.get_actor("foo")
|
||
assert ray.get(get_by_name.inc_and_get.remote()) == 3
|
||
|
||
|
||
def test_wrapped_actor_handle(ray_start_regular_shared):
|
||
@ray.remote
|
||
class B:
|
||
def doit(self):
|
||
return 2
|
||
|
||
@ray.remote
|
||
class A:
|
||
def __init__(self):
|
||
self.b = B.remote()
|
||
|
||
def get_actor_ref(self):
|
||
return [self.b]
|
||
|
||
a = A.remote()
|
||
b_list = ray.get(a.get_actor_ref.remote())
|
||
assert ray.get(b_list[0].doit.remote()) == 2
|
||
|
||
|
||
@pytest.mark.parametrize("enable_concurrency_group", [True, False])
|
||
@pytest.mark.parametrize(
|
||
"exit_condition",
|
||
[
|
||
# "out_of_scope", TODO(edoakes): enable this once fixed.
|
||
"__ray_terminate__",
|
||
"ray.actor.exit_actor",
|
||
"ray.kill",
|
||
],
|
||
)
|
||
def test_atexit_handler(
|
||
ray_start_regular_shared, exit_condition, enable_concurrency_group
|
||
):
|
||
concurrency_groups = {"io": 1} if enable_concurrency_group else None
|
||
|
||
@ray.remote(concurrency_groups=concurrency_groups)
|
||
class A:
|
||
def __init__(self, tmpfile, data):
|
||
import atexit
|
||
|
||
def f():
|
||
with open(tmpfile, "w") as f:
|
||
f.write(data)
|
||
f.flush()
|
||
|
||
atexit.register(f)
|
||
|
||
def ready(self):
|
||
pass
|
||
|
||
def exit(self):
|
||
ray.actor.exit_actor()
|
||
|
||
data = "hello"
|
||
tmpfile = tempfile.NamedTemporaryFile("w+", suffix=".tmp", delete=False)
|
||
tmpfile.close()
|
||
|
||
a = A.remote(tmpfile.name, data)
|
||
ray.get(a.ready.remote())
|
||
|
||
if exit_condition == "out_of_scope":
|
||
del a
|
||
elif exit_condition == "__ray_terminate__":
|
||
ray.wait([a.__ray_terminate__.remote()])
|
||
elif exit_condition == "ray.actor.exit_actor":
|
||
ray.wait([a.exit.remote()])
|
||
elif exit_condition == "ray.kill":
|
||
ray.kill(a)
|
||
else:
|
||
assert False, "Unrecognized condition"
|
||
|
||
def check_file_written():
|
||
with open(tmpfile.name, "r") as f:
|
||
if f.read() == data:
|
||
return True
|
||
return False
|
||
|
||
# ray.kill() should not trigger atexit handlers, all other methods should.
|
||
if exit_condition == "ray.kill":
|
||
assert not check_file_written()
|
||
else:
|
||
wait_for_condition(check_file_written)
|
||
|
||
os.unlink(tmpfile.name)
|
||
|
||
|
||
def test_actor_ready(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Actor:
|
||
pass
|
||
|
||
actor = Actor.remote()
|
||
|
||
with pytest.raises(TypeError):
|
||
# Method can't be called directly
|
||
actor.__ray_ready__()
|
||
|
||
assert ray.get(actor.__ray_ready__.remote())
|
||
|
||
|
||
def test_actor_generic_call(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Actor:
|
||
pass
|
||
|
||
actor = Actor.remote()
|
||
|
||
with pytest.raises(TypeError):
|
||
# Method can't be called directly
|
||
actor.__ray_call__()
|
||
|
||
assert ray.get(actor.__ray_call__.remote(lambda self: 4)) == 4
|
||
assert ray.get(actor.__ray_call__.remote(lambda self, x: x * 2, 2)) == 4
|
||
assert ray.get(actor.__ray_call__.remote(lambda self, x: x * 2, x=2)) == 4
|
||
|
||
|
||
def test_ray_call_with_state_access(ray_start_regular_shared):
|
||
"""Test that __ray_call__ can read and mutate actor state via closure."""
|
||
|
||
@ray.remote
|
||
class Store:
|
||
def __init__(self):
|
||
self.data = {}
|
||
self.counter = 0
|
||
|
||
def increment(self):
|
||
self.counter += 1
|
||
|
||
actor = Store.remote()
|
||
ray.get(actor.increment.remote())
|
||
ray.get(actor.increment.remote())
|
||
|
||
# Read state via closure
|
||
count = ray.get(actor.__ray_call__.remote(lambda self: self.counter))
|
||
assert count == 2
|
||
|
||
# Mutate state via closure
|
||
ray.get(actor.__ray_call__.remote(lambda self: self.data.update({"key": "value"})))
|
||
result = ray.get(actor.__ray_call__.remote(lambda self: self.data))
|
||
assert result == {"key": "value"}
|
||
|
||
|
||
def test_ray_call_with_extra_args(ray_start_regular_shared):
|
||
"""Test that __ray_call__ correctly forwards *args and **kwargs to fn."""
|
||
|
||
@ray.remote
|
||
class Calculator:
|
||
def __init__(self):
|
||
self.base = 10
|
||
|
||
actor = Calculator.remote()
|
||
|
||
# Test *args forwarding
|
||
result = ray.get(
|
||
actor.__ray_call__.remote(lambda self, x, y: self.base + x + y, 1, 2)
|
||
)
|
||
assert result == 13 # 10 + 1 + 2
|
||
|
||
# Test **kwargs forwarding
|
||
result = ray.get(
|
||
actor.__ray_call__.remote(
|
||
lambda self, multiplier=1: self.base * multiplier,
|
||
multiplier=3,
|
||
)
|
||
)
|
||
assert result == 30 # 10 * 3
|
||
|
||
|
||
def test_return_actor_handle_from_actor(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Inner:
|
||
def ping(self):
|
||
return "pong"
|
||
|
||
@ray.remote
|
||
class Outer:
|
||
def __init__(self):
|
||
self.inner = Inner.remote()
|
||
|
||
def get_ref(self):
|
||
return self.inner
|
||
|
||
outer = Outer.remote()
|
||
inner = ray.get(outer.get_ref.remote())
|
||
assert ray.get(inner.ping.remote()) == "pong"
|
||
|
||
|
||
def test_actor_autocomplete(ray_start_regular_shared):
|
||
"""
|
||
Test that autocomplete works with actors by checking that the builtin dir()
|
||
function works as expected.
|
||
"""
|
||
|
||
@ray.remote
|
||
class Foo:
|
||
def method_one(self) -> None:
|
||
pass
|
||
|
||
class_calls = [fn for fn in dir(Foo) if not fn.startswith("_")]
|
||
|
||
assert set(class_calls) == {"method_one", "options", "remote", "bind"}
|
||
|
||
f = Foo.remote()
|
||
|
||
methods = [fn for fn in dir(f) if not fn.startswith("_")]
|
||
assert methods == ["method_one"]
|
||
|
||
all_methods = set(dir(f))
|
||
assert all_methods == {
|
||
"__init__",
|
||
"method_one",
|
||
"__ray_ready__",
|
||
"__ray_call__",
|
||
"__ray_terminate__",
|
||
}
|
||
|
||
method_options = [fn for fn in dir(f.method_one) if not fn.startswith("_")]
|
||
|
||
if client_test_enabled():
|
||
assert set(method_options) == {"options", "remote"}
|
||
else:
|
||
assert set(method_options) == {"options", "remote", "bind"}
|
||
|
||
|
||
def test_actor_mro(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Foo:
|
||
def __init__(self, x):
|
||
self.x = x
|
||
|
||
@classmethod
|
||
def factory_f(cls, x):
|
||
return cls(x)
|
||
|
||
def get_x(self):
|
||
return self.x
|
||
|
||
obj = Foo.factory_f(1)
|
||
assert obj.get_x() == 1
|
||
|
||
|
||
@pytest.mark.skipif(client_test_enabled(), reason="differing deletion behaviors")
|
||
def test_keep_calling_get_actor(ray_start_regular_shared):
|
||
"""
|
||
Test keep calling get_actor.
|
||
"""
|
||
|
||
@ray.remote
|
||
class Actor:
|
||
def hello(self):
|
||
return "hello"
|
||
|
||
actor = Actor.options(name="ABC").remote()
|
||
assert ray.get(actor.hello.remote()) == "hello"
|
||
|
||
# Getting the actor by name acts as a weakref.
|
||
for _ in range(10):
|
||
named_actor = ray.get_actor("ABC")
|
||
assert ray.get(named_actor.hello.remote()) == "hello"
|
||
|
||
del actor
|
||
|
||
# Verify the actor is killed
|
||
def actor_removed():
|
||
try:
|
||
ray.get_actor("ABC")
|
||
return False
|
||
except ValueError:
|
||
return True
|
||
|
||
wait_for_condition(actor_removed)
|
||
|
||
|
||
@pytest.mark.skipif(client_test_enabled(), reason="internal api")
|
||
@pytest.mark.parametrize(
|
||
"actor_type",
|
||
[
|
||
"actor",
|
||
"threaded_actor",
|
||
"async_actor",
|
||
],
|
||
)
|
||
def test_actor_parent_task_correct(shutdown_only, actor_type):
|
||
"""Verify the parent task id is correct for all actors."""
|
||
|
||
@ray.remote
|
||
def child():
|
||
pass
|
||
|
||
@ray.remote
|
||
class ChildActor:
|
||
def child(self):
|
||
pass
|
||
|
||
def parent_func(child_actor):
|
||
core_worker = ray._private.worker.global_worker.core_worker
|
||
refs = [child_actor.child.remote(), child.remote()]
|
||
expected = {ref.task_id().hex() for ref in refs}
|
||
task_id_hex = ray.get_runtime_context().get_task_id()
|
||
task_id = ray.TaskID(hex_to_binary(task_id_hex))
|
||
children_task_ids = core_worker.get_pending_children_task_ids(task_id)
|
||
actual = {task_id.hex() for task_id in children_task_ids}
|
||
ray.get(refs)
|
||
return expected, actual
|
||
|
||
if actor_type == "actor":
|
||
|
||
@ray.remote
|
||
class Actor:
|
||
def parent(self, child_actor):
|
||
return parent_func(child_actor)
|
||
|
||
@ray.remote
|
||
class GeneratorActor:
|
||
def parent(self, child_actor):
|
||
yield parent_func(child_actor)
|
||
|
||
if actor_type == "threaded_actor":
|
||
|
||
@ray.remote(max_concurrency=5)
|
||
class Actor: # noqa
|
||
def parent(self, child_actor):
|
||
return parent_func(child_actor)
|
||
|
||
@ray.remote(max_concurrency=5)
|
||
class GeneratorActor: # noqa
|
||
def parent(self, child_actor):
|
||
yield parent_func(child_actor)
|
||
|
||
if actor_type == "async_actor":
|
||
|
||
@ray.remote
|
||
class Actor: # noqa
|
||
async def parent(self, child_actor):
|
||
return parent_func(child_actor)
|
||
|
||
@ray.remote
|
||
class GeneratorActor: # noqa
|
||
async def parent(self, child_actor):
|
||
yield parent_func(child_actor)
|
||
|
||
# Verify a regular actor.
|
||
actor = Actor.remote()
|
||
child_actor = ChildActor.remote()
|
||
actual, expected = ray.get(actor.parent.remote(child_actor))
|
||
assert actual == expected
|
||
# return True
|
||
|
||
# Verify a generator actor
|
||
actor = GeneratorActor.remote()
|
||
child_actor = ChildActor.remote()
|
||
gen = actor.parent.remote(child_actor)
|
||
for ref in gen:
|
||
result = ray.get(ref)
|
||
actual, expected = result
|
||
assert actual == expected
|
||
|
||
|
||
@pytest.mark.skipif(client_test_enabled(), reason="internal api")
|
||
def test_parent_task_correct_concurrent_async_actor(shutdown_only):
|
||
"""Make sure when there are concurrent async tasks
|
||
the parent -> children task ids are properly mapped.
|
||
"""
|
||
sig = SignalActor.remote()
|
||
|
||
@ray.remote
|
||
def child(sig):
|
||
ray.get(sig.wait.remote())
|
||
|
||
@ray.remote
|
||
class AsyncActor:
|
||
async def f(self, sig):
|
||
refs = [child.remote(sig) for _ in range(2)]
|
||
core_worker = ray._private.worker.global_worker.core_worker
|
||
expected = {ref.task_id().hex() for ref in refs}
|
||
task_id_hex = ray.get_runtime_context().get_task_id()
|
||
task_id = ray.TaskID(hex_to_binary(task_id_hex))
|
||
children_task_ids = core_worker.get_pending_children_task_ids(task_id)
|
||
actual = {task_id.hex() for task_id in children_task_ids}
|
||
await sig.wait.remote()
|
||
ray.get(refs)
|
||
return actual, expected
|
||
|
||
a = AsyncActor.remote()
|
||
# Run 3 concurrent tasks.
|
||
refs = [a.f.remote(sig) for _ in range(20)]
|
||
# 3 concurrent task will finish.
|
||
ray.get(sig.send.remote())
|
||
# Verify children task mapping is correct.
|
||
result = ray.get(refs)
|
||
for actual, expected in result:
|
||
assert actual, expected
|
||
|
||
|
||
def test_actor_hash(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Actor:
|
||
...
|
||
|
||
origin = Actor.remote()
|
||
|
||
@ray.remote
|
||
def get_actor(actor):
|
||
return actor
|
||
|
||
remote = ray.get(get_actor.remote(origin))
|
||
assert hash(origin) == hash(remote)
|
||
|
||
|
||
def test_actor_equal(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Actor:
|
||
...
|
||
|
||
origin = Actor.remote()
|
||
assert origin != 1
|
||
|
||
@ray.remote
|
||
def get_actor(actor):
|
||
return actor
|
||
|
||
remote = ray.get(get_actor.remote(origin))
|
||
assert origin == remote
|
||
|
||
|
||
@pytest.mark.parametrize("cross_language", [False, True], ids=["python", "cross_lang"])
|
||
def test_actor_handle_hash_eq(ray_start_regular_shared, cross_language):
|
||
"""hash()/eq/set/dict ops must work for both Python and cross-language handles."""
|
||
|
||
@ray.remote
|
||
class Actor:
|
||
pass
|
||
|
||
handle = Actor.remote()
|
||
if cross_language:
|
||
handle._ray_is_cross_language = True
|
||
|
||
h = hash(handle)
|
||
assert isinstance(h, int)
|
||
assert hash(handle) == h
|
||
|
||
assert handle == handle
|
||
assert handle in {handle}
|
||
assert {handle: "v"}[handle] == "v"
|
||
|
||
|
||
def test_actor_handle_weak_ref_counting(ray_start_regular_shared):
|
||
"""
|
||
Actors can get handles to themselves or to named actors but these count
|
||
only as weak refs. Check that this pattern does not crash the normal ref
|
||
counting protocol, which tracks handles passed through task args and return
|
||
values.
|
||
"""
|
||
|
||
@ray.remote
|
||
class WeakReferenceHolder:
|
||
def pass_weak_ref(self, handle):
|
||
self.handle = handle
|
||
|
||
@ray.remote
|
||
class Actor:
|
||
def read_self_handle(self, self_handle):
|
||
# This actor has a strong reference to itself through the arg
|
||
# self_handle.
|
||
|
||
# Get and delete a weak reference to ourselves. This should not
|
||
# crash the distributed ref counting protocol.
|
||
# TODO(swang): Commenting these lines out currently causes the
|
||
# actor handle to leak.
|
||
weak_self_handle = ray.get_runtime_context().current_actor
|
||
del weak_self_handle
|
||
|
||
def pass_self_handle(self, self_handle, weak_ref_holder):
|
||
# This actor has a strong reference to itself through the arg
|
||
# self_handle.
|
||
|
||
# Pass a weak reference to ourselves to another actor. This should
|
||
# not count towards the distributed ref counting protocol.
|
||
weak_self_handle = ray.get_runtime_context().current_actor
|
||
ray.get(weak_ref_holder.pass_weak_ref.remote(weak_self_handle))
|
||
|
||
def read_handle_by_name(self, handle, name):
|
||
# This actor has a strong reference to another actor through the
|
||
# arg handle.
|
||
|
||
# Get and delete a weak reference to the same actor as the one
|
||
# passed through handle. This should not crash the distributed ref
|
||
# counting protocol.
|
||
weak_handle = ray.get_actor(name=name)
|
||
del weak_handle
|
||
|
||
def pass_named_handle(self, handle, name, weak_ref_holder):
|
||
# This actor has a strong reference to another actor through the
|
||
# arg handle.
|
||
|
||
# Pass a weak reference to the actor to another actor. This should
|
||
# not count towards the distributed ref counting protocol.
|
||
weak_handle = ray.get_actor(name=name)
|
||
ray.get(weak_ref_holder.pass_weak_ref.remote(weak_handle))
|
||
|
||
def getpid(self):
|
||
return os.getpid()
|
||
|
||
# Check ref counting when getting actors via self handle.
|
||
a = Actor.remote()
|
||
pid = ray.get(a.getpid.remote())
|
||
for _ in range(3):
|
||
ray.get(a.read_self_handle.remote(a))
|
||
# Check that there are no leaks after all handles have gone out of scope.
|
||
a = None
|
||
wait_for_pid_to_exit(pid)
|
||
|
||
# Check that passing a weak ref to the self actor to other actors does not
|
||
# count towards the ref count.
|
||
weak_ref_holder = WeakReferenceHolder.remote()
|
||
a = Actor.remote()
|
||
pid = ray.get(a.getpid.remote())
|
||
for _ in range(3):
|
||
ray.get(a.pass_self_handle.remote(a, weak_ref_holder))
|
||
# Check that there are no leaks after all strong refs have gone out of
|
||
# scope.
|
||
a = None
|
||
wait_for_pid_to_exit(pid)
|
||
|
||
# Check ref counting when getting actors by name.
|
||
a = Actor.remote()
|
||
b = Actor.options(name="actor").remote()
|
||
pid = ray.get(b.getpid.remote())
|
||
for _ in range(3):
|
||
ray.get(a.read_handle_by_name.remote(b, "actor"))
|
||
# Check that there are no leaks after all handles have gone out of scope.
|
||
b = None
|
||
wait_for_pid_to_exit(pid)
|
||
|
||
# Check that passing a weak ref to an actor handle that was gotten by name
|
||
# to other actors does not count towards the ref count.
|
||
a = Actor.remote()
|
||
b = Actor.options(name="actor").remote()
|
||
pid = ray.get(b.getpid.remote())
|
||
for _ in range(3):
|
||
ray.get(a.pass_named_handle.remote(b, "actor", weak_ref_holder))
|
||
# Check that there are no leaks after all strong refs have gone out of
|
||
# scope.
|
||
b = None
|
||
wait_for_pid_to_exit(pid)
|
||
|
||
|
||
def test_self_handle_leak(ray_start_regular_shared):
|
||
"""
|
||
Actors can get handles to themselves. Check that holding such a reference
|
||
does not cause the actor to leak.
|
||
"""
|
||
|
||
@ray.remote
|
||
class Actor:
|
||
def read_self_handle(self, self_handle):
|
||
pass
|
||
|
||
def getpid(self):
|
||
return os.getpid()
|
||
|
||
# Check ref counting when getting actors via self handle.
|
||
a = Actor.remote()
|
||
pid = ray.get(a.getpid.remote())
|
||
for _ in range(3):
|
||
ray.get(a.read_self_handle.remote(a))
|
||
# Check that there are no leaks after all handles have gone out of scope.
|
||
a = None
|
||
wait_for_pid_to_exit(pid)
|
||
|
||
|
||
@pytest.mark.skipif(client_test_enabled(), reason="internal api")
|
||
def test_get_local_actor_state(ray_start_regular_shared):
|
||
@ray.remote
|
||
class Actor:
|
||
def ping(self):
|
||
pass
|
||
|
||
actor = Actor.remote()
|
||
ray.get(actor.ping.remote())
|
||
assert actor._get_local_state() == gcs_pb2.ActorTableData.ActorState.ALIVE
|
||
ray.kill(actor)
|
||
wait_for_condition(
|
||
lambda: actor._get_local_state() == gcs_pb2.ActorTableData.ActorState.DEAD
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize("exit_type", ["ray.kill", "out_of_scope"])
|
||
def test_exit_immediately_after_creation(ray_start_regular_shared, exit_type: str):
|
||
if client_test_enabled() and exit_type == "out_of_scope":
|
||
pytest.skip("out_of_scope actor cleanup doesn't work with Ray client.")
|
||
|
||
@ray.remote
|
||
class A:
|
||
pass
|
||
|
||
a = A.remote()
|
||
a_id = a._actor_id.hex()
|
||
b = A.remote()
|
||
b_id = b._actor_id.hex()
|
||
|
||
def _num_actors_alive() -> int:
|
||
still_alive = list(
|
||
filter(
|
||
lambda a: a.actor_id in {a_id, b_id},
|
||
list_actors(filters=[("state", "=", "ALIVE")]),
|
||
)
|
||
)
|
||
print(still_alive)
|
||
return len(still_alive)
|
||
|
||
wait_for_condition(lambda: _num_actors_alive() == 2)
|
||
|
||
if exit_type == "ray.kill":
|
||
ray.kill(a)
|
||
ray.kill(b)
|
||
elif exit_type == "out_of_scope":
|
||
del a
|
||
del b
|
||
else:
|
||
pytest.fail(f"Unrecognized exit_type: '{exit_type}'.")
|
||
|
||
wait_for_condition(lambda: _num_actors_alive() == 0)
|
||
|
||
|
||
def test_one_liner_actor_method_invocation(shutdown_only):
|
||
@ray.remote
|
||
class Foo:
|
||
def method(self):
|
||
return "ok"
|
||
|
||
# This one‐liner used to fail with “Lost reference to actor”.
|
||
# Now it should succeed and return our value.
|
||
# See https://github.com/ray-project/ray/pull/53178
|
||
result = ray.get(Foo.remote().method.remote())
|
||
assert result == "ok"
|
||
|
||
|
||
@pytest.mark.skipif(
|
||
client_test_enabled(),
|
||
reason="Out of scope actor cleanup doesn't work with Ray client.",
|
||
)
|
||
def test_get_actor_after_same_name_actor_dead(shutdown_only):
|
||
ACTOR_NAME = "test_actor"
|
||
NAMESPACE_NAME = "test_namespace"
|
||
|
||
ray.init(namespace=NAMESPACE_NAME)
|
||
|
||
@ray.remote
|
||
class Actor:
|
||
def get_pid(self):
|
||
return os.getpid()
|
||
|
||
a = Actor.options(name=ACTOR_NAME, max_restarts=0, max_task_retries=-1).remote()
|
||
|
||
pid = ray.get(a.get_pid.remote())
|
||
psutil.Process(pid).kill()
|
||
a_actor_id = a._actor_id.hex()
|
||
|
||
wait_for_condition(lambda: ray.util.state.get_actor(id=a_actor_id).state == "DEAD")
|
||
|
||
# When a reference is held, the name cannot be reused.
|
||
with pytest.raises(ValueError):
|
||
Actor.options(name=ACTOR_NAME).remote()
|
||
|
||
# Deleting the remaining reference so the name can be reused
|
||
del a
|
||
|
||
b = None
|
||
|
||
def wait_new_actor_ready():
|
||
nonlocal b
|
||
b = Actor.options(name=ACTOR_NAME).remote()
|
||
return True
|
||
|
||
wait_for_condition(wait_new_actor_ready)
|
||
|
||
ray.get(b.__ray_ready__.remote())
|
||
_ = ray.get_actor(ACTOR_NAME, namespace=NAMESPACE_NAME)
|
||
|
||
# ray.kill can proactively release the name.
|
||
ray.kill(b)
|
||
wait_for_condition(
|
||
lambda: ray.util.state.get_actor(id=b._actor_id.hex()).state == "DEAD"
|
||
)
|
||
|
||
c = Actor.options(name=ACTOR_NAME, lifetime="detached").remote()
|
||
ray.get(c.__ray_ready__.remote())
|
||
_ = ray.get_actor(ACTOR_NAME, namespace=NAMESPACE_NAME)
|
||
|
||
pid = ray.get(c.get_pid.remote())
|
||
psutil.Process(pid).kill()
|
||
|
||
wait_for_condition(
|
||
lambda: ray.util.state.get_actor(id=c._actor_id.hex()).state == "DEAD"
|
||
)
|
||
|
||
# Detached actors do not subscribe to reference counting, so
|
||
# they release the actor name when the actor is dead, without waiting for the reference count
|
||
# to be released or the execution of ray.kill.
|
||
d = Actor.options(name=ACTOR_NAME).remote()
|
||
ray.get(d.__ray_ready__.remote())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(pytest.main(["-sv", __file__]))
|