import asyncio import json import os import sys import tempfile from collections import defaultdict from concurrent.futures import ThreadPoolExecutor from pathlib import Path import pytest import requests import ray from ray._common.test_utils import wait_for_condition from ray._private.profiling import chrome_tracing_dump from ray._private.test_utils import ( check_call_subprocess, wait_for_aggregator_agent_if_enabled, ) from ray.util.state import ( get_actor, list_actors, list_nodes, list_tasks, list_workers, ) pytestmark = [ pytest.mark.parametrize( "event_routing_config", ["default", "aggregator"], indirect=True ), pytest.mark.usefixtures("event_routing_config"), ] def test_timeline(shutdown_only): ray.init(num_cpus=8) job_id = ray.get_runtime_context().get_job_id() TASK_SLEEP_TIME_S = 1 @ray.remote def f(): import time time.sleep(TASK_SLEEP_TIME_S) @ray.remote class Actor: def ready(self): pass @ray.remote class AsyncActor: async def f(self): await asyncio.sleep(5) async def g(self): await asyncio.sleep(5) @ray.remote class ThreadedActor: def f(self): import time time.sleep(5) def g(self): import time time.sleep(5) [f.remote() for _ in range(4)] a = Actor.remote() b = AsyncActor.remote() c = ThreadedActor.options(max_concurrency=15).remote() [a.ready.remote() for _ in range(4)] ray.get(b.f.remote()) [b.f.remote() for _ in range(4)] [b.g.remote() for _ in range(4)] [c.f.remote() for _ in range(4)] [c.g.remote() for _ in range(4)] result = json.loads(chrome_tracing_dump(list_tasks(detail=True))) # ph is the type of the event actor_to_events = defaultdict(list) task_to_events = defaultdict(list) index_to_workers = {} index_to_nodes = {} for item in result: if item["ph"] == "M": # metadata event name = item["name"] if name == "thread_name": index_to_workers[item["tid"]] = item["args"]["name"] elif name == "process_name": index_to_nodes[item["pid"]] = item["args"]["name"] else: raise ValueError(f"Unexecpted name from metadata event {name}") elif item["ph"] == "X": # regular interval event actor_id = item["args"]["actor_id"] assert "actor_id" in item["args"] assert "attempt_number" in item["args"] assert "func_or_class_name" in item["args"] assert "job_id" in item["args"] assert "task_id" in item["args"] if actor_id: actor_to_events[actor_id].append(item) else: task_to_events[item["args"]["task_id"]].append(item) else: raise ValueError(f"Unexpected event type {item['ph']}") actors = {actor["actor_id"]: actor for actor in list_actors(detail=True)} tasks = {task["task_id"]: task for task in list_tasks(detail=True)} workers = {worker["worker_id"]: worker for worker in list_workers(detail=True)} nodes = {node["node_ip"]: node for node in list_nodes(detail=True)} for actor_id, events in actor_to_events.items(): # Event type is tested from test_advanced.py::test_profiling_api for event in events: # Make sure actor id is correctly set. assert event["args"]["actor_id"] == actor_id assert event["args"]["job_id"] == job_id task_id = event["args"]["task_id"] assert ( event["args"]["func_or_class_name"] == tasks[task_id]["func_or_class_name"] ) # noqa # Make sure the worker id is correct. # ID is recorded as [worker_type]:[worker_id] worker_id_from_event = index_to_workers[event["tid"]].split(":")[1] # Node is recorded as Node [ip_address] node_id_from_event = index_to_nodes[event["pid"]].split(" ")[1] assert actors[actor_id]["pid"] == workers[worker_id_from_event]["pid"] assert actors[actor_id]["node_id"] == nodes[node_id_from_event]["node_id"] for task_id, events in task_to_events.items(): for event in events: # Make sure actor id is correctly set. assert event["args"]["job_id"] == job_id task_id = event["args"]["task_id"] assert ( event["args"]["func_or_class_name"] == tasks[task_id]["func_or_class_name"] ) # noqa # Make sure the duration is correct. # duration is in microseconds. # Since the task sleeps for TASK_SLEEP_TIME_S, # task:execute should have a similar sleep time. if event["cat"] == "task:execute": assert ( TASK_SLEEP_TIME_S * 1e6 * 0.9 < event["dur"] < TASK_SLEEP_TIME_S * 1e6 * 1.1 ) # noqa # Make sure the worker id is correct. worker_id_from_event = index_to_workers[event["tid"]].split(":")[1] node_id_from_event = index_to_nodes[event["pid"]].split(" ")[1] assert tasks[task_id]["worker_id"] == worker_id_from_event assert tasks[task_id]["node_id"] == nodes[node_id_from_event]["node_id"] # Verify the number of metadata events are correct. metadata_events = list(filter(lambda e: e["ph"] == "M", result)) assert len(metadata_events) == len(index_to_workers) + len(index_to_nodes) def test_timeline_request(shutdown_only): context = ray.init() dashboard_url = f"http://{context['webui_url']}" @ray.remote def f(): pass ray.get([f.remote() for _ in range(5)]) # Make sure the API works. def verify(): resp = requests.get(f"{dashboard_url}/api/v0/tasks/timeline") resp.raise_for_status() assert resp.json(), "No result has returned" return True wait_for_condition(verify, timeout=10) def test_actor_repr_name(shutdown_only): def _verify_repr_name(id, name): actor = get_actor(id=id) assert actor is not None assert actor["repr_name"] == name return True # Assert simple actor repr name @ray.remote class ReprActor: def __init__(self, x) -> None: self.x = x def __repr__(self) -> str: return self.x def ready(self): pass a = ReprActor.remote(x="repr-name-a") b = ReprActor.remote(x="repr-name-b") wait_for_condition(_verify_repr_name, id=a._actor_id.hex(), name="repr-name-a") wait_for_condition(_verify_repr_name, id=b._actor_id.hex(), name="repr-name-b") # Assert when no __repr__ defined. repr_name should be empty @ray.remote class Actor: pass a = Actor.remote() wait_for_condition(_verify_repr_name, id=a._actor_id.hex(), name="") # Assert special actors (async actor, threaded actor, detached actor, named actor) @ray.remote class AsyncActor: def __init__(self, x) -> None: self.x = x def __repr__(self) -> str: return self.x async def ready(self): pass a = AsyncActor.remote(x="async-x") wait_for_condition(_verify_repr_name, id=a._actor_id.hex(), name="async-x") a = ReprActor.options(max_concurrency=3).remote(x="x") wait_for_condition(_verify_repr_name, id=a._actor_id.hex(), name="x") a = ReprActor.options(name="named-actor").remote(x="repr-name") wait_for_condition(_verify_repr_name, id=a._actor_id.hex(), name="repr-name") a = ReprActor.options(name="detached-actor", lifetime="detached").remote( x="repr-name" ) wait_for_condition(_verify_repr_name, id=a._actor_id.hex(), name="repr-name") ray.kill(a) # Assert nested actor class. class OutClass: @ray.remote class InnerActor: def __init__(self, name) -> None: self.name = name def __repr__(self) -> str: return self.name def get_actor(self, name): return OutClass.InnerActor.remote(name=name) a = OutClass().get_actor(name="inner") wait_for_condition(_verify_repr_name, id=a._actor_id.hex(), name="inner") def test_experimental_import_deprecation(): for name in list(sys.modules): if name.startswith("ray.experimental.state"): sys.modules.pop(name, None) with pytest.warns(DeprecationWarning): from ray.experimental.state.api import list_tasks # noqa: F401 with pytest.warns(DeprecationWarning): from ray.experimental.state.common import DEFAULT_RPC_TIMEOUT # noqa: F401 with pytest.warns(DeprecationWarning): from ray.experimental.state.custom_types import ACTOR_STATUS # noqa: F401 with pytest.warns(DeprecationWarning): from ray.experimental.state.exception import RayStateApiException # noqa: F401 with pytest.warns(DeprecationWarning): from ray.experimental.state.state_cli import ray_get # noqa: F401 with pytest.warns(DeprecationWarning): from ray.experimental.state.state_manager import ( # noqa: F401 StateDataSourceClient, ) with pytest.warns(DeprecationWarning): from ray.experimental.state.util import convert_string_to_type # noqa: F401 def test_actor_task_with_repr_name(ray_start_with_dashboard): wait_for_aggregator_agent_if_enabled( ray_start_with_dashboard["gcs_address"], ray_start_with_dashboard["node_id"] ) @ray.remote class ReprActor: def __init__(self, x) -> None: self.x = x def __repr__(self) -> str: return self.x def f(self): pass a = ReprActor.remote(x="repr-name-a") ray.get(a.f.remote()) def verify(): tasks = list_tasks(detail=True, filters=[("type", "=", "ACTOR_TASK")]) assert len(tasks) == 1, tasks assert tasks[0].name == "repr-name-a.f" assert tasks[0].func_or_class_name == "ReprActor.f" return True wait_for_condition(verify) b = ReprActor.remote(x="repr-name-b") ray.get(b.f.options(name="custom-name").remote()) def verify(): tasks = list_tasks( detail=True, filters=[("actor_id", "=", b._actor_id.hex()), ("type", "=", "ACTOR_TASK")], ) assert len(tasks) == 1, tasks assert tasks[0].name == "custom-name" assert tasks[0].func_or_class_name == "ReprActor.f" return True wait_for_condition(verify) @ray.remote class Actor: def f(self): pass c = Actor.remote() ray.get(c.f.remote()) def verify(): tasks = list_tasks( detail=True, filters=[("actor_id", "=", c._actor_id.hex()), ("type", "=", "ACTOR_TASK")], ) assert len(tasks) == 1, tasks assert tasks[0].name == "Actor.f" assert tasks[0].func_or_class_name == "Actor.f" return True wait_for_condition(verify) @pytest.mark.skipif( sys.platform == "win32", reason="Release test not expected to work on non-linux." ) def test_state_api_scale_smoke(shutdown_only, monkeypatch): address_info = ray.init() wait_for_aggregator_agent_if_enabled( address_info["gcs_address"], address_info["node_id"] ) monkeypatch.setenv("RAY_ADDRESS", address_info["gcs_address"]) release_test_file_path = ( "../../release/nightly_tests/stress_tests/test_state_api_scale.py" ) full_path = Path(ray.__file__).parents[0] / release_test_file_path assert full_path.exists() check_call_subprocess(["python", str(full_path), "--smoke-test"]) def test_ray_timeline(shutdown_only): context = ray.init(num_cpus=8) wait_for_aggregator_agent_if_enabled(context["gcs_address"], context["node_id"]) @ray.remote def f(): import time time.sleep(0.1) ray.get(f.remote()) with tempfile.TemporaryDirectory() as tmpdirname: filename = os.path.join(tmpdirname, "timeline.json") def verify(): ray.timeline(filename) with open(filename, "r") as timeline_file: dumped = json.load(timeline_file) # TODO(swang): Check actual content. It doesn't seem to match the # return value of chrome_tracing_dump in above tests? assert len(dumped) > 0 return True wait_for_condition(verify, timeout=20, retry_interval_ms=1000) def test_state_init_multiple_threads(shutdown_only): ray.init() global_state = ray._private.state.state global_state._connect_and_get_accessor() gcs_options = global_state.gcs_options def disconnect(): global_state.disconnect() global_state._initialize_global_state(gcs_options) return True def get_nodes_from_state_api(): try: return len(global_state.node_table()) == 1 except ray.exceptions.RaySystemError: # There's a gap between disconnect and _initialize_global_state # and this will be raised if we try to connect during that gap return True disconnect() with ThreadPoolExecutor(max_workers=50) as executor: futures = [executor.submit(get_nodes_from_state_api) for _ in range(50)] futures.extend([executor.submit(disconnect) for _ in range(50)]) futures.extend([executor.submit(get_nodes_from_state_api) for _ in range(50)]) results = [future.result() for future in futures] # Assert that all calls returned True assert all(results) assert len(results) == 150 if __name__ == "__main__": sys.exit(pytest.main(["-sv", __file__]))