import os import signal import sys import threading import time import numpy as np import pytest import ray import ray._private.ray_constants as ray_constants import ray._private.utils from ray._common.network_utils import parse_address from ray._common.test_utils import Semaphore, wait_for_condition from ray._private.ray_constants import DEBUG_AUTOSCALING_ERROR from ray._private.test_utils import ( get_error_message, get_log_batch, init_error_pubsub, run_string_as_driver_nonblocking, ) from ray.cluster_utils import cluster_not_supported from ray.experimental.internal_kv import _internal_kv_get def test_warning_for_too_many_actors(shutdown_only): # Check that if we run a workload which requires too many workers to be # started that we will receive a warning. num_cpus = 2 ray.init(num_cpus=num_cpus) p = init_error_pubsub() @ray.remote(num_cpus=0) class Foo: def __init__(self): time.sleep(1000) # NOTE: We should save actor, otherwise it will be out of scope. actor_group1 = [Foo.remote() for _ in range(num_cpus * 10)] assert len(actor_group1) == num_cpus * 10 errors = get_error_message(p, 1, ray_constants.WORKER_POOL_LARGE_ERROR) assert len(errors) == 1 assert errors[0]["type"] == ray_constants.WORKER_POOL_LARGE_ERROR actor_group2 = [Foo.remote() for _ in range(num_cpus * 3)] assert len(actor_group2) == num_cpus * 3 errors = get_error_message(p, 1, ray_constants.WORKER_POOL_LARGE_ERROR) assert len(errors) == 1 assert errors[0]["type"] == ray_constants.WORKER_POOL_LARGE_ERROR p.close() def test_warning_for_too_many_nested_tasks(shutdown_only): # Check that if we run a workload which requires too many workers to be # started that we will receive a warning. num_cpus = 2 ray.init(num_cpus=num_cpus) p = init_error_pubsub() remote_wait = Semaphore.remote(value=0) nested_wait = Semaphore.remote(value=0) ray.get( [ remote_wait.locked.remote(), nested_wait.locked.remote(), ] ) @ray.remote(num_cpus=0.25) def f(): time.sleep(1000) return 1 @ray.remote(num_cpus=0.25) def h(nested_waits): nested_wait.release.remote() ray.get(nested_waits) ray.get(f.remote()) @ray.remote(num_cpus=0.25) def g(remote_waits, nested_waits): # Sleep so that the f tasks all get submitted to the scheduler after # the g tasks. remote_wait.release.remote() # wait until every lock is released. ray.get(remote_waits) ray.get(h.remote(nested_waits)) num_root_tasks = num_cpus * 4 # Lock remote task until everything is scheduled. remote_waits = [] nested_waits = [] for _ in range(num_root_tasks): remote_waits.append(remote_wait.acquire.remote()) nested_waits.append(nested_wait.acquire.remote()) [g.remote(remote_waits, nested_waits) for _ in range(num_root_tasks)] errors = get_error_message(p, 1, ray_constants.WORKER_POOL_LARGE_ERROR) assert len(errors) == 1 assert errors[0]["type"] == ray_constants.WORKER_POOL_LARGE_ERROR p.close() # Note that this test will take at least 10 seconds because it must wait for # the monitor to detect enough missed heartbeats. def test_warning_for_dead_node(ray_start_cluster_2_nodes, error_pubsub): cluster = ray_start_cluster_2_nodes cluster.wait_for_nodes() p = error_pubsub node_ids = {item["NodeID"] for item in ray.nodes()} # Try to make sure that the monitor has received at least one heartbeat # from the node. time.sleep(0.5) # Kill both raylets. cluster.list_all_nodes()[1].kill_raylet() cluster.list_all_nodes()[0].kill_raylet() # Check that we get warning messages for both raylets. errors = get_error_message(p, 2, ray_constants.REMOVED_NODE_ERROR, 40) # Extract the client IDs from the error messages. This will need to be # changed if the error message changes. warning_node_ids = {error["error_message"].split(" ")[5] for error in errors} assert node_ids == warning_node_ids @pytest.mark.skipif( sys.platform == "win32", reason="Killing process on Windows does not raise a signal" ) def test_warning_for_dead_autoscaler(ray_start_regular, error_pubsub): # Terminate the autoscaler process. from ray._private.worker import _global_node autoscaler_process = _global_node.all_processes[ray_constants.PROCESS_TYPE_MONITOR][ 0 ].process autoscaler_process.terminate() # Confirm that we receive an autoscaler failure error. errors = get_error_message( error_pubsub, 1, ray_constants.MONITOR_DIED_ERROR, timeout=5 ) assert len(errors) == 1 # Confirm that the autoscaler failure error is stored. error = _internal_kv_get(DEBUG_AUTOSCALING_ERROR) assert error is not None def test_raylet_crash_when_get(ray_start_regular): def sleep_to_kill_raylet(): # Don't kill raylet before default workers get connected. time.sleep(2) ray._private.worker._global_node.kill_raylet() object_ref = ray.put(np.zeros(200 * 1024, dtype=np.uint8)) ray._private.internal_api.free(object_ref) thread = threading.Thread(target=sleep_to_kill_raylet) thread.start() with pytest.raises(ray.exceptions.ObjectFreedError): ray.get(object_ref) thread.join() @pytest.mark.parametrize( "ray_start_cluster", [ { "num_nodes": 1, "num_cpus": 2, }, { "num_nodes": 2, "num_cpus": 1, }, ], indirect=True, ) def test_eviction(ray_start_cluster): @ray.remote def large_object(): return np.zeros(10 * 1024 * 1024) obj = large_object.remote() assert isinstance(ray.get(obj), np.ndarray) # Evict the object. ray._private.internal_api.free([obj]) # ray.get throws an exception. with pytest.raises(ray.exceptions.ObjectFreedError): ray.get(obj) @ray.remote def dependent_task(x): return # If the object is passed by reference, the task throws an # exception. with pytest.raises(ray.exceptions.RayTaskError): ray.get(dependent_task.remote(obj)) @pytest.mark.parametrize( "ray_start_cluster", [ { "num_nodes": 2, "num_cpus": 1, }, { "num_nodes": 1, "num_cpus": 2, }, ], indirect=True, ) def test_serialized_id(ray_start_cluster): @ray.remote def small_object(): # Sleep a bit before creating the object to force a timeout # at the getter. time.sleep(1) return 1 @ray.remote def dependent_task(x): return x @ray.remote def get(obj_refs, test_dependent_task): print("get", obj_refs) obj_ref = obj_refs[0] if test_dependent_task: assert ray.get(dependent_task.remote(obj_ref)) == 1 else: assert ray.get(obj_ref) == 1 obj = small_object.remote() ray.get(get.remote([obj], False)) obj = small_object.remote() ray.get(get.remote([obj], True)) obj = ray.put(1) ray.get(get.remote([obj], False)) obj = ray.put(1) ray.get(get.remote([obj], True)) @pytest.mark.xfail(cluster_not_supported, reason="cluster not supported") @pytest.mark.parametrize( "use_actors,node_failure", [(False, False), (False, True), (True, False), (True, True)], ) def test_fate_sharing(ray_start_cluster, use_actors, node_failure): config = { "health_check_initial_delay_ms": 0, "health_check_period_ms": 100, "health_check_failure_threshold": 10, } cluster = ray_start_cluster # Head node with no resources. cluster.add_node(num_cpus=0, _system_config=config) ray.init(address=cluster.address) # Node to place the parent actor. node_to_kill = cluster.add_node(num_cpus=1, resources={"parent": 1}) # Node to place the child actor. cluster.add_node(num_cpus=1, resources={"child": 1}) cluster.wait_for_nodes() @ray.remote def sleep(): time.sleep(1000) @ray.remote(resources={"child": 1}) def probe(): return # TODO(swang): This test does not pass if max_restarts > 0 for the # raylet codepath. Add this parameter once the GCS actor service is enabled # by default. @ray.remote class Actor(object): def __init__(self): return def start_child(self, use_actors): if use_actors: child = Actor.options(resources={"child": 1}).remote() ray.get(child.sleep.remote()) else: ray.get(sleep.options(resources={"child": 1}).remote()) def sleep(self): time.sleep(1000) def get_pid(self): return os.getpid() # Returns whether the "child" resource is available. def child_resource_available(): p = probe.remote() ready, _ = ray.wait([p], timeout=1) return len(ready) > 0 # Test fate sharing if the parent process dies. def test_process_failure(use_actors): a = Actor.options(resources={"parent": 1}).remote() pid = ray.get(a.get_pid.remote()) a.start_child.remote(use_actors=use_actors) # Wait for the child to be scheduled. wait_for_condition(lambda: not child_resource_available()) # Kill the parent process. os.kill(pid, 9) wait_for_condition(child_resource_available) # Test fate sharing if the parent node dies. def test_node_failure(node_to_kill, use_actors): a = Actor.options(resources={"parent": 1}).remote() a.start_child.remote(use_actors=use_actors) # Wait for the child to be scheduled. wait_for_condition(lambda: not child_resource_available()) # Kill the parent process. cluster.remove_node(node_to_kill, allow_graceful=False) node_to_kill = cluster.add_node(num_cpus=1, resources={"parent": 1}) wait_for_condition(child_resource_available) return node_to_kill if node_failure: test_node_failure(node_to_kill, use_actors) else: test_process_failure(use_actors) def test_list_named_actors_timeout(monkeypatch, shutdown_only): with monkeypatch.context() as m: # defer for 3s m.setenv( "RAY_testing_asio_delay_us", "ActorInfoGcsService.grpc_server.ListNamedActors=3000000:3000000", ) ray.init(_system_config={"gcs_server_request_timeout_seconds": 1}) @ray.remote class A: pass a = A.options(name="hi").remote() print(a) with pytest.raises(ray.exceptions.GetTimeoutError): ray.util.list_named_actors() def test_raylet_node_manager_server_failure(ray_start_cluster_head, log_pubsub): cluster = ray_start_cluster_head _, redis_port = parse_address(cluster.address) redis_port = int(redis_port) # Reuse redis port to make node manager grpc server fail to start. with pytest.raises(Exception): cluster.add_node(wait=False, node_manager_port=redis_port) # wait for max 10 seconds. def matcher(log_batch): return log_batch["pid"] == "raylet" and any( "Failed to start the grpc server." in line for line in log_batch["lines"] ) match = get_log_batch(log_pubsub, 1, timeout=10, matcher=matcher) assert len(match) > 0 def test_gcs_server_crash_cluster(ray_start_cluster): # Test the GCS server failures will crash the driver. cluster = ray_start_cluster GCS_RECONNECTION_TIMEOUT = 5 node = cluster.add_node( num_cpus=0, _system_config={"gcs_rpc_server_reconnect_timeout_s": GCS_RECONNECTION_TIMEOUT}, ) script = """ import ray import time ray.init(address="auto") time.sleep(60) """ # Get gcs server pid to send a signal. all_processes = node.all_processes gcs_server_process = all_processes["gcs_server"][0].process gcs_server_pid = gcs_server_process.pid proc = run_string_as_driver_nonblocking(script) # Wait long enough to start the driver. time.sleep(5) start = time.time() print(gcs_server_pid) os.kill(gcs_server_pid, signal.SIGKILL) wait_for_condition(lambda: proc.poll() is None, timeout=10) # Make sure the driver was exited within the timeout instead of hanging. # * 2 for avoiding flakiness. assert time.time() - start < GCS_RECONNECTION_TIMEOUT * 2 # Make sure all processes are cleaned up after GCS is crashed. # Currently, not every process is fate shared with GCS. # It seems like log monitor, ray client server, and Redis # are not fate shared. # TODO(sang): Fix it. # wait_for_condition(lambda: not node.any_processes_alive()) if __name__ == "__main__": sys.exit(pytest.main(["-sv", __file__]))