# coding: utf-8 import logging import os import pickle import random import re import sys import time import pytest import ray import ray.cluster_utils from ray._common.test_utils import ( SignalActor, run_string_as_driver, ) from ray._private.test_utils import client_test_enabled from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy import psutil logger = logging.getLogger(__name__) # https://github.com/ray-project/ray/issues/6662 @pytest.mark.skipif( os.environ.get("RAY_MINIMAL") == "1", reason="This test is not supposed to work for minimal installation.", ) @pytest.mark.skipif(client_test_enabled(), reason="interferes with grpc") def test_http_proxy(start_http_proxy, shutdown_only): # C++ config `grpc_enable_http_proxy` only initializes once, so we have to # run driver as a separate process to make sure the correct config value # is initialized. script = """ import ray ray.init(num_cpus=1) @ray.remote def f(): return 1 assert ray.get(f.remote()) == 1 """ env = start_http_proxy run_string_as_driver(script, dict(os.environ, **env)) def test_release_cpu_resources(shutdown_only): ray.init(num_cpus=1) @ray.remote(num_cpus=1) def child(): return 3 @ray.remote(num_cpus=1) def parent(): # Parent should release the CPU resource # to run child. return ray.get(child.remote()) assert ray.get(parent.remote()) == 3 # Make sure CPU resource inside PG can also be released properly. pg = ray.util.placement_group(bundles=[{"CPU": 1}]) assert ( ray.get( parent.options( scheduling_strategy=PlacementGroupSchedulingStrategy( placement_group=pg, placement_group_capture_child_tasks=True ) ).remote() ) == 3 ) assert ( ray.get( parent.options( scheduling_strategy=PlacementGroupSchedulingStrategy( placement_group=pg, placement_group_bundle_index=0, placement_group_capture_child_tasks=True, ) ).remote() ) == 3 ) # https://github.com/ray-project/ray/issues/16025 def test_release_resources_race(shutdown_only): ray.init(num_cpus=2) refs = [] for _ in range(10): refs.append(ray.put(bytearray(1024 * 1024))) @ray.remote def consume(refs): # Should work without releasing resources! ray.get(refs) return os.getpid() pids = set(ray.get([consume.remote(refs) for _ in range(10)])) # Should not have started multiple workers. assert len(pids) <= 2, pids def test_not_release_resource(shutdown_only): # Test to make sure we don't release CPU # resource if the object is already fetched. ray.init(num_cpus=1) @ray.remote def task1(): return [1] * (1024 * 1024) o1 = task1.remote() @ray.remote def task2(*args, **kwargs): # ray.get here should not release # CPU resource since the object is already # available in args[0] assert args[0] == ray.get(kwargs["o"][0]) return os.getpid() @ray.remote def task3(*args): return os.getpid() o2 = task2.remote(o1, o=[o1]) # This should run after task2 finishes o3 = task3.remote(o1) assert len(set(ray.get([o2, o3]))) == 1 # https://github.com/ray-project/ray/issues/22504 def test_worker_isolation_by_resources(shutdown_only): ray.init(num_cpus=1, num_gpus=1) @ray.remote(num_gpus=1) def gpu(): return os.getpid() @ray.remote def cpu(): return os.getpid() pid1 = ray.get(cpu.remote()) pid2 = ray.get(gpu.remote()) assert pid1 != pid2, (pid1, pid2) # https://github.com/ray-project/ray/issues/10960 def test_max_calls_releases_resources(shutdown_only): ray.init(num_cpus=2, num_gpus=1) @ray.remote(num_cpus=0) def g(): return 0 @ray.remote(num_cpus=1, num_gpus=1, max_calls=1, max_retries=0) def f(): return [g.remote()] for i in range(10): print(i) ray.get(f.remote()) # This will hang if GPU resources aren't released. # https://github.com/ray-project/ray/issues/7263 def test_grpc_message_size(shutdown_only): ray.init(num_cpus=1) @ray.remote def bar(*a): return # 50KiB, not enough to spill to plasma, but will be inlined. def f(): return bytearray(50000) # Executes a 10MiB task spec ray.get(bar.remote(*[f() for _ in range(200)])) def test_default_worker_import_dependency(shutdown_only): """ Test ray's python worker import doesn't import the not-allowed dependencies. """ # We don't allow numpy to be imported in the worker script to avoid slow # worker startup time, as well as interfering with OMP_NUM_THREADS which # is used by numpy when imported. # See https://github.com/ray-project/ray/issues/33891 blocked_deps = ["numpy"] # Ray should not be importing pydantic (used in serialization) eagerly. # This introduces regression in worker start up time. # https://github.com/ray-project/ray/issues/41338 blocked_deps += ["pydantic"] # Remove the ray module and the blocked deps from sys.modules. sys.modules.pop("ray", None) assert "ray" not in sys.modules for dep in blocked_deps: sys.modules.pop(dep, None) assert dep not in sys.modules # This imports the python worker. import ray._private.workers.default_worker # noqa: F401 # Check that the ray module is imported. assert "ray" in sys.modules # Check that the blocked deps are not imported. for dep in blocked_deps: assert dep not in sys.modules # Test starting a ray workers should not see unwanted deps loaded eagerly. ray.init() @ray.remote def f(): import ray # noqa: F401 assert "ray" in sys.modules for x in blocked_deps: assert x not in sys.modules ray.get(f.remote()) @pytest.mark.skipif( sys.platform != "linux", reason="Windows/OSX thread count not policed yet." ) def test_worker_thread_count(monkeypatch, shutdown_only): """This test will fail if the number of threads spawned by a worker process increases. If you find that a patch is now causing this test to fail, consider if this thread count change is expected and adjust the test (or your patch) accordingly! """ @ray.remote class Actor: def get_thread_count(self): try: process = psutil.Process(os.getpid()) return process.num_threads() except ImportError: return None # Set the environment variables used by the raylet and worker monkeypatch.setenv("RAY_worker_num_grpc_internal_threads", "1") monkeypatch.setenv("RAY_num_server_call_thread", "1") monkeypatch.setenv("RAY_core_worker_num_server_call_thread", "1") # TODO(#55215): The for loop and the 'assert ... in {..,..}' complicates this # test unnecessarily. We should only need to call the assert after # a single call to the worker. However, because the thread count # per worker today isn't entirely static, we need to allow for this # flexibility. https://github.com/ray-project/ray/issues/55215 actor = Actor.remote() for _ in range(5): ray.get(actor.get_thread_count.remote()) # Lowering these numbers in this assert should be celebrated, # increasing these numbers should be scrutinized assert ray.get(actor.get_thread_count.remote()) in {21, 22, 23, 24} # https://github.com/ray-project/ray/issues/7287 def test_omp_threads_set(ray_start_cluster, monkeypatch): cluster = ray_start_cluster cluster.add_node(num_cpus=2) ray.init(address=cluster.address) @ray.remote def f(): return os.environ.get("OMP_NUM_THREADS") @ray.remote class Actor: def f(self): return os.environ.get("OMP_NUM_THREADS") ########################### # Test basic tasks ########################### # Test override to num_cpus if OMP_NUM_THREADS not set assert ray.get(f.options(num_cpus=2).remote()) == "2" # Test override to default cpu number if OMP_NUM_THREADS not set assert ray.get(f.remote()) == "1" # Test set to 1 for fractional CPU assert ray.get(f.options(num_cpus=0.25).remote()) == "1" ########################### # Test not overriding env_variables ########################### from ray.runtime_env import RuntimeEnv assert ( ray.get( f.options( runtime_env=RuntimeEnv(env_vars={"OMP_NUM_THREADS": "2"}) ).remote() ) == "2" ) assert ( ray.get( f.options( num_cpus=1, runtime_env=RuntimeEnv(env_vars={"OMP_NUM_THREADS": "2"}) ).remote() ) == "2" ) ########################### # Test actor tasks ########################### # Test actor tasks set OMP_NUM_THREADS correctly in a similar way. assert ray.get(Actor.remote().f.remote()) == "1" assert ray.get(Actor.options(num_cpus=2).remote().f.remote()) == "2" assert ray.get(Actor.options(num_cpus=0.25).remote().f.remote()) == "1" ########################### # Test setting and restoring of the environ after tasks run ########################### @ray.remote def g(): return os.getpid(), os.environ.get("OMP_NUM_THREADS") # Set to 1 pid1, omp_num_threads = ray.get(g.remote()) assert omp_num_threads == "1" # Set to 2 pid2, omp_num_threads = ray.get(g.options(num_cpus=2).remote()) assert pid1 == pid2 assert omp_num_threads == "2" ########################### # Test not setting the value with environ already set to 1 in env ########################### with monkeypatch.context() as m: m.setenv("OMP_NUM_THREADS", "1") cluster.add_node(num_cpus=4) assert ray.get(f.options(num_cpus=4).remote()) == "1" def test_submit_api(shutdown_only): ray.init(num_cpus=2, num_gpus=1, resources={"Custom": 1}) @ray.remote def f(n): return list(range(n)) @ray.remote def g(): return ray.get_gpu_ids() assert f._remote([0], num_returns=0) is None id1 = f._remote(args=[1], num_returns=1) assert ray.get(id1) == [0] id1, id2 = f._remote(args=[2], num_returns=2) assert ray.get([id1, id2]) == [0, 1] id1, id2, id3 = f._remote(args=[3], num_returns=3) assert ray.get([id1, id2, id3]) == [0, 1, 2] assert ray.get( g._remote(args=[], num_cpus=1, num_gpus=1, resources={"Custom": 1}) ) == [0] infeasible_id = g._remote(args=[], resources={"NonexistentCustom": 1}) assert ray.get(g._remote()) == [] ready_ids, remaining_ids = ray.wait([infeasible_id], timeout=0.05) assert len(ready_ids) == 0 assert len(remaining_ids) == 1 # Check mismatch with num_returns. with pytest.raises(ValueError): ray.get(f.options(num_returns=2).remote(3)) with pytest.raises(ValueError): ray.get(f.options(num_returns=3).remote(2)) @ray.remote class Actor: def __init__(self, x, y=0): self.x = x self.y = y def method(self, a, b=0): return self.x, self.y, a, b def gpu_ids(self): return ray.get_gpu_ids() @ray.remote class Actor2: def __init__(self): pass def method(self): pass a = Actor._remote(args=[0], kwargs={"y": 1}, num_gpus=1, resources={"Custom": 1}) a2 = Actor2._remote() ray.get(a2.method._remote()) id1, id2, id3, id4 = a.method._remote(args=["test"], kwargs={"b": 2}, num_returns=4) assert ray.get([id1, id2, id3, id4]) == [0, 1, "test", 2] def test_invalid_arguments(): def f(): return 1 class A: x = 1 template1 = ( "The type of keyword '{}' " + f"must be {(int, type(None))}, but received type {float}" ) # Type check for keyword in ("max_retries", "max_calls"): with pytest.raises(TypeError, match=re.escape(template1.format(keyword))): ray.remote(**{keyword: random.random()})(f) num_returns_template = ( "The type of keyword 'num_returns' " + f"must be {(int, str, type(None))}, but received type {float}" ) with pytest.raises(TypeError, match=re.escape(num_returns_template)): ray.remote(**{"num_returns": random.random()})(f) for keyword in ("max_restarts", "max_task_retries"): with pytest.raises(TypeError, match=re.escape(template1.format(keyword))): ray.remote(**{keyword: random.random()})(A) # Value check for non-negative finite values for v in (random.randint(-100, -2), -1): keyword = "max_calls" with pytest.raises( ValueError, match=f"The keyword '{keyword}' only accepts None, " f"0 or a positive integer", ): ray.remote(**{keyword: v})(f) keyword = "num_returns" with pytest.raises( ValueError, match=f"The keyword '{keyword}' only accepts None, " "a non-negative integer, " r"'streaming' \(for generators\), or 'dynamic'", ): ray.remote(**{keyword: v})(f) # Value check for non-negative and infinite values template2 = ( "The keyword '{}' only accepts None, 0, -1 or a positive integer, " "where -1 represents infinity." ) with pytest.raises(ValueError, match=template2.format("max_retries")): ray.remote(max_retries=random.randint(-100, -2))(f) for keyword in ("max_restarts", "max_task_retries"): with pytest.raises(ValueError, match=template2.format(keyword)): ray.remote(**{keyword: random.randint(-100, -2)})(A) # Check invalid resource quantity with pytest.raises( ValueError, match=( "The precision of the fractional quantity of resource num_gpus" " cannot go beyond 0.0001" ), ): ray.remote(num_gpus=0.0000001)(f) with pytest.raises( ValueError, match=( "The precision of the fractional quantity of resource custom_resource" " cannot go beyond 0.0001" ), ): ray.remote(resources={"custom_resource": 0.0000001})(f) def test_options(): """General test of option keywords in Ray.""" from ray._common import ray_option_utils def f(): return 1 class A: x = 1 task_defaults = { k: v.default_value for k, v in ray_option_utils.task_options.items() } task_defaults_for_options = task_defaults.copy() task_defaults_for_options.pop("max_calls") ray.remote(f).options(**task_defaults_for_options) ray.remote(**task_defaults)(f).options(**task_defaults_for_options) with pytest.raises( ValueError, match=re.escape("Setting 'max_calls' is not supported in '.options()'."), ): ray.remote(f).options(max_calls=1) actor_defaults = { k: v.default_value for k, v in ray_option_utils.actor_options.items() } actor_defaults_for_options = actor_defaults.copy() actor_defaults_for_options.pop("concurrency_groups") ray.remote(A).options(**actor_defaults_for_options) ray.remote(**actor_defaults)(A).options(**actor_defaults_for_options) with pytest.raises( ValueError, match=re.escape( "Setting 'concurrency_groups' is not supported in '.options()'." ), ): ray.remote(A).options(concurrency_groups=[]) unique_object = type("###", (), {})() for k, v in ray_option_utils.task_options.items(): v.validate(k, v.default_value) with pytest.raises(TypeError): v.validate(k, unique_object) for k, v in ray_option_utils.actor_options.items(): v.validate(k, v.default_value) with pytest.raises(TypeError): v.validate(k, unique_object) @ray.remote(num_gpus=2) def foo(): pass assert foo._default_options == { "max_calls": 1, "num_gpus": 2, } f2 = foo.options(num_cpus=1, num_gpus=1) # TODO(suquark): The current implementation of `.options()` is so bad that we # cannot even access its options from outside. Here we hack the closures to # achieve our goal. Need futher efforts to clean up the tech debt. assert f2.remote.__closure__[2].cell_contents == { "num_cpus": 1, "num_gpus": 1, } # https://github.com/ray-project/ray/issues/17842 def test_disable_cuda_devices(): script = """ import ray ray.init() @ray.remote def check(): import os assert "CUDA_VISIBLE_DEVICES" not in os.environ print("remote", ray.get(check.remote())) """ run_string_as_driver( script, dict(os.environ, **{"RAY_EXPERIMENTAL_NOSET_CUDA_VISIBLE_DEVICES": "1"}) ) # https://github.com/ray-project/ray/issues/54868 def test_not_override_accelerator_ids_when_num_accelerators_is_zero(): not_override_check_script = """ import ray ray.init() @ray.remote(num_gpus=0) def check(): import os assert "CUDA_VISIBLE_DEVICES" not in os.environ @ray.remote(num_gpus=0) class Actor: def check(self): import os assert "CUDA_VISIBLE_DEVICES" not in os.environ print("task check", ray.get(check.remote())) print("actor check", ray.get(Actor.options(num_gpus=0).remote().check.remote())) """ run_string_as_driver(not_override_check_script) override_check_script = """ import ray ray.init() @ray.remote(num_gpus=0) def check(): import os assert os.environ.get("CUDA_VISIBLE_DEVICES") == "" @ray.remote(num_gpus=0) class Actor: def check(self): import os assert os.environ.get("CUDA_VISIBLE_DEVICES") == "" print("task check", ray.get(check.remote())) print("actor check", ray.get(Actor.options(num_gpus=0).remote().check.remote())) """ run_string_as_driver( override_check_script, dict( os.environ, **{"RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO": "1"}, ), ) def test_put_get(shutdown_only): ray.init(num_cpus=0) for i in range(100): value_before = i * 10**6 object_ref = ray.put(value_before) value_after = ray.get(object_ref) assert value_before == value_after for i in range(100): value_before = i * 10**6 * 1.0 object_ref = ray.put(value_before) value_after = ray.get(object_ref) assert value_before == value_after for i in range(100): value_before = "h" * i object_ref = ray.put(value_before) value_after = ray.get(object_ref) assert value_before == value_after for i in range(100): value_before = [1] * i object_ref = ray.put(value_before) value_after = ray.get(object_ref) assert value_before == value_after @pytest.mark.skipif(client_test_enabled(), reason="internal _raylet") def test_function_descriptor(): python_descriptor = ray._raylet.PythonFunctionDescriptor( "module_name", "function_name", "class_name", "function_hash" ) python_descriptor2 = pickle.loads(pickle.dumps(python_descriptor)) assert python_descriptor == python_descriptor2 assert hash(python_descriptor) == hash(python_descriptor2) assert python_descriptor.function_id == python_descriptor2.function_id java_descriptor = ray._raylet.JavaFunctionDescriptor( "class_name", "function_name", "signature" ) java_descriptor2 = pickle.loads(pickle.dumps(java_descriptor)) assert java_descriptor == java_descriptor2 assert python_descriptor != java_descriptor assert python_descriptor != object() d = {python_descriptor: 123} assert d.get(python_descriptor2) == 123 def test_ray_options(shutdown_only): ray.init(num_cpus=10, num_gpus=10, resources={"custom1": 2}) @ray.remote(num_cpus=2, num_gpus=3, memory=150 * 2**20, resources={"custom1": 1}) def foo(expected_resources): # Possibly wait until the available resources have been updated # (there might be a delay due to heartbeats) retries = 10 keys = ["CPU", "GPU", "custom1"] while retries >= 0: resources = ray.available_resources() do_return = True for key in keys: if resources[key] != expected_resources[key]: print(key, resources[key], expected_resources[key]) do_return = False break if do_return: return resources["memory"] time.sleep(0.1) retries -= 1 raise RuntimeError("Number of retries exceeded") expected_resources_without_options = {"CPU": 8.0, "GPU": 7.0, "custom1": 1.0} memory_available_without_options = ray.get( foo.remote(expected_resources_without_options) ) expected_resources_with_options = {"CPU": 7.0, "GPU": 6.0, "custom1": 1.5} memory_available_with_options = ray.get( foo.options( num_cpus=3, num_gpus=4, memory=50 * 2**20, resources={"custom1": 0.5} ).remote(expected_resources_with_options) ) assert memory_available_without_options < memory_available_with_options @pytest.mark.skipif(client_test_enabled(), reason="internal api") @pytest.mark.parametrize( "ray_start_cluster_head", [ { "num_cpus": 0, "object_store_memory": 75 * 1024 * 1024, "_system_config": {"automatic_object_spilling_enabled": False}, } ], indirect=True, ) def test_fetch_local(ray_start_cluster_head): cluster = ray_start_cluster_head cluster.add_node(num_cpus=2, object_store_memory=75 * 1024 * 1024) signal_actor = SignalActor.remote() @ray.remote def put(): ray.wait([signal_actor.wait.remote()]) return bytearray(40 * 1024 * 1024) # 40 MB data local_ref = ray.put(bytearray(40 * 1024 * 1024)) remote_ref = put.remote() # Data is not ready in any node (ready_ref, remaining_ref) = ray.wait([remote_ref], timeout=2, fetch_local=False) assert (0, 1) == (len(ready_ref), len(remaining_ref)) ray.wait([signal_actor.send.remote()]) # Data is ready in some node, but not local node. (ready_ref, remaining_ref) = ray.wait([remote_ref], fetch_local=False) assert (1, 0) == (len(ready_ref), len(remaining_ref)) (ready_ref, remaining_ref) = ray.wait([remote_ref], timeout=2, fetch_local=True) assert (0, 1) == (len(ready_ref), len(remaining_ref)) del local_ref (ready_ref, remaining_ref) = ray.wait([remote_ref], fetch_local=True) assert (1, 0) == (len(ready_ref), len(remaining_ref)) def test_nested_functions(ray_start_regular_shared): # Make sure that remote functions can use other values that are defined # after the remote function but before the first function invocation. @ray.remote def f(): return g(), ray.get(h.remote()) def g(): return 1 @ray.remote def h(): return 2 assert ray.get(f.remote()) == (1, 2) def test_recursive_remote_call(ray_start_regular_shared): # Test a remote function that recursively calls itself. @ray.remote def factorial(n): if n == 0: return 1 return n * ray.get(factorial.remote(n - 1)) assert ray.get(factorial.remote(0)) == 1 assert ray.get(factorial.remote(1)) == 1 assert ray.get(factorial.remote(2)) == 2 assert ray.get(factorial.remote(3)) == 6 assert ray.get(factorial.remote(4)) == 24 assert ray.get(factorial.remote(5)) == 120 def test_mutually_recursive_functions(ray_start_regular_shared): # Test remote functions that recursively call each other. @ray.remote def factorial_even(n): assert n % 2 == 0 if n == 0: return 1 return n * ray.get(factorial_odd.remote(n - 1)) @ray.remote def factorial_odd(n): assert n % 2 == 1 return n * ray.get(factorial_even.remote(n - 1)) assert ray.get(factorial_even.remote(4)) == 24 assert ray.get(factorial_odd.remote(5)) == 120 def test_ray_recursive_objects(ray_start_regular_shared): class ClassA: pass # Make a list that contains itself. lst = [] lst.append(lst) # Make an object that contains itself as a field. a1 = ClassA() a1.field = a1 # Make two objects that contain each other as fields. a2 = ClassA() a3 = ClassA() a2.field = a3 a3.field = a2 # Make a dictionary that contains itself. d1 = {} d1["key"] = d1 # Create a list of recursive objects. recursive_objects = [lst, a1, a2, a3, d1] # Serialize the recursive objects. for obj in recursive_objects: ray.put(obj) def test_passing_arguments_by_value_out_of_the_box(ray_start_regular_shared): @ray.remote def f(x): return x # Test passing lambdas. def temp(): return 1 assert ray.get(f.remote(temp))() == 1 assert ray.get(f.remote(lambda x: x + 1))(3) == 4 # Test sets. assert ray.get(f.remote(set())) == set() s = {1, (1, 2, "hi")} assert ray.get(f.remote(s)) == s # Test types. assert ray.get(f.remote(int)) is int assert ray.get(f.remote(float)) is float assert ray.get(f.remote(str)) is str class Foo: def __init__(self): pass # Make sure that we can put and get a custom type. Note that the result # won't be "equal" to Foo. ray.get(ray.put(Foo)) def test_putting_object_that_closes_over_object_ref(ray_start_regular_shared): # This test is here to prevent a regression of # https://github.com/ray-project/ray/issues/1317. class Foo: def __init__(self): self.val = ray.put(0) def method(self): _ = f f = Foo() ray.put(f) def test_keyword_args(ray_start_regular_shared): @ray.remote def keyword_fct1(a, b="hello"): return "{} {}".format(a, b) @ray.remote def keyword_fct2(a="hello", b="world"): return "{} {}".format(a, b) @ray.remote def keyword_fct3(a, b, c="hello", d="world"): return "{} {} {} {}".format(a, b, c, d) x = keyword_fct1.remote(1) assert ray.get(x) == "1 hello" x = keyword_fct1.remote(1, "hi") assert ray.get(x) == "1 hi" x = keyword_fct1.remote(1, b="world") assert ray.get(x) == "1 world" x = keyword_fct1.remote(a=1, b="world") assert ray.get(x) == "1 world" x = keyword_fct2.remote(a="w", b="hi") assert ray.get(x) == "w hi" x = keyword_fct2.remote(b="hi", a="w") assert ray.get(x) == "w hi" x = keyword_fct2.remote(a="w") assert ray.get(x) == "w world" x = keyword_fct2.remote(b="hi") assert ray.get(x) == "hello hi" x = keyword_fct2.remote("w") assert ray.get(x) == "w world" x = keyword_fct2.remote("w", "hi") assert ray.get(x) == "w hi" x = keyword_fct3.remote(0, 1, c="w", d="hi") assert ray.get(x) == "0 1 w hi" x = keyword_fct3.remote(0, b=1, c="w", d="hi") assert ray.get(x) == "0 1 w hi" x = keyword_fct3.remote(a=0, b=1, c="w", d="hi") assert ray.get(x) == "0 1 w hi" x = keyword_fct3.remote(0, 1, d="hi", c="w") assert ray.get(x) == "0 1 w hi" x = keyword_fct3.remote(0, 1, c="w") assert ray.get(x) == "0 1 w world" x = keyword_fct3.remote(0, 1, d="hi") assert ray.get(x) == "0 1 hello hi" x = keyword_fct3.remote(0, 1) assert ray.get(x) == "0 1 hello world" x = keyword_fct3.remote(a=0, b=1) assert ray.get(x) == "0 1 hello world" # Check that we cannot pass invalid keyword arguments to functions. @ray.remote def f1(): return @ray.remote def f2(x, y=0, z=0): return # Make sure we get an exception if too many arguments are passed in. with pytest.raises(TypeError): f1.remote(3) with pytest.raises(TypeError): f1.remote(x=3) with pytest.raises(TypeError): f2.remote(0, w=0) with pytest.raises(TypeError): f2.remote(3, x=3) # Make sure we get an exception if too many arguments are passed in. with pytest.raises(TypeError): f2.remote(1, 2, 3, 4) @ray.remote def f3(x): return x assert ray.get(f3.remote(4)) == 4 def test_args_starkwargs(ray_start_regular_shared): def starkwargs(a, b, **kwargs): return a, b, kwargs class TestActor: def starkwargs(self, a, b, **kwargs): return a, b, kwargs def test_function(fn, remote_fn): assert fn(1, 2, x=3) == ray.get(remote_fn.remote(1, 2, x=3)) with pytest.raises(TypeError): remote_fn.remote(3) remote_test_function = ray.remote(test_function) remote_starkwargs = ray.remote(starkwargs) test_function(starkwargs, remote_starkwargs) ray.get(remote_test_function.remote(starkwargs, remote_starkwargs)) remote_actor_class = ray.remote(TestActor) remote_actor = remote_actor_class.remote() actor_method = remote_actor.starkwargs local_actor = TestActor() local_method = local_actor.starkwargs test_function(local_method, actor_method) ray.get(remote_test_function.remote(local_method, actor_method)) def test_args_named_and_star(ray_start_regular_shared): def hello(a, x="hello", **kwargs): return a, x, kwargs class TestActor: def hello(self, a, x="hello", **kwargs): return a, x, kwargs def test_function(fn, remote_fn): assert fn(1, x=2, y=3) == ray.get(remote_fn.remote(1, x=2, y=3)) assert fn(1, 2, y=3) == ray.get(remote_fn.remote(1, 2, y=3)) assert fn(1, y=3) == ray.get(remote_fn.remote(1, y=3)) assert fn(1,) == ray.get( remote_fn.remote( 1, ) ) assert fn(1) == ray.get(remote_fn.remote(1)) with pytest.raises(TypeError): remote_fn.remote(1, 2, x=3) remote_test_function = ray.remote(test_function) remote_hello = ray.remote(hello) test_function(hello, remote_hello) ray.get(remote_test_function.remote(hello, remote_hello)) remote_actor_class = ray.remote(TestActor) remote_actor = remote_actor_class.remote() actor_method = remote_actor.hello local_actor = TestActor() local_method = local_actor.hello test_function(local_method, actor_method) ray.get(remote_test_function.remote(local_method, actor_method)) def test_oversized_function(ray_start_regular_shared): bar = bytearray(800 * 1024 * 125) @ray.remote class Actor: def foo(self): return len(bar) @ray.remote def f(): return len(bar) with pytest.raises(ValueError, match="The remote function .*f is too large"): f.remote() with pytest.raises(ValueError, match="The actor Actor is too large"): Actor.remote() def test_args_stars_after(ray_start_regular_shared): def star_args_after(a="hello", b="heo", *args, **kwargs): return a, b, args, kwargs class TestActor: def star_args_after(self, a="hello", b="heo", *args, **kwargs): return a, b, args, kwargs def test_function(fn, remote_fn): assert fn("hi", "hello", 2) == ray.get(remote_fn.remote("hi", "hello", 2)) assert fn("hi", "hello", 2, hi="hi") == ray.get( remote_fn.remote("hi", "hello", 2, hi="hi") ) assert fn(hi="hi") == ray.get(remote_fn.remote(hi="hi")) remote_test_function = ray.remote(test_function) remote_star_args_after = ray.remote(star_args_after) test_function(star_args_after, remote_star_args_after) ray.get(remote_test_function.remote(star_args_after, remote_star_args_after)) remote_actor_class = ray.remote(TestActor) remote_actor = remote_actor_class.remote() actor_method = remote_actor.star_args_after local_actor = TestActor() local_method = local_actor.star_args_after test_function(local_method, actor_method) ray.get(remote_test_function.remote(local_method, actor_method)) @pytest.mark.skipif(client_test_enabled(), reason="internal api") def test_object_id_backward_compatibility(ray_start_regular_shared): # We've renamed Python's `ObjectID` to `ObjectRef`, and added a type # alias for backward compatibility. # This test is to make sure legacy code can still use `ObjectID`. # TODO(hchen): once we completely remove Python's `ObjectID`, # this test can be removed as well. # Check that these 2 types are the same. assert ray.ObjectID == ray.ObjectRef object_ref = ray.put(1) # Check that users can use either type in `isinstance` assert isinstance(object_ref, ray.ObjectID) assert isinstance(object_ref, ray.ObjectRef) def test_nonascii_in_function_body(ray_start_regular_shared): @ray.remote def return_a_greek_char(): return "φ" assert ray.get(return_a_greek_char.remote()) == "φ" def test_failed_task(ray_start_regular_shared, error_pubsub): @ray.remote def throw_exception_fct1(): raise Exception("Test function 1 intentionally failed.") @ray.remote def throw_exception_fct2(): raise Exception("Test function 2 intentionally failed.") @ray.remote(num_returns=3) def throw_exception_fct3(x): raise Exception("Test function 3 intentionally failed.") throw_exception_fct1.remote() throw_exception_fct1.remote() x = throw_exception_fct2.remote() try: ray.get(x) except Exception as e: assert "Test function 2 intentionally failed." in str(e) else: # ray.get should throw an exception. assert False x, y, z = throw_exception_fct3.remote(1.0) for ref in [x, y, z]: try: ray.get(ref) except Exception as e: assert "Test function 3 intentionally failed." in str(e) else: # ray.get should throw an exception. assert False class CustomException(ValueError): def __init__(self, msg): super().__init__(msg) self.field = 1 def f(self): return 2 @ray.remote def f(): raise CustomException("This function failed.") try: ray.get(f.remote()) except Exception as e: assert "This function failed." in str(e) assert isinstance(e, ValueError) assert isinstance(e, CustomException) assert isinstance(e, ray.exceptions.RayTaskError) assert "RayTaskError(CustomException)" in repr(e) assert e.field == 1 assert e.f() == 2 else: # ray.get should throw an exception. assert False def test_base_exception_raised(ray_start_regular_shared): @ray.remote def f(): raise BaseException("rip") return 1 with pytest.raises(BaseException): ray.get(f.remote()) def test_import_ray_does_not_import_grpc(): # First unload grpc and ray if "grpc" in sys.modules: del sys.modules["grpc"] if "ray" in sys.modules: del sys.modules["ray"] # Then import ray from scratch import ray # noqa: F401 # Make sure grpc did not get imported by "import ray" assert "grpc" not in sys.modules # Load grpc back so other tests will not be affected try: import grpc # noqa: F401 except ImportError: # It's ok if we don't have grpc installed. pass if __name__ == "__main__": sys.exit(pytest.main(["-sv", __file__]))