1848 lines
56 KiB
Python
1848 lines
56 KiB
Python
import json
|
|
import logging
|
|
import os
|
|
import queue
|
|
import sys
|
|
import threading
|
|
import time
|
|
import warnings
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from typing import List, Optional
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import click
|
|
import pytest
|
|
import yaml
|
|
from click.testing import CliRunner
|
|
|
|
import ray
|
|
import ray._private.ray_constants as ray_constants
|
|
import ray._private.state as global_state
|
|
from ray._common.network_utils import find_free_port, parse_address
|
|
from ray._common.test_utils import (
|
|
SignalActor,
|
|
async_wait_for_condition,
|
|
wait_for_condition,
|
|
)
|
|
from ray._private.grpc_utils import init_grpc_channel
|
|
from ray._private.state_api_test_utils import create_api_options
|
|
from ray._raylet import GcsClient, NodeID
|
|
from ray.cluster_utils import cluster_not_supported
|
|
from ray.core.generated.common_pb2 import (
|
|
Address,
|
|
CoreWorkerStats,
|
|
ObjectRefInfo,
|
|
TaskInfoEntry,
|
|
TaskStatus,
|
|
TaskType,
|
|
WorkerType,
|
|
)
|
|
from ray.core.generated.gcs_pb2 import (
|
|
ActorTableData,
|
|
GcsNodeInfo,
|
|
PlacementGroupTableData,
|
|
TaskEvents,
|
|
TaskStateUpdate,
|
|
WorkerTableData,
|
|
)
|
|
from ray.core.generated.gcs_service_pb2 import (
|
|
GcsStatus,
|
|
GetAllActorInfoReply,
|
|
GetTaskEventsReply,
|
|
)
|
|
from ray.core.generated.runtime_env_agent_pb2 import GetRuntimeEnvsInfoReply
|
|
from ray.core.generated.runtime_env_common_pb2 import (
|
|
RuntimeEnvState as RuntimeEnvStateProto,
|
|
)
|
|
from ray.dashboard.state_aggregator import StateAPIManager
|
|
from ray.dashboard.state_api_utils import convert_filters_type
|
|
from ray.dashboard.utils import ray_address_to_api_server_url
|
|
from ray.job_submission import JobSubmissionClient
|
|
from ray.util.scheduling_strategies import PlacementGroupSchedulingStrategy
|
|
from ray.util.state import (
|
|
StateApiClient,
|
|
get_actor,
|
|
list_actors,
|
|
list_cluster_events,
|
|
list_jobs,
|
|
list_nodes,
|
|
list_objects,
|
|
list_placement_groups,
|
|
list_runtime_envs,
|
|
list_tasks,
|
|
list_workers,
|
|
summarize_actors,
|
|
summarize_objects,
|
|
summarize_tasks,
|
|
)
|
|
from ray.util.state.common import (
|
|
ActorState,
|
|
Humanify,
|
|
ObjectState,
|
|
RuntimeEnvState,
|
|
StateResource,
|
|
StateSchema,
|
|
state_column,
|
|
)
|
|
from ray.util.state.exception import DataSourceUnavailable, RayStateApiException
|
|
from ray.util.state.state_cli import (
|
|
AvailableFormat,
|
|
_normalize_filter_keys,
|
|
_parse_filter,
|
|
format_list_api_output,
|
|
ray_get,
|
|
ray_list,
|
|
summary_state_cli_group,
|
|
)
|
|
from ray.util.state.state_manager import StateDataSourceClient
|
|
|
|
"""
|
|
Unit tests
|
|
"""
|
|
|
|
|
|
@pytest.fixture
|
|
def state_api_manager():
|
|
data_source_client = AsyncMock(StateDataSourceClient)
|
|
manager = StateAPIManager(
|
|
data_source_client, thread_pool_executor=ThreadPoolExecutor()
|
|
)
|
|
yield manager
|
|
|
|
|
|
def state_source_client(gcs_address):
|
|
GRPC_CHANNEL_OPTIONS = (
|
|
*ray_constants.GLOBAL_GRPC_OPTIONS,
|
|
("grpc.max_send_message_length", ray_constants.GRPC_CPP_MAX_MESSAGE_SIZE),
|
|
("grpc.max_receive_message_length", ray_constants.GRPC_CPP_MAX_MESSAGE_SIZE),
|
|
)
|
|
gcs_channel = init_grpc_channel(
|
|
gcs_address, GRPC_CHANNEL_OPTIONS, asynchronous=True
|
|
)
|
|
gcs_client = GcsClient(address=gcs_address)
|
|
client = StateDataSourceClient(gcs_channel=gcs_channel, gcs_client=gcs_client)
|
|
return client
|
|
|
|
|
|
def generate_actor_data(id, state=ActorTableData.ActorState.ALIVE, class_name="class"):
|
|
return ActorTableData(
|
|
actor_id=id,
|
|
state=state,
|
|
name="abc",
|
|
pid=1234,
|
|
class_name=class_name,
|
|
address=Address(node_id=id, ip_address="127.0.0.1", port=124, worker_id=id),
|
|
job_id=b"123",
|
|
node_id=None,
|
|
ray_namespace="",
|
|
)
|
|
|
|
|
|
def generate_pg_data(
|
|
id,
|
|
name="abc",
|
|
topology_strategy=None,
|
|
topology_assignments=None,
|
|
):
|
|
return PlacementGroupTableData(
|
|
placement_group_id=id,
|
|
state=PlacementGroupTableData.PlacementGroupState.CREATED,
|
|
name=name,
|
|
creator_job_dead=True,
|
|
creator_actor_dead=False,
|
|
topology_strategy=topology_strategy or {},
|
|
topology_assignments=topology_assignments or {},
|
|
)
|
|
|
|
|
|
def generate_node_data(id):
|
|
return GcsNodeInfo(
|
|
node_id=id,
|
|
state=GcsNodeInfo.GcsNodeState.ALIVE,
|
|
node_manager_address="127.0.0.1",
|
|
raylet_socket_name="abcd",
|
|
object_store_socket_name="False",
|
|
)
|
|
|
|
|
|
def generate_worker_data(
|
|
id,
|
|
pid=1234,
|
|
worker_launch_time_ms=1,
|
|
worker_launched_time_ms=2,
|
|
start_time_ms=3,
|
|
end_time_ms=4,
|
|
):
|
|
return WorkerTableData(
|
|
worker_address=Address(
|
|
node_id=id, ip_address="127.0.0.1", port=124, worker_id=id
|
|
),
|
|
is_alive=True,
|
|
timestamp=1234,
|
|
worker_type=WorkerType.WORKER,
|
|
pid=pid,
|
|
exit_type=None,
|
|
worker_launch_time_ms=worker_launch_time_ms,
|
|
worker_launched_time_ms=worker_launched_time_ms,
|
|
start_time_ms=start_time_ms,
|
|
end_time_ms=end_time_ms,
|
|
)
|
|
|
|
|
|
def generate_task_event(
|
|
id,
|
|
name="class",
|
|
func_or_class="class",
|
|
state=TaskStatus.PENDING_NODE_ASSIGNMENT,
|
|
type=TaskType.NORMAL_TASK,
|
|
node_id=NodeID.from_random(),
|
|
attempt_number=0,
|
|
job_id=b"0001",
|
|
):
|
|
if node_id is not None:
|
|
node_id = node_id.binary()
|
|
|
|
task_info = TaskInfoEntry(
|
|
task_id=id,
|
|
name=name,
|
|
func_or_class_name=func_or_class,
|
|
type=type,
|
|
)
|
|
state_updates = TaskStateUpdate(
|
|
node_id=node_id,
|
|
state_ts_ns={state: 1},
|
|
)
|
|
return TaskEvents(
|
|
task_id=id,
|
|
job_id=job_id,
|
|
attempt_number=attempt_number,
|
|
task_info=task_info,
|
|
state_updates=state_updates,
|
|
)
|
|
|
|
|
|
def generate_task_data(events_by_task):
|
|
return GetTaskEventsReply(
|
|
status=GcsStatus(),
|
|
events_by_task=events_by_task,
|
|
num_status_task_events_dropped=0,
|
|
num_profile_task_events_dropped=0,
|
|
num_total_stored=len(events_by_task),
|
|
)
|
|
|
|
|
|
def generate_failure_test_data():
|
|
return GetTaskEventsReply(
|
|
status=GcsStatus(code=34, message="Unknown filter predicate"),
|
|
events_by_task=[],
|
|
num_status_task_events_dropped=0,
|
|
num_profile_task_events_dropped=0,
|
|
num_total_stored=0,
|
|
num_filtered_on_gcs=0,
|
|
num_truncated=0,
|
|
)
|
|
|
|
|
|
def generate_early_return_task_data():
|
|
return GetTaskEventsReply(
|
|
num_profile_task_events_dropped=0,
|
|
num_status_task_events_dropped=0,
|
|
num_total_stored=0,
|
|
num_filtered_on_gcs=0,
|
|
num_truncated=0,
|
|
)
|
|
|
|
|
|
def generate_object_info(
|
|
obj_id,
|
|
size_bytes=1,
|
|
callsite="main.py",
|
|
task_state=TaskStatus.PENDING_NODE_ASSIGNMENT,
|
|
local_ref_count=1,
|
|
attempt_number=1,
|
|
pid=1234,
|
|
ip="1234",
|
|
worker_type=WorkerType.DRIVER,
|
|
pinned_in_memory=True,
|
|
):
|
|
return CoreWorkerStats(
|
|
pid=pid,
|
|
worker_type=worker_type,
|
|
ip_address=ip,
|
|
object_refs=[
|
|
ObjectRefInfo(
|
|
object_id=obj_id,
|
|
call_site=callsite,
|
|
object_size=size_bytes,
|
|
local_ref_count=local_ref_count,
|
|
submitted_task_ref_count=1,
|
|
contained_in_owned=[],
|
|
pinned_in_memory=pinned_in_memory,
|
|
task_status=task_state,
|
|
attempt_number=attempt_number,
|
|
)
|
|
],
|
|
)
|
|
|
|
|
|
def generate_runtime_env_info(runtime_env, creation_time=None, success=True):
|
|
return GetRuntimeEnvsInfoReply(
|
|
runtime_env_states=[
|
|
RuntimeEnvStateProto(
|
|
runtime_env=runtime_env.serialize(),
|
|
ref_cnt=1,
|
|
success=success,
|
|
error=None,
|
|
creation_time_ms=creation_time,
|
|
)
|
|
],
|
|
total=1,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_ray_address_to_api_server_url(shutdown_only):
|
|
ctx = ray.init()
|
|
api_server_url = f'http://{ctx.address_info["webui_url"]}'
|
|
address = ctx.address_info["address"]
|
|
gcs_address = ctx.address_info["gcs_address"]
|
|
|
|
# None should auto detect current ray address
|
|
assert api_server_url == ray_address_to_api_server_url(None)
|
|
# 'auto' should get
|
|
assert api_server_url == ray_address_to_api_server_url("auto")
|
|
# ray address
|
|
assert api_server_url == ray_address_to_api_server_url(address)
|
|
# explicit head node gcs address
|
|
assert api_server_url == ray_address_to_api_server_url(gcs_address)
|
|
# localhost string
|
|
_, gcs_port = parse_address(gcs_address)
|
|
assert api_server_url == ray_address_to_api_server_url(f"localhost:{gcs_port}")
|
|
|
|
|
|
def test_state_schema():
|
|
import pydantic
|
|
from pydantic.dataclasses import dataclass
|
|
|
|
@dataclass
|
|
class TestSchema(StateSchema):
|
|
column_a: int
|
|
column_b: int = state_column(filterable=False)
|
|
column_c: int = state_column(filterable=True)
|
|
column_d: int = state_column(filterable=False, detail=False)
|
|
column_f: int = state_column(filterable=True, detail=False)
|
|
column_e: int = state_column(filterable=False, detail=True)
|
|
column_g: int = state_column(filterable=True, detail=True)
|
|
|
|
# Correct input validation should work without an exception.
|
|
TestSchema(
|
|
column_a=1,
|
|
column_b=1,
|
|
column_c=1,
|
|
column_d=1,
|
|
column_e=1,
|
|
column_f=1,
|
|
column_g=1,
|
|
)
|
|
|
|
# Incorrect input type.
|
|
with pytest.raises(pydantic.ValidationError):
|
|
TestSchema(
|
|
column_a=1,
|
|
column_b=1,
|
|
column_c=1,
|
|
column_d=1,
|
|
column_e=1,
|
|
column_f=1,
|
|
column_g="a",
|
|
)
|
|
|
|
assert TestSchema.filterable_columns() == {
|
|
"column_c",
|
|
"column_f",
|
|
"column_g",
|
|
}
|
|
|
|
assert TestSchema.base_columns() == {
|
|
"column_a",
|
|
"column_b",
|
|
"column_c",
|
|
"column_d",
|
|
"column_f",
|
|
}
|
|
|
|
assert TestSchema.columns() == {
|
|
"column_a",
|
|
"column_b",
|
|
"column_c",
|
|
"column_d",
|
|
"column_e",
|
|
"column_f",
|
|
"column_g",
|
|
}
|
|
|
|
|
|
def test_parse_filter():
|
|
# Basic
|
|
assert _parse_filter("key=value") == ("key", "=", "value")
|
|
assert _parse_filter("key!=value") == ("key", "!=", "value")
|
|
|
|
# Predicate =
|
|
assert _parse_filter("key=value=123=1") == ("key", "=", "value=123=1")
|
|
assert _parse_filter("key=value!=123!=1") == ("key", "=", "value!=123!=1")
|
|
assert _parse_filter("key=value!=123=1") == ("key", "=", "value!=123=1")
|
|
assert _parse_filter("key=value!=123=1!") == ("key", "=", "value!=123=1!")
|
|
assert _parse_filter("key=value!=123=1=") == ("key", "=", "value!=123=1=")
|
|
assert _parse_filter("key=value!=123=1!=") == ("key", "=", "value!=123=1!=")
|
|
|
|
# Predicate !=
|
|
assert _parse_filter("key!=value=123=1") == ("key", "!=", "value=123=1")
|
|
assert _parse_filter("key!=value!=123!=1") == ("key", "!=", "value!=123!=1")
|
|
assert _parse_filter("key!=value!=123=1") == ("key", "!=", "value!=123=1")
|
|
assert _parse_filter("key!=value!=123=1!") == ("key", "!=", "value!=123=1!")
|
|
assert _parse_filter("key!=value!=123=1=") == ("key", "!=", "value!=123=1=")
|
|
assert _parse_filter("key!=value!=123=1!=") == ("key", "!=", "value!=123=1!=")
|
|
|
|
# Incorrect cases
|
|
with pytest.raises(ValueError):
|
|
_parse_filter("keyvalue")
|
|
|
|
with pytest.raises(ValueError):
|
|
_parse_filter("keyvalue!")
|
|
with pytest.raises(ValueError):
|
|
_parse_filter("keyvalue!=")
|
|
with pytest.raises(ValueError):
|
|
_parse_filter("keyvalue=")
|
|
|
|
with pytest.raises(ValueError):
|
|
_parse_filter("!keyvalue")
|
|
with pytest.raises(ValueError):
|
|
_parse_filter("!=keyvalue")
|
|
with pytest.raises(ValueError):
|
|
_parse_filter("=keyvalue")
|
|
|
|
with pytest.raises(ValueError):
|
|
_parse_filter("=keyvalue=")
|
|
with pytest.raises(ValueError):
|
|
_parse_filter("!=keyvalue=")
|
|
with pytest.raises(ValueError):
|
|
_parse_filter("=keyvalue!=")
|
|
with pytest.raises(ValueError):
|
|
_parse_filter("!=keyvalue!=")
|
|
|
|
with pytest.raises(ValueError):
|
|
_parse_filter("key>value")
|
|
with pytest.raises(ValueError):
|
|
_parse_filter("key>value!=")
|
|
|
|
|
|
# Without this, capsys will have a race condition
|
|
# that causes
|
|
# ValueError: I/O operation on closed file.
|
|
@pytest.fixture
|
|
def clear_loggers():
|
|
"""Remove handlers from all loggers"""
|
|
yield
|
|
loggers = [logging.getLogger()] + list(logging.Logger.manager.loggerDict.values())
|
|
for logger in loggers:
|
|
handlers = getattr(logger, "handlers", [])
|
|
for handler in handlers:
|
|
logger.removeHandler(handler)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_state_api_client_periodic_warning(shutdown_only, capsys, clear_loggers):
|
|
ray.init()
|
|
timeout = 10
|
|
StateApiClient()._make_http_get_request("/api/v0/delay/5", {}, timeout, True)
|
|
captured = capsys.readouterr()
|
|
lines = captured.err.strip().split("\n")
|
|
# Lines are printed 1.25, 2.5, and 5 seconds.
|
|
# First line is the dashboard start log.
|
|
# INFO services.py:1477 -- View the Ray dashboard at http://127.0.0.1:8265
|
|
print(lines)
|
|
|
|
expected_elapsed = [1.25, 2.5, 5.0]
|
|
expected_lines = []
|
|
for elapsed in expected_elapsed:
|
|
expected_lines.append(
|
|
f"({elapsed} / 10 seconds) Waiting for the "
|
|
"response from the API "
|
|
"server address http://127.0.0.1:8265/api/v0/delay/5."
|
|
)
|
|
for expected_line in expected_lines:
|
|
expected_line in lines
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
("exception", "status_code"),
|
|
[
|
|
(None, 200),
|
|
(ValueError("Invalid filter parameter"), 400),
|
|
(DataSourceUnavailable("GCS connection failed"), 500),
|
|
],
|
|
)
|
|
async def test_handle_list_api_status_codes(
|
|
exception: Optional[Exception], status_code: int
|
|
):
|
|
"""Test that handle_list_api calls do_reply with correct status codes.
|
|
|
|
This directly tests the HTTP layer logic that maps exceptions to status codes:
|
|
- Success → HTTP 200 OK
|
|
- ValueError → HTTP 400 BAD_REQUEST
|
|
- DataSourceUnavailable → HTTP 500 INTERNAL_ERROR
|
|
"""
|
|
from ray.dashboard.state_api_utils import handle_list_api
|
|
from ray.util.state.common import ListApiResponse
|
|
|
|
# 1. Mock aiohttp request with proper query interface
|
|
mock_request = MagicMock()
|
|
|
|
def mock_get(key, default=None):
|
|
return default
|
|
|
|
mock_request.query = MagicMock()
|
|
mock_request.query.get = mock_get
|
|
|
|
# 2. Mock response whether success or failure.
|
|
if exception is None:
|
|
mock_backend = AsyncMock(
|
|
return_value=ListApiResponse(
|
|
result=[],
|
|
total=0,
|
|
num_after_truncation=0,
|
|
num_filtered=0,
|
|
partial_failure_warning="",
|
|
)
|
|
)
|
|
else:
|
|
mock_backend = AsyncMock(side_effect=exception)
|
|
|
|
response = await handle_list_api(mock_backend, mock_request)
|
|
|
|
# 3. Assert status_code is correct.
|
|
assert response.status == status_code
|
|
|
|
|
|
def test_type_conversion():
|
|
# Test string
|
|
r = convert_filters_type([("actor_id", "=", "123")], ActorState)
|
|
assert r[0][2] == "123"
|
|
r = convert_filters_type([("actor_id", "=", "abcd")], ActorState)
|
|
assert r[0][2] == "abcd"
|
|
r = convert_filters_type([("actor_id", "=", "True")], ActorState)
|
|
assert r[0][2] == "True"
|
|
|
|
# Test boolean
|
|
r = convert_filters_type([("success", "=", "1")], RuntimeEnvState)
|
|
assert r[0][2]
|
|
r = convert_filters_type([("success", "=", "True")], RuntimeEnvState)
|
|
assert r[0][2]
|
|
r = convert_filters_type([("success", "=", "true")], RuntimeEnvState)
|
|
assert r[0][2]
|
|
with pytest.raises(ValueError):
|
|
r = convert_filters_type([("success", "=", "random_string")], RuntimeEnvState)
|
|
r = convert_filters_type([("success", "=", "false")], RuntimeEnvState)
|
|
assert r[0][2] is False
|
|
r = convert_filters_type([("success", "=", "False")], RuntimeEnvState)
|
|
assert r[0][2] is False
|
|
r = convert_filters_type([("success", "=", "0")], RuntimeEnvState)
|
|
assert r[0][2] is False
|
|
|
|
# Test int
|
|
r = convert_filters_type([("pid", "=", "0")], ObjectState)
|
|
assert r[0][2] == 0
|
|
r = convert_filters_type([("pid", "=", "123")], ObjectState)
|
|
assert r[0][2] == 123
|
|
# Only integer can be provided.
|
|
with pytest.raises(ValueError):
|
|
r = convert_filters_type([("pid", "=", "123.3")], ObjectState)
|
|
with pytest.raises(ValueError):
|
|
r = convert_filters_type([("pid", "=", "abc")], ObjectState)
|
|
|
|
# currently, there's no schema that has float column.
|
|
|
|
|
|
def test_humanify():
|
|
raw_bytes = 1024
|
|
assert Humanify.memory(raw_bytes) == "1.000 KiB"
|
|
raw_bytes *= 1024
|
|
assert Humanify.memory(raw_bytes) == "1.000 MiB"
|
|
raw_bytes *= 1024
|
|
assert Humanify.memory(raw_bytes) == "1.000 GiB"
|
|
timestamp = 1610000000
|
|
assert "1970-01" in Humanify.timestamp(timestamp)
|
|
assert Humanify.duration(timestamp) == "18 days, 15:13:20"
|
|
|
|
|
|
def test_runtime_env_state_humanify_creation_time_ms():
|
|
state = {"creation_time_ms": 36639}
|
|
RuntimeEnvState.humanify(state)
|
|
assert state["creation_time_ms"] == "0:00:36.639000"
|
|
|
|
|
|
def is_hex(val):
|
|
try:
|
|
int_val = int(val, 16)
|
|
except ValueError:
|
|
return False
|
|
# Should remove leading 0 because when the value is converted back
|
|
# to hex, it is removed.
|
|
val = val.lstrip("0")
|
|
return f"0x{val}" == hex(int_val)
|
|
|
|
|
|
"""
|
|
Integration tests
|
|
"""
|
|
|
|
|
|
@pytest.mark.xfail(cluster_not_supported, reason="cluster not supported on Windows")
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_cli_apis_sanity_check(ray_start_cluster):
|
|
"""Test all of CLI APIs work as expected."""
|
|
NUM_NODES = 4
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=2)
|
|
ray.init(address=cluster.address)
|
|
for _ in range(NUM_NODES - 1):
|
|
cluster.add_node(num_cpus=2, dashboard_agent_listen_port=find_free_port())
|
|
runner = CliRunner()
|
|
|
|
client = JobSubmissionClient(
|
|
f"http://{ray._private.worker.global_worker.node.address_info['webui_url']}"
|
|
)
|
|
|
|
@ray.remote
|
|
def f():
|
|
time.sleep(30)
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
pass
|
|
|
|
obj = ray.put(3) # noqa
|
|
task = f.remote() # noqa
|
|
actor = Actor.remote() # noqa
|
|
actor_runtime_env = Actor.options( # noqa
|
|
runtime_env={"pip": ["requests"]}
|
|
).remote()
|
|
job_id = client.submit_job( # noqa
|
|
# Entrypoint shell command to execute
|
|
entrypoint="ls",
|
|
)
|
|
pg = ray.util.placement_group(bundles=[{"CPU": 1}]) # noqa
|
|
|
|
def verify_output(cmd, args: List[str], necessary_substrings: List[str]):
|
|
result = runner.invoke(cmd, args)
|
|
print(result)
|
|
exit_code_correct = result.exit_code == 0
|
|
substring_matched = all(
|
|
substr in result.output for substr in necessary_substrings
|
|
)
|
|
print(result.output)
|
|
return exit_code_correct and substring_matched
|
|
|
|
wait_for_condition(
|
|
lambda: verify_output(ray_list, ["actors"], ["Stats:", "Table:", "ACTOR_ID"])
|
|
)
|
|
# TODO(sang): Enable it.
|
|
# wait_for_condition(
|
|
# lambda: verify_output(
|
|
# ray_list, ["cluster-events"], ["Stats:", "Table:", "EVENT_ID"]
|
|
# )
|
|
# )
|
|
wait_for_condition(
|
|
lambda: verify_output(ray_list, ["workers"], ["Stats:", "Table:", "WORKER_ID"])
|
|
)
|
|
wait_for_condition(
|
|
lambda: verify_output(ray_list, ["nodes"], ["Stats:", "Table:", "NODE_ID"])
|
|
)
|
|
wait_for_condition(
|
|
lambda: verify_output(
|
|
ray_list, ["placement-groups"], ["Stats:", "Table:", "PLACEMENT_GROUP_ID"]
|
|
)
|
|
)
|
|
wait_for_condition(lambda: verify_output(ray_list, ["jobs"], ["raysubmit"]))
|
|
wait_for_condition(
|
|
lambda: verify_output(ray_list, ["tasks"], ["Stats:", "Table:", "TASK_ID"])
|
|
)
|
|
wait_for_condition(
|
|
lambda: verify_output(ray_list, ["objects"], ["Stats:", "Table:", "OBJECT_ID"])
|
|
)
|
|
wait_for_condition(
|
|
lambda: verify_output(
|
|
ray_list, ["runtime-envs"], ["Stats:", "Table:", "RUNTIME_ENV"]
|
|
)
|
|
)
|
|
|
|
# Test get node by id
|
|
nodes = ray.nodes()
|
|
wait_for_condition(
|
|
lambda: verify_output(
|
|
ray_get, ["nodes", nodes[0]["NodeID"]], ["node_id", nodes[0]["NodeID"]]
|
|
)
|
|
)
|
|
# Test get workers by id
|
|
workers = global_state.workers()
|
|
assert len(workers) > 0
|
|
worker_id = list(workers.keys())[0]
|
|
wait_for_condition(
|
|
lambda: verify_output(ray_get, ["workers", worker_id], ["worker_id", worker_id])
|
|
)
|
|
|
|
# Test get actors by id
|
|
wait_for_condition(
|
|
lambda: verify_output(
|
|
ray_get,
|
|
["actors", actor._actor_id.hex()],
|
|
["actor_id", actor._actor_id.hex()],
|
|
)
|
|
)
|
|
|
|
# Test get task by ID
|
|
wait_for_condition(
|
|
lambda: verify_output(
|
|
ray_get, ["tasks", task.task_id().hex()], ["task_id", task.task_id().hex()]
|
|
)
|
|
)
|
|
|
|
# Test get placement groups by id
|
|
wait_for_condition(
|
|
lambda: verify_output(
|
|
ray_get,
|
|
["placement-groups", pg.id.hex()],
|
|
["placement_group_id", pg.id.hex()],
|
|
)
|
|
)
|
|
|
|
# Test get objects by id
|
|
wait_for_condition(
|
|
lambda: verify_output(ray_get, ["objects", obj.hex()], ["object_id", obj.hex()])
|
|
)
|
|
|
|
# Test address flag auto detection
|
|
wait_for_condition(
|
|
lambda: verify_output(
|
|
ray_get,
|
|
["objects", obj.hex(), "--address", "auto"],
|
|
["object_id", obj.hex()],
|
|
)
|
|
)
|
|
wait_for_condition(
|
|
lambda: verify_output(
|
|
ray_list, ["tasks", "--address", "auto"], ["Stats:", "Table:", "TASK_ID"]
|
|
)
|
|
)
|
|
|
|
# TODO(rickyyx:alpha-obs):
|
|
# - get job by id: jobs is not currently filterable by id
|
|
# - get task by id: no easy access to tasks yet
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
sys.platform == "win32",
|
|
reason="Failed on Windows",
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"override_url",
|
|
[
|
|
"https://external_dashboard_url",
|
|
"https://external_dashboard_url/path1/?query_param1=val1&query_param2=val2",
|
|
"new_external_dashboard_url",
|
|
],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_state_api_with_external_dashboard_override(
|
|
shutdown_only, override_url, monkeypatch
|
|
):
|
|
with monkeypatch.context() as m:
|
|
if override_url:
|
|
m.setenv(
|
|
ray_constants.RAY_OVERRIDE_DASHBOARD_URL,
|
|
override_url,
|
|
)
|
|
|
|
ray.init()
|
|
|
|
@ray.remote
|
|
class A:
|
|
pass
|
|
|
|
a = A.remote() # noqa
|
|
|
|
def verify():
|
|
# Test list
|
|
actors = list_actors()
|
|
assert len(actors) == 1
|
|
assert actors[0]["state"] == "ALIVE"
|
|
assert is_hex(actors[0]["actor_id"])
|
|
assert a._actor_id.hex() == actors[0]["actor_id"]
|
|
|
|
# Test get
|
|
actors = list_actors(detail=True)
|
|
for actor in actors:
|
|
get_actor_data = get_actor(actor["actor_id"])
|
|
assert get_actor_data is not None
|
|
assert get_actor_data == actor
|
|
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
print(list_actors())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
async def test_cloud_envs(ray_start_cluster, monkeypatch):
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=1, node_name="head_node")
|
|
ray.init(address=cluster.address)
|
|
with monkeypatch.context() as m:
|
|
m.setenv(
|
|
"RAY_CLOUD_INSTANCE_ID",
|
|
"test_cloud_id",
|
|
)
|
|
m.setenv("RAY_NODE_TYPE_NAME", "test-node-type")
|
|
cluster.add_node(
|
|
num_cpus=1,
|
|
node_name="worker_node",
|
|
dashboard_agent_listen_port=find_free_port(),
|
|
)
|
|
client = state_source_client(cluster.address)
|
|
|
|
async def verify():
|
|
node_infos, _ = await client.get_all_node_info()
|
|
assert len(node_infos) == 2
|
|
for node_info in node_infos.values():
|
|
if node_info.node_name == "worker_node":
|
|
assert node_info.instance_id == "test_cloud_id"
|
|
assert node_info.node_type_name == "test-node-type"
|
|
else:
|
|
assert node_info.instance_id == ""
|
|
assert node_info.node_type_name == ""
|
|
|
|
return True
|
|
|
|
await async_wait_for_condition(verify)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_pg_worker_id_tasks(shutdown_only):
|
|
ray.init(num_cpus=1)
|
|
pg = ray.util.placement_group(bundles=[{"CPU": 1}])
|
|
pg.wait()
|
|
|
|
@ray.remote
|
|
def f():
|
|
pass
|
|
|
|
@ray.remote
|
|
class A:
|
|
def ready(self):
|
|
return os.getpid()
|
|
|
|
ray.get(
|
|
f.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
|
|
).remote()
|
|
)
|
|
|
|
def verify():
|
|
tasks = list_tasks(detail=True)
|
|
workers = list_workers(
|
|
filters=[("worker_type", "=", "WORKER")], raise_on_missing_output=False
|
|
)
|
|
assert len(tasks) == 1
|
|
assert len(workers) == 1
|
|
|
|
assert tasks[0]["placement_group_id"] == pg.id.hex()
|
|
assert tasks[0]["worker_id"] == workers[0]["worker_id"]
|
|
assert tasks[0]["worker_pid"] == workers[0]["pid"]
|
|
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
print(list_tasks(detail=True))
|
|
|
|
a = A.options(
|
|
scheduling_strategy=PlacementGroupSchedulingStrategy(placement_group=pg)
|
|
).remote()
|
|
pid = ray.get(a.ready.remote())
|
|
|
|
def verify():
|
|
actors = list_actors(detail=True)
|
|
workers = list_workers(
|
|
detail=True, filters=[("pid", "=", pid)], raise_on_missing_output=False
|
|
)
|
|
assert len(actors) == 1
|
|
assert len(workers) == 1
|
|
|
|
assert actors[0]["placement_group_id"] == pg.id.hex()
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
print(list_actors(detail=True))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_parent_task_id(shutdown_only):
|
|
"""Test parent task id set up properly"""
|
|
ray.init(num_cpus=2)
|
|
|
|
@ray.remote
|
|
def child():
|
|
pass
|
|
|
|
@ray.remote
|
|
def parent():
|
|
ray.get(child.remote())
|
|
|
|
ray.get(parent.remote())
|
|
|
|
def verify():
|
|
tasks = list_tasks(detail=True)
|
|
assert len(tasks) == 2, "Expect 2 tasks to finished"
|
|
parent_task_id = None
|
|
child_parent_task_id = None
|
|
for task in tasks:
|
|
if task["func_or_class_name"] == "parent":
|
|
parent_task_id = task["task_id"]
|
|
elif task["func_or_class_name"] == "child":
|
|
child_parent_task_id = task["parent_task_id"]
|
|
|
|
assert (
|
|
parent_task_id == child_parent_task_id
|
|
), "Child should have the parent task id"
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
sys.platform == "win32",
|
|
reason="Failed on Windows",
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_network_failure(shutdown_only):
|
|
"""When the request fails due to network failure,
|
|
verifies it raises an exception."""
|
|
ray.init()
|
|
|
|
@ray.remote
|
|
def f():
|
|
time.sleep(30)
|
|
|
|
a = [f.remote() for _ in range(4)] # noqa
|
|
wait_for_condition(lambda: len(list_tasks()) == 4)
|
|
|
|
# Kill raylet will not make list_tasks raise exceptions.
|
|
ray._private.worker._global_node.kill_raylet()
|
|
assert len(list_tasks()) == 4
|
|
|
|
# Kill GCS so that list_tasks will have network error on querying tasks.
|
|
ray._private.worker._global_node.kill_gcs_server()
|
|
|
|
with pytest.raises(ray.exceptions.RpcError):
|
|
list_tasks(_explain=True)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_network_partial_failures(monkeypatch, ray_start_cluster):
|
|
"""When the request fails due to network failure,
|
|
verifies it prints proper warning."""
|
|
with monkeypatch.context() as m:
|
|
# defer for 5s for the second node.
|
|
# This will help the API not return until the node is killed.
|
|
m.setenv(
|
|
"RAY_testing_asio_delay_us",
|
|
"NodeManagerService.grpc_server.GetObjectsInfo=5000000:5000000",
|
|
)
|
|
m.setenv("RAY_record_ref_creation_sites", "1")
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=2)
|
|
ray.init(address=cluster.address)
|
|
n = cluster.add_node(num_cpus=2)
|
|
|
|
@ray.remote
|
|
def f():
|
|
ray.put(1)
|
|
|
|
a = [f.remote() for _ in range(4)] # noqa
|
|
wait_for_condition(lambda: len(list_objects()) == 4)
|
|
|
|
# Make sure when there's 0 node failure, it doesn't print the error.
|
|
with warnings.catch_warnings(record=True) as record:
|
|
warnings.simplefilter("always")
|
|
list_objects(_explain=True)
|
|
assert len(record) == 0
|
|
|
|
# Kill raylet so that list_objects will have network error on querying raylets.
|
|
cluster.remove_node(n, allow_graceful=False)
|
|
|
|
with pytest.warns(UserWarning):
|
|
list_objects(raise_on_missing_output=False, _explain=True)
|
|
|
|
# Make sure when _explain == False, warning is not printed.
|
|
with warnings.catch_warnings(record=True) as record:
|
|
warnings.simplefilter("always")
|
|
list_objects(raise_on_missing_output=False, _explain=False)
|
|
assert len(record) == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_network_partial_failures_timeout(monkeypatch, ray_start_cluster):
|
|
"""When the request fails due to network timeout,
|
|
verifies it prints proper warning."""
|
|
monkeypatch.setenv("RAY_record_ref_creation_sites", "1")
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=2)
|
|
ray.init(address=cluster.address)
|
|
with monkeypatch.context() as m:
|
|
# defer for 10s for the second node.
|
|
m.setenv(
|
|
"RAY_testing_asio_delay_us",
|
|
"NodeManagerService.grpc_server.GetObjectsInfo=10000000:10000000",
|
|
)
|
|
cluster.add_node(num_cpus=2)
|
|
|
|
@ray.remote
|
|
def f():
|
|
ray.put(1)
|
|
|
|
a = [f.remote() for _ in range(4)] # noqa
|
|
|
|
def verify():
|
|
with warnings.catch_warnings(record=True) as record:
|
|
warnings.simplefilter("always")
|
|
list_objects(raise_on_missing_output=False, _explain=True, timeout=5)
|
|
return len(record) == 1
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cli_format_print(state_api_manager):
|
|
data_source_client = state_api_manager.data_source_client
|
|
actor_id = b"1234"
|
|
data_source_client.get_all_actor_info.return_value = GetAllActorInfoReply(
|
|
actor_table_data=[generate_actor_data(actor_id), generate_actor_data(b"12345")]
|
|
)
|
|
result = await state_api_manager.list_actors(option=create_api_options())
|
|
print(result)
|
|
result = [ActorState(**d) for d in result.result]
|
|
# If the format is not yaml, it will raise an exception.
|
|
yaml.safe_load(
|
|
format_list_api_output(result, schema=ActorState, format=AvailableFormat.YAML)
|
|
)
|
|
# If the format is not json, it will raise an exception.
|
|
json.loads(
|
|
format_list_api_output(result, schema=ActorState, format=AvailableFormat.JSON)
|
|
)
|
|
# Test a table formatting.
|
|
output = format_list_api_output(
|
|
result, schema=ActorState, format=AvailableFormat.TABLE
|
|
)
|
|
assert "Table:" in output
|
|
assert "Stats:" in output
|
|
with pytest.raises(ValueError):
|
|
format_list_api_output(result, schema=ActorState, format="random_format")
|
|
|
|
# Verify the default format.
|
|
output = format_list_api_output(result, schema=ActorState)
|
|
assert "Table:" in output
|
|
assert "Stats:" in output
|
|
|
|
# Verify the ordering is equal to it is defined in `StateSchema` class.
|
|
# Index 8 contains headers
|
|
headers = output.split("\n")[8]
|
|
cols = ActorState.list_columns()
|
|
headers = list(filter(lambda item: item != "", headers.strip().split(" ")))
|
|
|
|
for i in range(len(headers)):
|
|
header = headers[i].upper()
|
|
col = cols[i].upper()
|
|
assert header == col
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_filter(shutdown_only):
|
|
ray.init()
|
|
|
|
# Test unsupported predicates.
|
|
with pytest.raises(ValueError):
|
|
list_actors(filters=[("state", ">", "DEAD")])
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def __init__(self):
|
|
self.obj = None
|
|
|
|
def ready(self):
|
|
pass
|
|
|
|
def put(self):
|
|
self.obj = ray.put(123)
|
|
|
|
def getpid(self):
|
|
return os.getpid()
|
|
|
|
"""
|
|
Test basic case.
|
|
"""
|
|
a = Actor.remote()
|
|
b = Actor.remote()
|
|
|
|
a_pid = ray.get(a.getpid.remote())
|
|
b_pid = ray.get(b.getpid.remote())
|
|
|
|
ray.get([a.ready.remote(), b.ready.remote()])
|
|
ray.kill(b)
|
|
|
|
def verify():
|
|
result = list_actors(filters=[("state", "=", "DEAD")])
|
|
assert len(result) == 1
|
|
actor = result[0]
|
|
assert actor["pid"] == b_pid
|
|
|
|
result = list_actors(filters=[("state", "!=", "DEAD")])
|
|
assert len(result) == 1
|
|
actor = result[0]
|
|
assert actor["pid"] == a_pid
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
"""
|
|
Test filter with different types (integer/bool).
|
|
"""
|
|
obj_1 = ray.put(123) # noqa
|
|
ray.get(a.put.remote())
|
|
pid = ray.get(a.getpid.remote())
|
|
|
|
def verify():
|
|
# There's only 1 object.
|
|
result = list_objects(
|
|
filters=[("pid", "=", pid), ("reference_type", "=", "LOCAL_REFERENCE")]
|
|
)
|
|
return len(result) == 1
|
|
|
|
wait_for_condition(verify)
|
|
|
|
def verify():
|
|
workers = list_workers()
|
|
live_workers = list_workers(
|
|
filters=[("is_alive", "=", "true")], raise_on_missing_output=False
|
|
)
|
|
non_alive_workers = list_workers(
|
|
filters=[("is_alive", "!=", "true")], raise_on_missing_output=False
|
|
)
|
|
assert len(live_workers) + len(non_alive_workers) == len(workers)
|
|
|
|
live_workers = list_workers(
|
|
filters=[("is_alive", "=", "1")], raise_on_missing_output=False
|
|
)
|
|
non_alive_workers = list_workers(
|
|
filters=[("is_alive", "!=", "1")], raise_on_missing_output=False
|
|
)
|
|
assert len(live_workers) + len(non_alive_workers) == len(workers)
|
|
|
|
live_workers = list_workers(
|
|
filters=[("is_alive", "=", "True")], raise_on_missing_output=False
|
|
)
|
|
non_alive_workers = list_workers(
|
|
filters=[("is_alive", "!=", "True")], raise_on_missing_output=False
|
|
)
|
|
assert len(live_workers) + len(non_alive_workers) == len(workers)
|
|
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
"""
|
|
Test CLI
|
|
"""
|
|
dead_actor_id = list_actors(filters=[("state", "=", "DEAD")])[0]["actor_id"]
|
|
alive_actor_id = list_actors(filters=[("state", "=", "ALIVE")])[0]["actor_id"]
|
|
runner = CliRunner()
|
|
result = runner.invoke(ray_list, ["actors", "--filter", "state=DEAD"])
|
|
assert result.exit_code == 0
|
|
assert dead_actor_id in result.output
|
|
assert alive_actor_id not in result.output
|
|
|
|
result = runner.invoke(ray_list, ["actors", "--filter", "state!=DEAD"])
|
|
assert result.exit_code == 0
|
|
assert dead_actor_id not in result.output
|
|
assert alive_actor_id in result.output
|
|
|
|
"""
|
|
Test case insensitive match on string fields.
|
|
"""
|
|
|
|
@ray.remote
|
|
def task():
|
|
pass
|
|
|
|
ray.get(task.remote())
|
|
|
|
def verify():
|
|
result_1 = list_tasks(filters=[("name", "=", "task")])
|
|
result_2 = list_tasks(filters=[("name", "=", "TASK")])
|
|
assert result_1 == result_2
|
|
|
|
result_1 = list_tasks(filters=[("state", "=", "FINISHED")])
|
|
result_2 = list_tasks(filters=[("state", "=", "finished")])
|
|
assert result_1 == result_2
|
|
|
|
result_1 = list_objects(
|
|
filters=[("pid", "=", pid), ("reference_type", "=", "LOCAL_REFERENCE")]
|
|
)
|
|
|
|
result_2 = list_objects(
|
|
filters=[("pid", "=", pid), ("reference_type", "=", "local_reference")]
|
|
)
|
|
assert result_1 == result_2
|
|
|
|
result_1 = list_actors(filters=[("state", "=", "DEAD")])
|
|
result_2 = list_actors(filters=[("state", "=", "dead")])
|
|
|
|
assert result_1 == result_2
|
|
|
|
result_1 = list_actors(filters=[("state", "!=", "DEAD")])
|
|
result_2 = list_actors(filters=[("state", "!=", "dead")])
|
|
|
|
assert result_1 == result_2
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_data_truncate(shutdown_only, monkeypatch):
|
|
"""
|
|
Verify the data is properly truncated when there are too many entries to return.
|
|
"""
|
|
with monkeypatch.context() as m:
|
|
max_limit_data_source = 10
|
|
max_limit_api_server = 1000
|
|
m.setenv("RAY_MAX_LIMIT_FROM_API_SERVER", f"{max_limit_api_server}")
|
|
m.setenv("RAY_MAX_LIMIT_FROM_DATA_SOURCE", f"{max_limit_data_source}")
|
|
|
|
ray.init(num_cpus=16)
|
|
|
|
pgs = [ # noqa
|
|
ray.util.placement_group(bundles=[{"CPU": 0.001}])
|
|
for _ in range(max_limit_data_source + 1)
|
|
]
|
|
runner = CliRunner()
|
|
with pytest.warns(UserWarning) as record:
|
|
result = runner.invoke(ray_list, ["placement-groups"])
|
|
assert (
|
|
f"{max_limit_data_source} ({max_limit_data_source + 1} total "
|
|
"from the cluster) placement_groups are retrieved from the "
|
|
"data source. 1 entries have been truncated." in record[0].message.args[0]
|
|
)
|
|
assert result.exit_code == 0
|
|
|
|
# Make sure users cannot specify higher limit than MAX_LIMIT_FROM_API_SERVER
|
|
with pytest.raises(RayStateApiException):
|
|
list_placement_groups(limit=max_limit_api_server + 1)
|
|
|
|
# TODO(rickyyx): We should support error code or more granular errors from
|
|
# the server to the client so we could assert the specific type of error.
|
|
# assert (
|
|
# f"Given limit {max_limit_api_server+1} exceeds the supported "
|
|
# f"limit {max_limit_api_server}." in str(e)
|
|
# )
|
|
|
|
# Make sure warning is not printed when truncation doesn't happen.
|
|
@ray.remote
|
|
class A:
|
|
def ready(self):
|
|
pass
|
|
|
|
a = A.remote()
|
|
ray.get(a.ready.remote())
|
|
|
|
with warnings.catch_warnings(record=True) as record:
|
|
warnings.simplefilter("always")
|
|
result = runner.invoke(ray_list, ["actors"])
|
|
assert len(record) == 0
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_detail(shutdown_only):
|
|
ray.init(num_cpus=1)
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
def ready(self):
|
|
pass
|
|
|
|
a = Actor.remote()
|
|
ray.get(a.ready.remote())
|
|
|
|
"""
|
|
Test CLI
|
|
"""
|
|
runner = CliRunner()
|
|
result = runner.invoke(ray_list, ["actors", "--detail"])
|
|
print(result.output)
|
|
assert result.exit_code == 0
|
|
# The column for --detail should be in the output.
|
|
assert "test_detail" in result.output
|
|
|
|
# Columns are upper case in the default formatting (table).
|
|
assert "serialized_runtime_env" in result.output
|
|
assert "actor_id" in result.output
|
|
|
|
# Make sure when the --detail option is specified, the default formatting
|
|
# is yaml. If the format is not yaml, the below line will raise an yaml exception.
|
|
# Retrieve yaml content from result output
|
|
print(yaml.safe_load(result.output.split("---")[1].split("...")[0]))
|
|
|
|
# When the format is given, it should respect that formatting.
|
|
result = runner.invoke(ray_list, ["actors", "--detail", "--format=json"])
|
|
assert result.exit_code == 0
|
|
# Fails if output is not JSON
|
|
print(json.loads(result.output))
|
|
|
|
|
|
def _try_state_query_expect_rate_limit(api_func, res_q, start_q=None, **kwargs):
|
|
"""Utility functions for rate limit related e2e tests below"""
|
|
try:
|
|
# Indicate start of the process
|
|
if start_q is not None:
|
|
start_q.put(1)
|
|
api_func(**kwargs)
|
|
except RayStateApiException as e:
|
|
# Other exceptions will be thrown
|
|
if "Max number of in-progress requests" in str(e):
|
|
res_q.put(1)
|
|
else:
|
|
res_q.put(e)
|
|
except Exception as e:
|
|
res_q.put(e)
|
|
else:
|
|
res_q.put(0)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
sys.platform == "win32",
|
|
reason="Lambda test functions could not be pickled on Windows",
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_state_api_rate_limit_with_failure(monkeypatch, shutdown_only):
|
|
# Set environment
|
|
with monkeypatch.context() as m:
|
|
m.setenv("RAY_STATE_SERVER_MAX_HTTP_REQUEST", "3")
|
|
# These make list_nodes, list_workers, list_actors never return in 20secs
|
|
m.setenv(
|
|
"RAY_testing_asio_delay_us",
|
|
(
|
|
"TaskInfoGcsService.grpc_server.GetTaskEvents=20000000:20000000,"
|
|
"WorkerInfoGcsService.grpc_server.GetAllWorkerInfo=20000000:20000000,"
|
|
"ActorInfoGcsService.grpc_server.GetAllActorInfo=20000000:20000000"
|
|
),
|
|
)
|
|
|
|
# Set up scripts
|
|
ray.init()
|
|
|
|
@ray.remote
|
|
def f():
|
|
time.sleep(30)
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
pass
|
|
|
|
task = f.remote() # noqa
|
|
actor = Actor.remote() # noqa
|
|
actor_runtime_env = Actor.options( # noqa
|
|
runtime_env={"pip": ["requests"]}
|
|
).remote()
|
|
pg = ray.util.placement_group(bundles=[{"CPU": 1}]) # noqa
|
|
|
|
_objs = [ray.put(x) for x in range(10)] # noqa
|
|
|
|
# Running 3 slow apis to exhaust the limits
|
|
res_q = queue.Queue()
|
|
start_q = queue.Queue() # used for sync
|
|
procs = [
|
|
threading.Thread(
|
|
target=_try_state_query_expect_rate_limit,
|
|
args=(
|
|
list_workers,
|
|
res_q,
|
|
start_q,
|
|
),
|
|
kwargs={"timeout": 6},
|
|
),
|
|
threading.Thread(
|
|
target=_try_state_query_expect_rate_limit,
|
|
args=(
|
|
list_tasks,
|
|
res_q,
|
|
start_q,
|
|
),
|
|
kwargs={"timeout": 6},
|
|
),
|
|
threading.Thread(
|
|
target=_try_state_query_expect_rate_limit,
|
|
args=(
|
|
list_actors,
|
|
res_q,
|
|
start_q,
|
|
),
|
|
kwargs={"timeout": 6},
|
|
),
|
|
]
|
|
|
|
[p.start() for p in procs]
|
|
|
|
# Wait for other processes to start so rate limit will be reached
|
|
def _wait_to_start():
|
|
started = 0
|
|
for _ in range(3):
|
|
started += start_q.get()
|
|
return started == 3
|
|
|
|
wait_for_condition(_wait_to_start)
|
|
# Wait 1 more second to make sure the API call happens after all
|
|
# process has a call.
|
|
time.sleep(1)
|
|
|
|
# Running another 1 should return error
|
|
with pytest.raises(RayStateApiException) as e:
|
|
print(list_objects())
|
|
# TODO(rickyyx): We will use fine-grained exceptions/error code soon
|
|
assert "Max" in str(
|
|
e
|
|
), f"Expect an exception raised due to rate limit, but have {str(e)}"
|
|
|
|
# Consecutive APIs should be successful after the previous delay ones timeout
|
|
def verify():
|
|
assert len(list_objects()) > 0, "non-delay APIs should be successful"
|
|
"after previous ones timeout"
|
|
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
sys.platform == "win32",
|
|
reason="Lambda test functions could not be pickled on Windows",
|
|
)
|
|
@pytest.mark.parametrize(
|
|
"api_func",
|
|
[
|
|
# NOTE(rickyyx): arbitrary list of APIs, not exhaustive.
|
|
list_objects,
|
|
list_tasks,
|
|
list_actors,
|
|
list_nodes,
|
|
list_placement_groups,
|
|
],
|
|
)
|
|
def test_state_api_server_enforce_concurrent_http_requests(
|
|
api_func, monkeypatch, shutdown_only
|
|
):
|
|
# Set environment
|
|
with monkeypatch.context() as m:
|
|
max_requests = 2
|
|
m.setenv("RAY_STATE_SERVER_MAX_HTTP_REQUEST", str(max_requests))
|
|
# All relevant calls delay to 2 secs
|
|
m.setenv(
|
|
"RAY_testing_asio_delay_us",
|
|
(
|
|
"TaskInfoGcsService.grpc_server.GetTaskEvents=200000:200000,"
|
|
"NodeManagerService.grpc_server.GetObjectsInfo=200000:200000,"
|
|
"ActorInfoGcsService.grpc_server.GetAllActorInfo=200000:200000,"
|
|
"NodeInfoGcsService.grpc_server.GetAllNodeInfo=200000:200000,"
|
|
"PlacementGroupInfoGcsService.grpc_server.GetAllPlacementGroup="
|
|
"200000:200000"
|
|
),
|
|
)
|
|
|
|
ray.init()
|
|
|
|
# Set up scripts
|
|
@ray.remote
|
|
def f():
|
|
time.sleep(30)
|
|
|
|
@ray.remote
|
|
class Actor:
|
|
pass
|
|
|
|
task = f.remote() # noqa
|
|
actor = Actor.remote() # noqa
|
|
actor_runtime_env = Actor.options( # noqa
|
|
runtime_env={"pip": ["requests"]}
|
|
).remote()
|
|
pg = ray.util.placement_group(bundles=[{"CPU": 1}]) # noqa
|
|
|
|
_objs = [ray.put(x) for x in range(10)] # noqa
|
|
|
|
def verify():
|
|
q = queue.Queue()
|
|
num_procs = 3
|
|
procs = [
|
|
threading.Thread(
|
|
target=_try_state_query_expect_rate_limit,
|
|
args=(
|
|
api_func,
|
|
q,
|
|
),
|
|
)
|
|
for _ in range(num_procs)
|
|
]
|
|
|
|
[p.start() for p in procs]
|
|
|
|
max_concurrent_reqs_error = 0
|
|
for _ in range(num_procs):
|
|
try:
|
|
res = q.get(timeout=10)
|
|
if isinstance(res, Exception):
|
|
assert False, f"State API error: {res}"
|
|
elif isinstance(res, int):
|
|
max_concurrent_reqs_error += res
|
|
else:
|
|
raise ValueError(res)
|
|
except queue.Empty:
|
|
assert False, "Failed to get some results from a subprocess"
|
|
|
|
# We should run into max in-progress requests errors
|
|
assert (
|
|
max_concurrent_reqs_error == num_procs - max_requests
|
|
), f"{num_procs - max_requests} requests should be rate limited"
|
|
[p.join(5) for p in procs]
|
|
for proc in procs:
|
|
assert not proc.is_alive(), "All threads should exit"
|
|
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
@pytest.mark.parametrize("callsite_enabled", [True, False])
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_callsite_warning(callsite_enabled, monkeypatch, shutdown_only):
|
|
# Set environment
|
|
with monkeypatch.context() as m:
|
|
m.setenv("RAY_record_ref_creation_sites", str(int(callsite_enabled)))
|
|
ray.init()
|
|
|
|
a = ray.put(1) # noqa
|
|
|
|
runner = CliRunner()
|
|
wait_for_condition(lambda: len(list_objects()) > 0)
|
|
|
|
with warnings.catch_warnings(record=True) as record:
|
|
warnings.simplefilter("always")
|
|
result = runner.invoke(ray_list, ["objects"])
|
|
assert result.exit_code == 0
|
|
|
|
if callsite_enabled:
|
|
assert len(record) == 0
|
|
else:
|
|
assert len(record) == 1
|
|
assert "RAY_record_ref_creation_sites=1" in str(record[0].message)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_raise_on_missing_output_partial_failures(monkeypatch, ray_start_cluster):
|
|
"""
|
|
Verify when there are network partial failures,
|
|
state API raises an exception when `raise_on_missing_output=True`.
|
|
"""
|
|
monkeypatch.setenv("RAY_record_ref_creation_sites", "1")
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=2)
|
|
ray.init(address=cluster.address)
|
|
with monkeypatch.context() as m:
|
|
# defer for 10s for the second node.
|
|
m.setenv(
|
|
"RAY_testing_asio_delay_us",
|
|
"NodeManagerService.grpc_server.GetObjectsInfo=10000000:10000000",
|
|
)
|
|
cluster.add_node(num_cpus=2)
|
|
|
|
@ray.remote
|
|
def f():
|
|
ray.put(1)
|
|
|
|
a = [f.remote() for _ in range(4)] # noqa
|
|
|
|
runner = CliRunner()
|
|
|
|
# Verify
|
|
def verify():
|
|
# Verify when raise_on_missing_output=True, it raises an exception.
|
|
try:
|
|
list_objects(_explain=True, timeout=3)
|
|
except RayStateApiException as e:
|
|
assert "Failed to retrieve all objects from the cluster" in str(e)
|
|
assert "due to query failures to the data sources." in str(e)
|
|
else:
|
|
assert False
|
|
|
|
try:
|
|
summarize_objects(_explain=True, timeout=3)
|
|
except RayStateApiException as e:
|
|
assert "Failed to retrieve all objects from the cluster" in str(e)
|
|
assert "due to query failures to the data sources." in str(e)
|
|
else:
|
|
assert False
|
|
|
|
# Verify when raise_on_missing_output=False, it prints warnings.
|
|
with pytest.warns(UserWarning):
|
|
list_objects(raise_on_missing_output=False, _explain=True, timeout=3)
|
|
|
|
with pytest.warns(UserWarning):
|
|
summarize_objects(raise_on_missing_output=False, _explain=True, timeout=3)
|
|
|
|
# Verify when CLI is used, exceptions are not raised.
|
|
with pytest.warns(UserWarning):
|
|
result = runner.invoke(ray_list, ["objects", "--timeout=3"])
|
|
assert result.exit_code == 0
|
|
|
|
# Verify summary CLI also doesn't raise an exception.
|
|
with pytest.warns(UserWarning):
|
|
result = runner.invoke(summary_state_cli_group, ["objects", "--timeout=3"])
|
|
assert result.exit_code == 0
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_raise_on_missing_output_truncation(monkeypatch, shutdown_only):
|
|
with monkeypatch.context() as m:
|
|
# defer for 10s for the second node.
|
|
m.setenv(
|
|
"RAY_MAX_LIMIT_FROM_DATA_SOURCE",
|
|
"10",
|
|
)
|
|
m.setenv(
|
|
"RAY_task_events_skip_driver_for_test",
|
|
"1",
|
|
)
|
|
ray.init()
|
|
|
|
@ray.remote
|
|
def task():
|
|
time.sleep(300)
|
|
|
|
tasks = [task.remote() for _ in range(15)] # noqa
|
|
|
|
runner = CliRunner()
|
|
|
|
# Verify
|
|
def verify():
|
|
# Verify when raise_on_missing_output=True, it raises an exception.
|
|
try:
|
|
list_tasks(_explain=True, timeout=3)
|
|
except RayStateApiException as e:
|
|
assert "Failed to retrieve all" in str(e)
|
|
assert "(> 10)" in str(e)
|
|
else:
|
|
assert False
|
|
|
|
try:
|
|
summarize_tasks(_explain=True, timeout=3)
|
|
except RayStateApiException as e:
|
|
assert "Failed to retrieve all" in str(e)
|
|
assert "(> 10)" in str(e)
|
|
else:
|
|
assert False
|
|
|
|
# Verify when raise_on_missing_output=False, it prints warnings.
|
|
with pytest.warns(UserWarning):
|
|
list_tasks(raise_on_missing_output=False, _explain=True, timeout=3)
|
|
|
|
with pytest.warns(UserWarning):
|
|
summarize_tasks(raise_on_missing_output=False, _explain=True, timeout=3)
|
|
|
|
# Verify when CLI is used, exceptions are not raised.
|
|
with pytest.warns(UserWarning):
|
|
result = runner.invoke(ray_list, ["tasks", "--timeout=3"])
|
|
assert result.exit_code == 0
|
|
|
|
# Verify summary CLI also doesn't raise an exception.
|
|
with pytest.warns(UserWarning):
|
|
result = runner.invoke(summary_state_cli_group, ["tasks", "--timeout=3"])
|
|
assert result.exit_code == 0
|
|
return True
|
|
|
|
wait_for_condition(verify)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_core_state_api_usage_tags(shutdown_only):
|
|
from ray._common.usage.usage_lib import TagKey, get_extra_usage_tags_to_report
|
|
|
|
ctx = ray.init()
|
|
gcs_client = GcsClient(address=ctx.address_info["gcs_address"])
|
|
list_actors()
|
|
list_tasks()
|
|
list_jobs()
|
|
list_cluster_events()
|
|
list_nodes()
|
|
list_objects()
|
|
list_runtime_envs()
|
|
list_workers()
|
|
|
|
summarize_actors()
|
|
summarize_objects()
|
|
summarize_tasks()
|
|
|
|
result = get_extra_usage_tags_to_report(gcs_client)
|
|
|
|
expected_tags = [
|
|
TagKey.CORE_STATE_API_LIST_ACTORS,
|
|
TagKey.CORE_STATE_API_LIST_TASKS,
|
|
TagKey.CORE_STATE_API_LIST_JOBS,
|
|
TagKey.CORE_STATE_API_LIST_CLUSTER_EVENTS,
|
|
TagKey.CORE_STATE_API_LIST_NODES,
|
|
TagKey.CORE_STATE_API_LIST_OBJECTS,
|
|
TagKey.CORE_STATE_API_LIST_RUNTIME_ENVS,
|
|
TagKey.CORE_STATE_API_LIST_WORKERS,
|
|
TagKey.CORE_STATE_API_SUMMARIZE_ACTORS,
|
|
TagKey.CORE_STATE_API_SUMMARIZE_OBJECTS,
|
|
TagKey.CORE_STATE_API_SUMMARIZE_TASKS,
|
|
]
|
|
assert set(result.keys()).issuperset(
|
|
{TagKey.Name(tag).lower() for tag in expected_tags}
|
|
)
|
|
|
|
|
|
# Tests fix for https://github.com/ray-project/ray/issues/44459
|
|
def test_job_info_is_running_task(shutdown_only):
|
|
ray.init()
|
|
|
|
# To reliably know a job has a long running task, we need to wait a SignalActor
|
|
# to know the task has started.
|
|
signal = SignalActor.remote()
|
|
|
|
@ray.remote
|
|
def f(signal):
|
|
ray.get(signal.send.remote())
|
|
while True:
|
|
time.sleep(10000)
|
|
|
|
long_running = f.remote(signal) # noqa: F841
|
|
ray.get(signal.wait.remote())
|
|
|
|
client = ray.worker.global_worker.gcs_client
|
|
job_id = ray.worker.global_worker.current_job_id
|
|
all_job_info = client.get_all_job_info()
|
|
assert len(all_job_info) == 1
|
|
assert job_id in all_job_info
|
|
assert all_job_info[job_id].is_running_tasks is True
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"event_routing_config", ["default", "aggregator"], indirect=True
|
|
)
|
|
@pytest.mark.usefixtures("event_routing_config")
|
|
def test_hang_driver_has_no_is_running_task(monkeypatch, ray_start_cluster):
|
|
"""
|
|
When there's a call to JobInfoGcsService.GetAllJobInfo, GCS sends RPC
|
|
CoreWorkerService.NumPendingTasks to all drivers for "is_running_task". Our driver
|
|
however has trouble serving such RPC, and GCS should timeout that RPC and unsest the
|
|
field.
|
|
"""
|
|
cluster = ray_start_cluster
|
|
cluster.add_node(num_cpus=10)
|
|
address = cluster.address
|
|
|
|
monkeypatch.setenv(
|
|
"RAY_testing_asio_delay_us",
|
|
"CoreWorkerService.grpc_server.NumPendingTasks=2000000:2000000",
|
|
)
|
|
ray.init(address=address)
|
|
|
|
client = ray.worker.global_worker.gcs_client
|
|
my_job_id = ray.worker.global_worker.current_job_id
|
|
all_job_info = client.get_all_job_info()
|
|
assert list(all_job_info.keys()) == [my_job_id]
|
|
assert not all_job_info[my_job_id].HasField("is_running_tasks")
|
|
|
|
|
|
def test_normalize_filter_keys_accepts_case_insensitive_keys():
|
|
filters = [("STATE", "=", "RUNNING")]
|
|
|
|
normalized_filters = _normalize_filter_keys(StateResource.TASKS, filters)
|
|
|
|
assert normalized_filters == [("state", "=", "RUNNING")]
|
|
|
|
|
|
def test_normalize_filter_keys_rejects_invalid_keys():
|
|
filters = [("invalid_key", "=", "RUNNING")]
|
|
|
|
with pytest.raises(click.BadParameter, match="Invalid filter key"):
|
|
_normalize_filter_keys(StateResource.TASKS, filters)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-sv", __file__]))
|