# coding: utf-8 import array import logging import os import random import subprocess import sys import tempfile import threading import time from unittest.mock import MagicMock, patch import pytest from ray._private.ray_constants import KV_NAMESPACE_FUNCTION_TABLE from ray._private.test_utils import client_test_enabled from ray.cluster_utils import Cluster, cluster_not_supported from ray.exceptions import GetTimeoutError, RayTaskError from ray.tests.client_test_utils import create_remote_signal_actor if client_test_enabled(): from ray.util.client import ray else: import ray logger = logging.getLogger(__name__) def test_variable_number_of_args(shutdown_only): ray.init(num_cpus=1) @ray.remote def varargs_fct1(*a): return " ".join(map(str, a)) @ray.remote def varargs_fct2(a, *b): return " ".join(map(str, b)) x = varargs_fct1.remote(0, 1, 2) assert ray.get(x) == "0 1 2" x = varargs_fct2.remote(0, 1, 2) assert ray.get(x) == "1 2" @ray.remote def f1(*args): return args @ray.remote def f2(x, y, *args): return x, y, args assert ray.get(f1.remote()) == () assert ray.get(f1.remote(1)) == (1,) assert ray.get(f1.remote(1, 2, 3)) == (1, 2, 3) with pytest.raises(TypeError): f2.remote() with pytest.raises(TypeError): f2.remote(1) assert ray.get(f2.remote(1, 2)) == (1, 2, ()) assert ray.get(f2.remote(1, 2, 3)) == (1, 2, (3,)) assert ray.get(f2.remote(1, 2, 3, 4)) == (1, 2, (3, 4)) def testNoArgs(self): @ray.remote def no_op(): pass self.ray_start() ray.get(no_op.remote()) def test_defining_remote_functions(shutdown_only): ray.init(num_cpus=3) # Test that we can close over plain old data. data = [ (1, 2, "a"), [0.0, 1.0, 1 << 62], 1 << 60, {"a": bytes(3)}, ] @ray.remote def g(): return data ray.get(g.remote()) # Test that we can close over modules. @ray.remote def h(): return array.array("d", [1.0, 2.0, 3.0]) assert ray.get(h.remote()) == array.array("d", [1.0, 2.0, 3.0]) @ray.remote def j(): return time.time() ray.get(j.remote()) # Test that we can define remote functions that call other remote # functions. @ray.remote def k(x): return x + 1 @ray.remote def k2(x): return ray.get(k.remote(x)) @ray.remote def m(x): return ray.get(k2.remote(x)) assert ray.get(k.remote(1)) == 2 assert ray.get(k2.remote(1)) == 2 assert ray.get(m.remote(1)) == 2 def test_redefining_remote_functions(shutdown_only): ray.init(num_cpus=1) # Test that we can define a remote function in the shell. @ray.remote def f(x): return x + 1 assert ray.get(f.remote(0)) == 1 # Test that we can redefine the remote function. @ray.remote def f(x): return x + 10 while True: val = ray.get(f.remote(0)) assert val in [1, 10] if val == 10: break else: logger.info("Still using old definition of f, trying again.") # Check that we can redefine functions even when the remote function source # doesn't change (see https://github.com/ray-project/ray/issues/6130). @ray.remote def g(): return nonexistent() with pytest.raises(RayTaskError, match="nonexistent"): ray.get(g.remote()) def nonexistent(): return 1 # Redefine the function and make sure it succeeds. @ray.remote def g(): return nonexistent() assert ray.get(g.remote()) == 1 # Check the same thing but when the redefined function is inside of another # task. @ray.remote def h(i): @ray.remote def j(): return i return j.remote() for i in range(20): assert ray.get(ray.get(h.remote(i))) == i def test_call_matrix(shutdown_only): ray.init(object_store_memory=1000 * 1024 * 1024) @ray.remote class Actor: def small_value(self): return 0 def large_value(self): return bytes(80 * 1024 * 1024) def echo(self, x): if isinstance(x, list): x = ray.get(x[0]) return x @ray.remote def small_value(): return 0 @ray.remote def large_value(): return bytes(80 * 1024 * 1024) @ray.remote def echo(x): if isinstance(x, list): x = ray.get(x[0]) return x def check(source_actor, dest_actor, is_large, out_of_band): print( "CHECKING", "actor" if source_actor else "task", "to", "actor" if dest_actor else "task", "large_object" if is_large else "small_object", "out_of_band" if out_of_band else "in_band", ) if source_actor: a = Actor.remote() if is_large: x_id = a.large_value.remote() else: x_id = a.small_value.remote() else: if is_large: x_id = large_value.remote() else: x_id = small_value.remote() if out_of_band: x_id = [x_id] if dest_actor: b = Actor.remote() x = ray.get(b.echo.remote(x_id)) else: x = ray.get(echo.remote(x_id)) if is_large: assert isinstance(x, bytes) else: assert isinstance(x, int) for is_large in [False, True]: for source_actor in [False, True]: for dest_actor in [False, True]: for out_of_band in [False, True]: check(source_actor, dest_actor, is_large, out_of_band) def test_actor_call_order(shutdown_only): ray.init(num_cpus=4) @ray.remote def small_value(): time.sleep(0.01 * random.randint(0, 10)) return 0 @ray.remote class Actor: def __init__(self): self.count = 0 def inc(self, count, dependency): assert count == self.count self.count += 1 return count a = Actor.remote() assert ray.get([a.inc.remote(i, small_value.remote()) for i in range(100)]) == list( range(100) ) def test_actor_pass_by_ref_order_optimization(shutdown_only): ray.init(num_cpus=4) @ray.remote class Actor: def __init__(self): pass def f(self, x): pass a = Actor.remote() @ray.remote def fast_value(): print("fast value") pass @ray.remote def slow_value(): print("start sleep") time.sleep(30) @ray.remote def runner(f): print("runner", a, f) return ray.get(a.f.remote(f.remote())) runner.remote(slow_value) time.sleep(1) x2 = runner.remote(fast_value) start = time.time() ray.get(x2) delta = time.time() - start assert delta < 10, "did not skip slow value" @pytest.mark.parametrize( "ray_start_cluster", [ { "num_cpus": 1, "num_nodes": 1, }, { "num_cpus": 1, "num_nodes": 2, }, ], indirect=True, ) def test_call_chain(ray_start_cluster): @ray.remote def g(x): return x + 1 x = 0 for _ in range(100): x = g.remote(x) assert ray.get(x) == 100 @pytest.mark.xfail(cluster_not_supported, reason="cluster not supported") @pytest.mark.skipif(client_test_enabled(), reason="init issue") def test_system_config_when_connecting(ray_start_cluster): config = {"object_timeout_milliseconds": 200} cluster = Cluster() cluster.add_node(_system_config=config, object_store_memory=100 * 1024 * 1024) cluster.wait_for_nodes() # Specifying _system_config when connecting to a cluster is disallowed. with pytest.raises(ValueError): ray.init(address=cluster.address, _system_config=config) # Check that the config was picked up (object pinning is disabled). ray.init(address=cluster.address) obj_ref = ray.put(bytes(40 * 1024 * 1024)) for _ in range(5): put_ref = ray.put(bytes(40 * 1024 * 1024)) del put_ref ray.get(obj_ref) def test_get_multiple(ray_start_regular_shared): object_refs = [ray.put(i) for i in range(10)] assert ray.get(object_refs) == list(range(10)) # Get a random choice of object refs with duplicates. indices = [random.choice(range(10)) for i in range(5)] indices += indices results = ray.get([object_refs[i] for i in indices]) assert results == indices def test_get_with_timeout(ray_start_regular_shared): SignalActor = create_remote_signal_actor(ray) signal = SignalActor.remote() # Check that get() returns early if object is ready. start = time.time() ray.get(signal.wait.remote(should_wait=False), timeout=30) assert time.time() - start < 30 # Check that get() raises a TimeoutError after the timeout if the object # is not ready yet. result_id = signal.wait.remote() with pytest.raises(GetTimeoutError): ray.get(result_id, timeout=0.1) assert issubclass(GetTimeoutError, TimeoutError) with pytest.raises(TimeoutError): ray.get(result_id, timeout=0.1) # timeout of 0 should raise an error with pytest.raises(GetTimeoutError): ray.get(result_id, timeout=0) # Check that a subsequent get() returns early. ray.get(signal.send.remote()) start = time.time() ray.get(result_id, timeout=30) assert time.time() - start < 30 # https://github.com/ray-project/ray/issues/6329 def test_call_actors_indirect_through_tasks(ray_start_regular_shared): @ray.remote class Counter: def __init__(self, value): self.value = int(value) def increase(self, delta): self.value += int(delta) return self.value @ray.remote def foo(object): return ray.get(object.increase.remote(1)) @ray.remote def bar(object): return ray.get(object.increase.remote(1)) @ray.remote def zoo(object): return ray.get(object[0].increase.remote(1)) c = Counter.remote(0) for _ in range(0, 100): ray.get(foo.remote(c)) ray.get(bar.remote(c)) ray.get(zoo.remote([c])) def test_inline_arg_memory_corruption(ray_start_regular_shared): @ray.remote def f(): return bytes(1000) @ray.remote class Actor: def __init__(self): self.z = [] def add(self, x): self.z.append(x) for prev in self.z: assert sum(prev) == 0, ("memory corruption detected", prev) a = Actor.remote() for i in range(100): ray.get(a.add.remote(f.remote())) @pytest.mark.skipif(client_test_enabled(), reason="internal api") def test_skip_plasma(ray_start_regular_shared): @ray.remote class Actor: def __init__(self): pass def f(self, x): return x * 2 a = Actor.remote() obj_ref = a.f.remote(1) # it is not stored in plasma assert not ray._private.worker.global_worker.core_worker.object_exists(obj_ref) assert ray.get(obj_ref) == 2 @pytest.mark.skipif(client_test_enabled(), reason="internal api") def test_actor_large_objects(ray_start_regular_shared): @ray.remote class Actor: def __init__(self): pass def f(self): time.sleep(1) return bytes(80000000) a = Actor.remote() obj_ref = a.f.remote() assert not ray._private.worker.global_worker.core_worker.object_exists(obj_ref) done, _ = ray.wait([obj_ref]) assert len(done) == 1 assert ray._private.worker.global_worker.core_worker.object_exists(obj_ref) assert isinstance(ray.get(obj_ref), bytes) def test_actor_pass_by_ref(ray_start_regular_shared): @ray.remote class Actor: def __init__(self): pass def f(self, x): return x * 2 @ray.remote def f(x): return x @ray.remote def error(): sys.exit(0) a = Actor.remote() assert ray.get(a.f.remote(f.remote(1))) == 2 fut = [a.f.remote(f.remote(i)) for i in range(100)] assert ray.get(fut) == [i * 2 for i in range(100)] # propagates errors for pass by ref with pytest.raises(Exception): ray.get(a.f.remote(error.remote())) def test_actor_recursive(ray_start_regular_shared): @ray.remote class Actor: def __init__(self, delegate=None): self.delegate = delegate def f(self, x): if self.delegate: return ray.get(self.delegate.f.remote(x)) return x * 2 a = Actor.remote() b = Actor.remote(a) c = Actor.remote(b) result = ray.get([c.f.remote(i) for i in range(100)]) assert result == [x * 2 for x in range(100)] result, _ = ray.wait([c.f.remote(i) for i in range(100)], num_returns=100) result = ray.get(result) assert result == [x * 2 for x in range(100)] def test_actor_concurrent(ray_start_regular_shared): @ray.remote class Batcher: def __init__(self): self.batch = [] self.event = threading.Event() def add(self, x): self.batch.append(x) if len(self.batch) >= 3: self.event.set() else: self.event.wait() return sorted(self.batch) a = Batcher.options(max_concurrency=3).remote() x1 = a.add.remote(1) x2 = a.add.remote(2) x3 = a.add.remote(3) r1 = ray.get(x1) r2 = ray.get(x2) r3 = ray.get(x3) assert r1 == [1, 2, 3] assert r1 == r2 == r3 def test_actor_max_concurrency(ray_start_regular_shared): """ Test that an actor of max_concurrency=N should only run N tasks at most concurrently. """ CONCURRENCY = 3 @ray.remote class ConcurentActor: def __init__(self): self.threads = set() def call(self): # Record the current thread that runs this function. self.threads.add(threading.get_ident()) def get_num_threads(self): return len(self.threads) @ray.remote def call(actor): for _ in range(CONCURRENCY * 100): ray.get(actor.call.remote()) return actor = ConcurentActor.options(max_concurrency=CONCURRENCY).remote() # Start many concurrent tasks that will call the actor many times. ray.get([call.remote(actor) for _ in range(CONCURRENCY * 10)]) # Check that the number of threads shouldn't be greater than CONCURRENCY. assert ray.get(actor.get_num_threads.remote()) <= CONCURRENCY def test_duplicate_args(ray_start_regular_shared): @ray.remote def f(arg1, arg2, arg1_duplicate, kwarg1=None, kwarg2=None, kwarg1_duplicate=None): assert arg1 == kwarg1 assert arg1 != arg2 assert arg1 == arg1_duplicate assert kwarg1 != kwarg2 assert kwarg1 == kwarg1_duplicate # Test by-value arguments. arg1 = [1] arg2 = [2] ray.get(f.remote(arg1, arg2, arg1, kwarg1=arg1, kwarg2=arg2, kwarg1_duplicate=arg1)) # Test by-reference arguments. arg1 = ray.put([1]) arg2 = ray.put([2]) ray.get(f.remote(arg1, arg2, arg1, kwarg1=arg1, kwarg2=arg2, kwarg1_duplicate=arg1)) # Test by-reference arguments on an actor task. @ray.remote class Actor: def f( self, arg1, arg2, arg1_duplicate, kwarg1=None, kwarg2=None, kwarg1_duplicate=None, ): assert arg1 == kwarg1 assert arg1 != arg2 assert arg1 == arg1_duplicate assert kwarg1 != kwarg2 assert kwarg1 == kwarg1_duplicate actor = Actor.remote() ray.get( actor.f.remote( arg1, arg2, arg1, kwarg1=arg1, kwarg2=arg2, kwarg1_duplicate=arg1 ) ) @pytest.mark.skipif(client_test_enabled(), reason="internal api") def test_get_correct_node_ip(): with patch("ray._private.worker") as worker_mock: node_mock = MagicMock() node_mock.node_ip_address = "10.0.0.111" worker_mock._global_node = node_mock found_ip = ray.util.get_node_ip_address() assert found_ip == "10.0.0.111" def test_load_code_from_local(ray_start_regular_shared): # This case writes a driver python file to a temporary directory. # # The driver starts a cluster with # `ray.init(ray.job_config.JobConfig(code_search_path=))`, # then creates a nested actor. The actor will be loaded from code in # worker. # # This tests the following two cases when : # 1) Load a nested class. # 2) Load a class defined in the `__main__` module. code_test = """ import os import ray class A: @ray.remote class B: def get(self): return "OK" if __name__ == "__main__": current_path = os.path.dirname(__file__) job_config = ray.job_config.JobConfig(code_search_path=[current_path]) ray.init({}, job_config=job_config) b = A.B.remote() print(ray.get(b.get.remote())) """ # Test code search path contains space. with tempfile.TemporaryDirectory(suffix="a b") as tmpdir: test_driver = os.path.join(tmpdir, "test_load_code_from_local.py") with open(test_driver, "w") as f: f.write(code_test.format(repr(ray_start_regular_shared["address"]))) # Ray's handling of sys.path does not work with PYTHONSAFEPATH. env = os.environ.copy() if env.get("PYTHONSAFEPATH", "") != "": env["PYTHONSAFEPATH"] = "" # Set to empty string to disable. output = subprocess.check_output([sys.executable, test_driver], env=env) assert b"OK" in output, f"Output has no 'OK': {output.decode()}" def test_load_from_local_function(ray_start_regular_shared): code_test = """ import ray import os @ray.remote def wrapped_func(): return "OK" def normal_func(): return "OK" if __name__ == "__main__": current_path = os.path.dirname(__file__) job_config = ray.job_config.JobConfig(code_search_path=[current_path]) ray.init({}, job_config=job_config) res1 = wrapped_func.remote() wrapped_normal_func = ray.remote(normal_func) res2 = wrapped_normal_func.remote() s1 = ray.get(res1) s2 = ray.get(res2) print(s1+"+"+s2) """ # Test code search path contains space. with tempfile.TemporaryDirectory(suffix="a b") as tmpdir: test_driver = os.path.join(tmpdir, "test_load_code_from_local.py") with open(test_driver, "w") as f: f.write(code_test.format(repr(ray_start_regular_shared["address"]))) env = os.environ.copy() if env.get("PYTHONSAFEPATH", "") != "": env["PYTHONSAFEPATH"] = "" # Set to empty string to disable. output = subprocess.check_output([sys.executable, test_driver], env=env) assert b"OK+OK" in output, f"Output has no 'OK+OK': {output.decode()}" @pytest.mark.skipif( client_test_enabled(), reason="JobConfig doesn't work in client mode" ) def test_use_dynamic_function_and_class(): # Test use dynamically defined functions # and classes for remote tasks and actors. # See https://github.com/ray-project/ray/issues/12834. ray.shutdown() current_path = os.path.dirname(__file__) job_config = ray.job_config.JobConfig(code_search_path=[current_path]) ray.init(job_config=job_config) def foo1(): @ray.remote def foo2(): return "OK" return foo2 @ray.remote class Foo: @ray.method(num_returns=1) def foo(self): return "OK" f = foo1() assert ray.get(f.remote()) == "OK" # Check whether the dynamic function is exported to GCS. # Note, the key format should be kept # the same as in `FunctionActorManager.export`. key_func = ( b"RemoteFunction:" + ray._private.worker.global_worker.current_job_id.hex().encode() + b":" + f._function_descriptor.function_id.binary() ) assert ray._private.worker.global_worker.gcs_client.internal_kv_exists( key_func, KV_NAMESPACE_FUNCTION_TABLE ) foo_actor = Foo.remote() assert ray.get(foo_actor.foo.remote()) == "OK" # Check whether the dynamic class is exported to GCS. # Note, the key format should be kept # the same as in `FunctionActorManager.export_actor_class`. key_cls = ( b"ActorClass:" + ray._private.worker.global_worker.current_job_id.hex().encode() + b":" + foo_actor._ray_actor_creation_function_descriptor.function_id.binary() ) assert ray._private.worker.global_worker.gcs_client.internal_kv_exists( key_cls, namespace=KV_NAMESPACE_FUNCTION_TABLE ) if __name__ == "__main__": import pytest # Skip test_basic_2_client_mode for now- the test suite is breaking. sys.exit(pytest.main(["-sv", __file__]))