import os import platform import sys import pytest import requests import ray from ray._common.network_utils import build_address from ray._common.test_utils import wait_for_condition from ray._private.test_utils import ( get_node_stats, wait_until_succeeded_without_exception, ) from ray.core.generated import common_pb2 import psutil _WIN32 = os.name == "nt" @pytest.mark.skipif(platform.system() == "Windows", reason="Hangs on Windows.") def test_worker_stats(shutdown_only): ray.init(num_cpus=2, include_dashboard=True) raylet = ray.nodes()[0] reply = get_node_stats(raylet) # Check that there is one connected driver. drivers = [ worker for worker in reply.core_workers_stats if worker.worker_type == common_pb2.DRIVER ] assert len(drivers) == 1 assert os.getpid() == drivers[0].pid @ray.remote def f(): return os.getpid() @ray.remote(num_cpus=1) class Actor: def __init__(self): pass def f(self): return os.getpid() # Run a remote function and actor to create workers. ray.get(f.remote()) a = Actor.remote() ray.get(a.f.remote()) # 1 actor + 1 worker for task + 1 driver num_workers = 3 def verify(): reply = get_node_stats(raylet) # Check that the rest of the processes are workers, 1 for each CPU. assert len(reply.core_workers_stats) == num_workers # Check that all processes are Python. pids = [worker.pid for worker in reply.core_workers_stats] processes = [ p.info["name"] for p in psutil.process_iter(attrs=["pid", "name"]) if p.info["pid"] in pids ] for process in processes: # TODO(ekl) why does travis/mi end up in the process list assert ( "python" in process or "mini" in process or "conda" in process or "travis" in process or "runner" in process or "pytest" in process or "ray" in process ), process return True wait_for_condition(verify) def get_owner_info(node_ids): node_addrs = { n["NodeID"]: (n["NodeManagerAddress"], n["NodeManagerPort"]) for n in ray.nodes() } # Force a global gc to clean up the object store. ray._private.internal_api.global_gc() owner_stats = {n: 0 for n in node_ids} primary_copy_stats = {n: 0 for n in node_ids} for node_id in node_ids: node_stats = ray._private.internal_api.node_stats( node_addrs[node_id][0], node_addrs[node_id][1], False ) num_owned_objects = sum( [stats.num_owned_objects for stats in node_stats.core_workers_stats] ) num_owned_actors = sum( [stats.num_owned_actors for stats in node_stats.core_workers_stats] ) owner_stats[node_id] = (num_owned_objects, num_owned_actors) primary_copy_stats[ node_id ] = node_stats.store_stats.num_object_store_primary_copies print(owner_stats) print(node_ids) owner_stats = [owner_stats.get(node_id, 0) for node_id in node_ids] primary_copy_stats = [primary_copy_stats.get(node_id, 0) for node_id in node_ids] print("owner_stats", owner_stats) print("primary_copy_stats", primary_copy_stats) return owner_stats, primary_copy_stats def test_node_object_metrics(ray_start_cluster): NUM_NODES = 3 cluster = ray_start_cluster for i in range(NUM_NODES): cluster.add_node(True, resources={f"node_{i}": 1}) if i == 0: ray.init(address=cluster.address) node_ids = [] for i in range(NUM_NODES): @ray.remote(resources={f"node_{i}": 1}) def get_node_id(): return ray.get_runtime_context().get_node_id() node_ids.append(ray.get(get_node_id.remote())) # Object store stats # x is owned by node_0 # x is stored at node_0 x = ray.put([1]) # noqa: F841 wait_for_condition( lambda: get_owner_info(node_ids) == ([(1, 0), (0, 0), (0, 0)], [1, 0, 0]) ) # Test nested with put @ray.remote(resources={"node_1": 1}) def big_obj(): # b is owned by node_1 # b is stored at node_1 b = ray.put([1] * 1024 * 1024 * 10) return b # Object store stats # big_obj is owned by node_0 # big_obj is stored in memory (no primary copy) big_obj_ref = big_obj.remote() # noqa: F841 wait_for_condition( lambda: get_owner_info(node_ids) == ([(2, 0), (1, 0), (0, 0)], [1, 1, 0]) ) # Test nested with task (small output) @ray.remote(resources={"node_1": 1}) def nest_task(s): @ray.remote(resources={"node_2": 1}) def task(): return [1] * s # t is owned by node_1 # if s is small, # then it's is stored in memory of node_1 (no primary copy) # else it's stored in object store of node_1 t = task.remote() return t # nest_ref is owned by node_0 # nest_ref is stored in memory (no primary copy) nest_ref = nest_task.remote(1) # noqa: F841 wait_for_condition( lambda: get_owner_info(node_ids) == ([(3, 0), (2, 0), (0, 0)], [1, 1, 0]) ) big_nest = nest_task.remote(1024 * 1024 * 10) # noqa: F841 wait_for_condition( lambda: get_owner_info(node_ids) == ([(4, 0), (3, 0), (0, 0)], [1, 1, 1]) ) @ray.remote(resources={"node_2": 0.5}, num_cpus=0) class A: def ready(self): return def gen(self): return ray.put(10) # actor is owned by node_0 # actor is not an object, so no object store copies actor = A.remote() # noqa: F841 ray.get(actor.ready.remote()) wait_for_condition( lambda: get_owner_info(node_ids) == ([(4, 1), (3, 0), (0, 0)], [1, 1, 1]) ) # Test with detached owned # detached actor is owned by GCS. So it's not counted in the owner stats detached_actor = A.options(lifetime="detached", name="A").remote() ray.get(detached_actor.ready.remote()) for i in range(3): assert get_owner_info(node_ids) == ([(4, 1), (3, 0), (0, 0)], [1, 1, 1]) import time time.sleep(1) # gen_obj is owned by node_0 # the inner object is owned by A (node_2) # the inner object is stored in object store of node_2 gen_obj = detached_actor.gen.remote() # noqa: F841 wait_for_condition( lambda: get_owner_info(node_ids) == ([(5, 1), (3, 0), (1, 0)], [1, 1, 2]) ) def test_running_tasks(ray_start_cluster): NUM_NODES = 3 cluster = ray_start_cluster for i in range(NUM_NODES): cluster.add_node(True, resources={f"node_{i}": 1}) if i == 0: ray.init(address=cluster.address) node_ids = [] for i in range(NUM_NODES): @ray.remote(resources={f"node_{i}": 1}) def get_node_id(): return ray.get_runtime_context().get_node_id() node_ids.append(ray.get(get_node_id.remote())) @ray.remote def f(t): import time time.sleep(t) tasks = [ f.options(resources={"node_0": 1}).remote(0), f.options(resources={"node_1": 1}).remote(100000), f.options(resources={"node_2": 1}).remote(100000), ] ready, pending = ray.wait(tasks) assert len(ready) == 1 assert len(pending) == 2 node_addrs = { n["NodeID"]: (n["NodeManagerAddress"], n["NodeManagerPort"]) for n in ray.nodes() } def check(): for i in range(NUM_NODES): node_stats = ray._private.internal_api.node_stats( node_addrs[node_ids[i]][0], node_addrs[node_ids[i]][1], False ) if i == 0: assert ( sum( [ stats.num_running_tasks for stats in node_stats.core_workers_stats ] ) == 0 ) else: assert ( sum( [ stats.num_running_tasks for stats in node_stats.core_workers_stats ] ) == 1 ) return True wait_for_condition(check) def test_multi_node_metrics_export_port_discovery(ray_start_cluster): NUM_NODES = 3 cluster = ray_start_cluster nodes = [cluster.add_node() for _ in range(NUM_NODES)] nodes = { node.address_info["metrics_export_port"]: node.address_info for node in nodes } cluster.wait_for_nodes() ray.init(address=cluster.address) node_info_list = ray.nodes() for node_info in node_info_list: metrics_export_port = node_info["MetricsExportPort"] address_info = nodes[metrics_export_port] assert address_info["raylet_socket_name"] == node_info["RayletSocketName"] # Make sure we can ping Prometheus endpoints. def test_prometheus_endpoint(): response = requests.get( f"http://{build_address('localhost', metrics_export_port)}", # Fail the request early on if connection timeout timeout=1.0, ) return response.status_code == 200 assert wait_until_succeeded_without_exception( test_prometheus_endpoint, (requests.exceptions.ConnectionError,), # The dashboard takes more than 2s to startup. timeout_ms=10 * 1000, ) def test_opentelemetry_conflict(shutdown_only): ray.init() # After ray.init(), opencensus protobuf should not be registered. # Otherwise, it might conflict with other versions generated opencensus protobuf. from google.protobuf.descriptor_pool import Default as DefaultPool pool = DefaultPool() try: found_file = pool.FindFileByName("opencensus/proto/resource/v1/resource.proto") except KeyError: found_file = None assert found_file is None, "opencensus protobuf registered after ray.init()" # Make sure the similar resource protobuf also doesn't raise an exception. from opentelemetry.proto.resource.v1 import resource_pb2 # noqa if __name__ == "__main__": sys.exit(pytest.main(["-sv", __file__]))