chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,995 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
# coding: utf-8
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, List, Optional, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
import ray._private.ray_constants as ray_constants
|
||||
from ray._common.test_utils import wait_for_condition
|
||||
from ray._private import authentication_test_utils
|
||||
from ray.autoscaler.v2.schema import (
|
||||
ClusterStatus,
|
||||
LaunchRequest,
|
||||
NodeInfo,
|
||||
ResourceRequestByCount,
|
||||
)
|
||||
from ray.autoscaler.v2.sdk import (
|
||||
get_cluster_status,
|
||||
request_cluster_resources,
|
||||
)
|
||||
from ray.autoscaler.v2.tests.util import (
|
||||
get_available_resources,
|
||||
get_cluster_resource_state,
|
||||
get_total_resources,
|
||||
report_autoscaling_state,
|
||||
)
|
||||
from ray.core.generated import autoscaler_pb2, autoscaler_pb2_grpc
|
||||
from ray.core.generated.autoscaler_pb2 import ClusterResourceState, NodeStatus
|
||||
from ray.core.generated.common_pb2 import LabelSelectorOperator
|
||||
from ray.util.state.api import list_nodes
|
||||
|
||||
|
||||
def _autoscaler_state_service_stub():
|
||||
"""Get the grpc stub for the autoscaler state service"""
|
||||
from ray._private.grpc_utils import init_grpc_channel
|
||||
|
||||
gcs_address = ray.get_runtime_context().gcs_address
|
||||
gcs_channel = init_grpc_channel(gcs_address, ray_constants.GLOBAL_GRPC_OPTIONS)
|
||||
return autoscaler_pb2_grpc.AutoscalerStateServiceStub(gcs_channel)
|
||||
|
||||
|
||||
def get_node_ids() -> Tuple[str, List[str]]:
|
||||
"""Get the node ids of the head node and a worker node"""
|
||||
head_node_id = None
|
||||
nodes = list_nodes()
|
||||
worker_node_ids = []
|
||||
for node in nodes:
|
||||
if node.is_head_node:
|
||||
head_node_id = node.node_id
|
||||
else:
|
||||
worker_node_ids += [node.node_id]
|
||||
return head_node_id, worker_node_ids
|
||||
|
||||
|
||||
def assert_cluster_resource_constraints(
|
||||
state: ClusterResourceState, expected_bundles: List[dict], expected_count: List[int]
|
||||
):
|
||||
"""
|
||||
Assert a GetClusterResourceStateReply has cluster_resource_constraints that
|
||||
matches with the expected resources.
|
||||
"""
|
||||
# We only have 1 constraint for now.
|
||||
assert len(state.cluster_resource_constraints) == 1
|
||||
|
||||
resource_requests = state.cluster_resource_constraints[0].resource_requests
|
||||
assert len(resource_requests) == len(expected_bundles) == len(expected_count)
|
||||
|
||||
# Sort all the bundles by bundle's resource names
|
||||
resource_requests = sorted(
|
||||
resource_requests,
|
||||
key=lambda bundle_by_count: "".join(
|
||||
bundle_by_count.request.resources_bundle.keys()
|
||||
),
|
||||
)
|
||||
expected = zip(expected_bundles, expected_count)
|
||||
expected = sorted(
|
||||
expected, key=lambda bundle_count: "".join(bundle_count[0].keys())
|
||||
)
|
||||
|
||||
for actual_bundle_count, expected_bundle_count in zip(resource_requests, expected):
|
||||
assert (
|
||||
dict(actual_bundle_count.request.resources_bundle)
|
||||
== expected_bundle_count[0]
|
||||
)
|
||||
assert actual_bundle_count.count == expected_bundle_count[1]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExpectedNodeState:
|
||||
node_id: str
|
||||
node_status: NodeStatus
|
||||
idle_time_check_cb: Optional[Callable] = None
|
||||
labels: Optional[dict] = None
|
||||
|
||||
|
||||
def assert_node_states(
|
||||
state: ClusterResourceState, expected_nodes: List[ExpectedNodeState]
|
||||
):
|
||||
"""
|
||||
Assert a GetClusterResourceStateReply has node states that
|
||||
matches with the expected nodes.
|
||||
"""
|
||||
assert len(state.node_states) == len(expected_nodes)
|
||||
|
||||
# Sort all the nodes by node's node_id
|
||||
node_states = sorted(state.node_states, key=lambda node: node.node_id)
|
||||
expected_nodes = sorted(expected_nodes, key=lambda node: node.node_id)
|
||||
|
||||
for actual_node, expected_node in zip(node_states, expected_nodes):
|
||||
assert actual_node.status == expected_node.node_status
|
||||
if expected_node.idle_time_check_cb:
|
||||
assert expected_node.idle_time_check_cb(actual_node.idle_duration_ms)
|
||||
|
||||
if expected_node.labels:
|
||||
assert sorted(actual_node.dynamic_labels) == sorted(expected_node.labels)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExpectedNodeInfo:
|
||||
node_id: Optional[str] = None
|
||||
node_status: Optional[str] = None
|
||||
idle_time_check_cb: Optional[Callable] = None
|
||||
instance_id: Optional[str] = None
|
||||
ray_node_type_name: Optional[str] = None
|
||||
instance_type_name: Optional[str] = None
|
||||
ip_address: Optional[str] = None
|
||||
details: Optional[str] = None
|
||||
|
||||
# Check those resources are included in the actual node info.
|
||||
total_resources: Optional[dict] = None
|
||||
available_resources: Optional[dict] = None
|
||||
|
||||
|
||||
def assert_nodes(actual_nodes: List[NodeInfo], expected_nodes: List[ExpectedNodeInfo]):
|
||||
|
||||
assert len(actual_nodes) == len(expected_nodes)
|
||||
# Sort the nodes by id.
|
||||
actual_nodes = sorted(actual_nodes, key=lambda node: node.node_id)
|
||||
expected_nodes = sorted(expected_nodes, key=lambda node: node.node_id)
|
||||
|
||||
for actual_node, expected_node in zip(actual_nodes, expected_nodes):
|
||||
if expected_node.node_id is not None:
|
||||
assert actual_node.node_id == expected_node.node_id
|
||||
if expected_node.node_status is not None:
|
||||
assert actual_node.node_status == expected_node.node_status
|
||||
if expected_node.instance_id is not None:
|
||||
assert actual_node.instance_id == expected_node.instance_id
|
||||
if expected_node.ray_node_type_name is not None:
|
||||
assert actual_node.ray_node_type_name == expected_node.ray_node_type_name
|
||||
if expected_node.instance_type_name is not None:
|
||||
assert actual_node.instance_type_name == expected_node.instance_type_name
|
||||
if expected_node.ip_address is not None:
|
||||
assert actual_node.ip_address == expected_node.ip_address
|
||||
if expected_node.details is not None:
|
||||
assert expected_node.details in actual_node.details
|
||||
|
||||
if expected_node.idle_time_check_cb:
|
||||
assert expected_node.idle_time_check_cb(
|
||||
actual_node.resource_usage.idle_time_ms
|
||||
)
|
||||
|
||||
if expected_node.total_resources:
|
||||
for resource_name, total in expected_node.total_resources.items():
|
||||
assert (
|
||||
total
|
||||
== get_total_resources(actual_node.resource_usage.usage)[
|
||||
resource_name
|
||||
]
|
||||
)
|
||||
|
||||
if expected_node.available_resources:
|
||||
for resource_name, available in expected_node.available_resources.items():
|
||||
assert (
|
||||
available
|
||||
== get_available_resources(actual_node.resource_usage.usage)[
|
||||
resource_name
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def assert_launches(
|
||||
cluster_status: ClusterStatus,
|
||||
expected_pending_launches: List[LaunchRequest],
|
||||
expected_failed_launches: List[LaunchRequest],
|
||||
):
|
||||
def assert_launches(actuals, expects):
|
||||
for actual, expect in zip(actuals, expects):
|
||||
assert actual.instance_type_name == expect.instance_type_name
|
||||
assert actual.ray_node_type_name == expect.ray_node_type_name
|
||||
assert actual.count == expect.count
|
||||
assert actual.state == expect.state
|
||||
assert actual.request_ts_s == expect.request_ts_s
|
||||
|
||||
assert len(cluster_status.pending_launches) == len(expected_pending_launches)
|
||||
assert len(cluster_status.failed_launches) == len(expected_failed_launches)
|
||||
|
||||
actual_pending = sorted(
|
||||
cluster_status.pending_launches, key=lambda launch: launch.ray_node_type_name
|
||||
)
|
||||
expected_pending = sorted(
|
||||
expected_pending_launches, key=lambda launch: launch.ray_node_type_name
|
||||
)
|
||||
|
||||
assert_launches(actual_pending, expected_pending)
|
||||
|
||||
actual_failed = sorted(
|
||||
cluster_status.failed_launches, key=lambda launch: launch.ray_node_type_name
|
||||
)
|
||||
expected_failed = sorted(
|
||||
expected_failed_launches, key=lambda launch: launch.ray_node_type_name
|
||||
)
|
||||
|
||||
assert_launches(actual_failed, expected_failed)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GangResourceRequest:
|
||||
# Resource bundles.
|
||||
bundles: List[dict]
|
||||
# List of detail information about the request
|
||||
details: List[str]
|
||||
|
||||
|
||||
def assert_gang_requests(
|
||||
state: ClusterResourceState, expected: List[GangResourceRequest]
|
||||
):
|
||||
"""
|
||||
Assert a GetClusterResourceStateReply has gang requests that
|
||||
matches with the expected requests.
|
||||
"""
|
||||
assert len(state.pending_gang_resource_requests) == len(expected)
|
||||
|
||||
# Sort all the requests by request's details
|
||||
requests = sorted(
|
||||
state.pending_gang_resource_requests, key=lambda request: request.details
|
||||
)
|
||||
expected = sorted(expected, key=lambda request: "".join(request.details))
|
||||
|
||||
for actual_request, expected_request in zip(requests, expected):
|
||||
# Assert the detail contains the expected details
|
||||
for detail_str in expected_request.details:
|
||||
assert detail_str in actual_request.details
|
||||
|
||||
|
||||
def test_request_cluster_resources_basic(shutdown_only):
|
||||
ctx = ray.init(num_cpus=1)
|
||||
stub = _autoscaler_state_service_stub()
|
||||
gcs_address = ctx.address_info["gcs_address"]
|
||||
|
||||
# Request one
|
||||
request_cluster_resources(gcs_address, [{"resources": {"CPU": 1}}])
|
||||
|
||||
def verify():
|
||||
state = get_cluster_resource_state(stub)
|
||||
assert_cluster_resource_constraints(state, [{"CPU": 1}], [1])
|
||||
return True
|
||||
|
||||
wait_for_condition(verify)
|
||||
|
||||
# Request another overrides the previous request
|
||||
request_cluster_resources(
|
||||
gcs_address, [{"resources": {"CPU": 2, "GPU": 1}}, {"resources": {"CPU": 1}}]
|
||||
)
|
||||
|
||||
def verify():
|
||||
state = get_cluster_resource_state(stub)
|
||||
assert_cluster_resource_constraints(
|
||||
state, [{"CPU": 2, "GPU": 1}, {"CPU": 1}], [1, 1]
|
||||
)
|
||||
return True
|
||||
|
||||
# Request multiple is aggregated by shape.
|
||||
request_cluster_resources(gcs_address, [{"resources": {"CPU": 1}}] * 100)
|
||||
|
||||
def verify():
|
||||
state = get_cluster_resource_state(stub)
|
||||
assert_cluster_resource_constraints(state, [{"CPU": 1}], [100])
|
||||
return True
|
||||
|
||||
wait_for_condition(verify)
|
||||
|
||||
|
||||
def test_request_cluster_resources_with_label_selectors(shutdown_only):
|
||||
ctx = ray.init(num_cpus=1)
|
||||
stub = _autoscaler_state_service_stub()
|
||||
gcs_address = ctx.address_info["gcs_address"]
|
||||
|
||||
# Define two bundles, each with its own label_selector, to request.
|
||||
bundles = [
|
||||
{"CPU": 1},
|
||||
{"GPU": 1, "CPU": 2},
|
||||
]
|
||||
bundle_label_selectors = [
|
||||
{"region": "us-west1"},
|
||||
{"accelerator-type": "!in(A100)"},
|
||||
]
|
||||
to_request = [
|
||||
{"resources": b, "label_selector": s}
|
||||
for b, s in zip(bundles, bundle_label_selectors)
|
||||
]
|
||||
|
||||
# Send the request for these resource bundles
|
||||
request_cluster_resources(gcs_address, to_request)
|
||||
|
||||
def verify():
|
||||
state = get_cluster_resource_state(stub)
|
||||
# Validate shape and resource request count
|
||||
assert_cluster_resource_constraints(state, bundles, [1, 1])
|
||||
|
||||
# Check that requests carry expected label selectors
|
||||
requests = state.cluster_resource_constraints[0].resource_requests
|
||||
|
||||
# First resource request
|
||||
label_selectors_0 = requests[0].request.label_selectors
|
||||
selector_0 = label_selectors_0[0]
|
||||
constraints_0 = {
|
||||
c.label_key: list(c.label_values) for c in selector_0.label_constraints
|
||||
}
|
||||
assert constraints_0 == {"region": ["us-west1"]}
|
||||
assert (
|
||||
selector_0.label_constraints[0].operator
|
||||
== LabelSelectorOperator.LABEL_OPERATOR_IN
|
||||
)
|
||||
|
||||
# Second resource request
|
||||
label_selectors_1 = requests[1].request.label_selectors
|
||||
selector_1 = label_selectors_1[0]
|
||||
constraints_1 = {
|
||||
c.label_key: list(c.label_values) for c in selector_1.label_constraints
|
||||
}
|
||||
assert constraints_1 == {"accelerator-type": ["A100"]}
|
||||
assert (
|
||||
selector_1.label_constraints[0].operator
|
||||
== LabelSelectorOperator.LABEL_OPERATOR_NOT_IN
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
wait_for_condition(verify)
|
||||
|
||||
|
||||
def test_node_info_basic(shutdown_only, monkeypatch):
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("RAY_CLOUD_INSTANCE_ID", "instance-id")
|
||||
m.setenv("RAY_NODE_TYPE_NAME", "node-type-name")
|
||||
m.setenv("RAY_CLOUD_INSTANCE_TYPE_NAME", "instance-type-name")
|
||||
|
||||
ctx = ray.init(num_cpus=1)
|
||||
ip = ctx.address_info["node_ip_address"]
|
||||
|
||||
stub = _autoscaler_state_service_stub()
|
||||
|
||||
def verify():
|
||||
state = get_cluster_resource_state(stub)
|
||||
|
||||
assert len(state.node_states) == 1
|
||||
node = state.node_states[0]
|
||||
|
||||
assert node.instance_id == "instance-id"
|
||||
assert node.ray_node_type_name == "node-type-name"
|
||||
assert node.node_ip_address == ip
|
||||
assert node.instance_type_name == "instance-type-name"
|
||||
|
||||
assert (
|
||||
state.cluster_session_name
|
||||
== ray._private.worker.global_worker.node.session_name
|
||||
)
|
||||
return True
|
||||
|
||||
wait_for_condition(verify)
|
||||
|
||||
|
||||
def test_pg_pending_gang_requests_basic(shutdown_only):
|
||||
ray.init(num_cpus=1)
|
||||
|
||||
# Create a pg that's pending.
|
||||
pg = ray.util.placement_group([{"CPU": 1}] * 3, strategy="STRICT_SPREAD")
|
||||
try:
|
||||
ray.get(pg.ready(), timeout=2)
|
||||
except TimeoutError:
|
||||
pass
|
||||
|
||||
pg_id = pg.id.hex()
|
||||
|
||||
stub = _autoscaler_state_service_stub()
|
||||
|
||||
def verify():
|
||||
state = get_cluster_resource_state(stub)
|
||||
assert_gang_requests(
|
||||
state,
|
||||
[
|
||||
GangResourceRequest(
|
||||
[{"CPU": 1}] * 3, details=[pg_id, "STRICT_SPREAD", "PENDING"]
|
||||
)
|
||||
],
|
||||
)
|
||||
return True
|
||||
|
||||
wait_for_condition(verify)
|
||||
|
||||
|
||||
def test_pg_usage_labels(shutdown_only):
|
||||
ray.init(num_cpus=1)
|
||||
|
||||
# Create a pg
|
||||
pg = ray.util.placement_group([{"CPU": 1}])
|
||||
ray.get(pg.ready())
|
||||
|
||||
# Check the labels
|
||||
stub = _autoscaler_state_service_stub()
|
||||
head_node_id, _ = get_node_ids()
|
||||
|
||||
pg_id = pg.id.hex()
|
||||
|
||||
def verify():
|
||||
state = get_cluster_resource_state(stub)
|
||||
assert_node_states(
|
||||
state,
|
||||
[
|
||||
ExpectedNodeState(
|
||||
head_node_id,
|
||||
NodeStatus.RUNNING,
|
||||
labels={f"_PG_{pg_id}": ""},
|
||||
),
|
||||
],
|
||||
)
|
||||
return True
|
||||
|
||||
wait_for_condition(verify)
|
||||
|
||||
|
||||
def test_node_state_lifecycle_basic(ray_start_cluster):
|
||||
start_s = time.perf_counter()
|
||||
cluster = ray_start_cluster
|
||||
cluster.add_node(num_cpus=0)
|
||||
ray.init(address=cluster.address)
|
||||
|
||||
node = cluster.add_node(num_cpus=1)
|
||||
|
||||
stub = _autoscaler_state_service_stub()
|
||||
|
||||
# We don't have node id from `add_node` unfortunately.
|
||||
def nodes_up():
|
||||
nodes = list_nodes()
|
||||
assert len(nodes) == 2
|
||||
return True
|
||||
|
||||
wait_for_condition(nodes_up)
|
||||
|
||||
head_node_id, worker_node_ids = get_node_ids()
|
||||
node_id = worker_node_ids[0]
|
||||
|
||||
def verify_cluster_idle():
|
||||
state = get_cluster_resource_state(stub)
|
||||
assert_node_states(
|
||||
state,
|
||||
[
|
||||
ExpectedNodeState(
|
||||
node_id, NodeStatus.IDLE, lambda idle_ms: idle_ms > 0
|
||||
),
|
||||
ExpectedNodeState(
|
||||
head_node_id, NodeStatus.IDLE, lambda idle_ms: idle_ms > 0
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
wait_for_condition(verify_cluster_idle)
|
||||
|
||||
# Schedule a task running
|
||||
@ray.remote(num_cpus=0.1)
|
||||
def f():
|
||||
while True:
|
||||
pass
|
||||
|
||||
t = f.remote()
|
||||
|
||||
def verify_cluster_busy():
|
||||
state = get_cluster_resource_state(stub)
|
||||
assert_node_states(
|
||||
state,
|
||||
[
|
||||
ExpectedNodeState(
|
||||
node_id, NodeStatus.RUNNING, lambda idle_ms: idle_ms == 0
|
||||
),
|
||||
ExpectedNodeState(
|
||||
head_node_id, NodeStatus.IDLE, lambda idle_ms: idle_ms > 0
|
||||
),
|
||||
],
|
||||
)
|
||||
return True
|
||||
|
||||
wait_for_condition(verify_cluster_busy)
|
||||
|
||||
# Kill the task
|
||||
ray.cancel(t, force=True)
|
||||
|
||||
wait_for_condition(verify_cluster_idle)
|
||||
|
||||
# Kill the node.
|
||||
cluster.remove_node(node)
|
||||
|
||||
# Sleep for a bit so head node should be idle longer than this.
|
||||
time.sleep(3)
|
||||
|
||||
def verify_cluster_no_node():
|
||||
state = get_cluster_resource_state(stub)
|
||||
now_s = time.perf_counter()
|
||||
test_dur_ms = (now_s - start_s) * 1000
|
||||
assert_node_states(
|
||||
state,
|
||||
[
|
||||
ExpectedNodeState(node_id, NodeStatus.DEAD),
|
||||
ExpectedNodeState(
|
||||
head_node_id,
|
||||
NodeStatus.IDLE,
|
||||
lambda idle_ms: idle_ms > 3 * 1000 and idle_ms < test_dur_ms,
|
||||
),
|
||||
],
|
||||
)
|
||||
return True
|
||||
|
||||
wait_for_condition(verify_cluster_no_node)
|
||||
|
||||
|
||||
# We test that a node with only workers blocked on get
|
||||
# is considered idle.
|
||||
def test_idle_node_blocked(ray_start_cluster):
|
||||
cluster = ray_start_cluster
|
||||
cluster.add_node(num_cpus=1)
|
||||
ray.init(address=cluster.address)
|
||||
|
||||
stub = _autoscaler_state_service_stub()
|
||||
|
||||
# We don't have node id from `add_node` unfortunately.
|
||||
def nodes_up():
|
||||
nodes = list_nodes()
|
||||
assert len(nodes) == 1
|
||||
return True
|
||||
|
||||
wait_for_condition(nodes_up)
|
||||
|
||||
head_node_id = get_node_ids()
|
||||
|
||||
def verify_cluster_idle():
|
||||
state = get_cluster_resource_state(stub)
|
||||
assert_node_states(
|
||||
state,
|
||||
[
|
||||
ExpectedNodeState(
|
||||
head_node_id, NodeStatus.IDLE, lambda idle_ms: idle_ms > 0
|
||||
),
|
||||
],
|
||||
)
|
||||
return True
|
||||
|
||||
wait_for_condition(verify_cluster_idle)
|
||||
|
||||
# Unschedulable
|
||||
@ray.remote(num_cpus=10000)
|
||||
def f():
|
||||
pass
|
||||
|
||||
# Schedule a task running
|
||||
@ray.remote(num_cpus=1)
|
||||
def g():
|
||||
ray.get(f.remote())
|
||||
|
||||
t = g.remote()
|
||||
|
||||
def verify_cluster_busy():
|
||||
state = get_cluster_resource_state(stub)
|
||||
assert_node_states(
|
||||
state,
|
||||
[
|
||||
ExpectedNodeState(
|
||||
head_node_id, NodeStatus.RUNNING, lambda idle_ms: idle_ms == 0
|
||||
),
|
||||
],
|
||||
)
|
||||
return True
|
||||
|
||||
wait_for_condition(verify_cluster_busy)
|
||||
for _ in range(10):
|
||||
time.sleep(0.5)
|
||||
verify_cluster_busy()
|
||||
|
||||
# Kill the task
|
||||
ray.cancel(t, force=True)
|
||||
wait_for_condition(verify_cluster_idle)
|
||||
|
||||
|
||||
def test_idle_node_no_resource(ray_start_cluster):
|
||||
cluster = ray_start_cluster
|
||||
cluster.add_node(num_cpus=1)
|
||||
ray.init(address=cluster.address)
|
||||
|
||||
stub = _autoscaler_state_service_stub()
|
||||
|
||||
# We don't have node id from `add_node` unfortunately.
|
||||
def nodes_up():
|
||||
nodes = list_nodes()
|
||||
assert len(nodes) == 1
|
||||
return True
|
||||
|
||||
wait_for_condition(nodes_up)
|
||||
|
||||
head_node_id = get_node_ids()
|
||||
|
||||
def verify_cluster_idle():
|
||||
state = get_cluster_resource_state(stub)
|
||||
assert_node_states(
|
||||
state,
|
||||
[
|
||||
ExpectedNodeState(
|
||||
head_node_id, NodeStatus.IDLE, lambda idle_ms: idle_ms > 0
|
||||
),
|
||||
],
|
||||
)
|
||||
return True
|
||||
|
||||
wait_for_condition(verify_cluster_idle)
|
||||
|
||||
# Schedule a task running
|
||||
@ray.remote(num_cpus=0)
|
||||
def f():
|
||||
while True:
|
||||
pass
|
||||
|
||||
t = f.remote()
|
||||
|
||||
def verify_cluster_busy():
|
||||
state = get_cluster_resource_state(stub)
|
||||
assert_node_states(
|
||||
state,
|
||||
[
|
||||
ExpectedNodeState(
|
||||
head_node_id, NodeStatus.RUNNING, lambda idle_ms: idle_ms == 0
|
||||
),
|
||||
],
|
||||
)
|
||||
return True
|
||||
|
||||
wait_for_condition(verify_cluster_busy)
|
||||
|
||||
# Kill the task
|
||||
ray.cancel(t, force=True)
|
||||
wait_for_condition(verify_cluster_idle)
|
||||
|
||||
|
||||
def test_get_cluster_status_resources(ray_start_cluster):
|
||||
cluster = ray_start_cluster
|
||||
# Head node
|
||||
cluster.add_node(num_cpus=1, _system_config={"enable_autoscaler_v2": True})
|
||||
ray.init(address=cluster.address)
|
||||
|
||||
# Worker node
|
||||
cluster.add_node(num_cpus=2)
|
||||
|
||||
@ray.remote(num_cpus=1)
|
||||
class Actor:
|
||||
def loop(self):
|
||||
while True:
|
||||
pass
|
||||
|
||||
# Schedule tasks to use all resources.
|
||||
@ray.remote(num_cpus=1)
|
||||
def loop():
|
||||
while True:
|
||||
pass
|
||||
|
||||
[loop.remote() for _ in range(2)]
|
||||
actor = Actor.remote()
|
||||
actor.loop.remote()
|
||||
|
||||
def verify_cpu_resources_all_used():
|
||||
cluster_status = get_cluster_status(cluster.address)
|
||||
total_cluster_resources = get_total_resources(
|
||||
cluster_status.cluster_resource_usage
|
||||
)
|
||||
assert total_cluster_resources["CPU"] == 3.0
|
||||
|
||||
available_cluster_resources = get_available_resources(
|
||||
cluster_status.cluster_resource_usage
|
||||
)
|
||||
assert available_cluster_resources["CPU"] == 0.0
|
||||
return True
|
||||
|
||||
wait_for_condition(verify_cpu_resources_all_used)
|
||||
|
||||
# Schedule more tasks should show up as task demands
|
||||
[loop.remote() for _ in range(2)]
|
||||
|
||||
def verify_task_demands():
|
||||
resource_demands = get_cluster_status(cluster.address).resource_demands
|
||||
assert len(resource_demands.ray_task_actor_demand) == 1
|
||||
assert resource_demands.ray_task_actor_demand[0].bundles_by_count == [
|
||||
ResourceRequestByCount(
|
||||
bundle={"CPU": 1.0},
|
||||
count=2,
|
||||
)
|
||||
]
|
||||
return True
|
||||
|
||||
wait_for_condition(verify_task_demands)
|
||||
|
||||
# Request resources through SDK
|
||||
request_cluster_resources(
|
||||
gcs_address=cluster.address, to_request=[{"resources": {"GPU": 1, "CPU": 2}}]
|
||||
)
|
||||
|
||||
def verify_cluster_constraint_demand():
|
||||
resource_demands = get_cluster_status(cluster.address).resource_demands
|
||||
assert len(resource_demands.cluster_constraint_demand) == 1
|
||||
assert resource_demands.cluster_constraint_demand[0].bundles_by_count == [
|
||||
ResourceRequestByCount(
|
||||
bundle={"GPU": 1.0, "CPU": 2.0},
|
||||
count=1,
|
||||
)
|
||||
]
|
||||
return True
|
||||
|
||||
wait_for_condition(verify_cluster_constraint_demand)
|
||||
|
||||
# Try to schedule some PGs
|
||||
pg1 = ray.util.placement_group([{"CPU": 1}] * 3)
|
||||
|
||||
def verify_pg_demands():
|
||||
resource_demands = get_cluster_status(cluster.address).resource_demands
|
||||
assert len(resource_demands.placement_group_demand) == 1
|
||||
assert resource_demands.placement_group_demand[0].bundles_by_count == [
|
||||
ResourceRequestByCount(
|
||||
bundle={"CPU": 1.0},
|
||||
count=3,
|
||||
)
|
||||
]
|
||||
assert resource_demands.placement_group_demand[0].pg_id == pg1.id.hex()
|
||||
assert resource_demands.placement_group_demand[0].strategy == "PACK"
|
||||
assert resource_demands.placement_group_demand[0].state == "PENDING"
|
||||
return True
|
||||
|
||||
wait_for_condition(verify_pg_demands)
|
||||
|
||||
|
||||
def test_get_cluster_status(ray_start_cluster):
|
||||
# This test is to make sure the grpc stub is working.
|
||||
# TODO(rickyx): Add e2e tests for the autoscaler state service in a separate PR
|
||||
# to validate the data content.
|
||||
cluster = ray_start_cluster
|
||||
# Head node
|
||||
cluster.add_node(num_cpus=1, _system_config={"enable_autoscaler_v2": True})
|
||||
ray.init(address=cluster.address)
|
||||
# Worker node
|
||||
cluster.add_node(num_cpus=2)
|
||||
|
||||
head_node_id, worker_node_ids = get_node_ids()
|
||||
|
||||
def verify_nodes():
|
||||
cluster_status = get_cluster_status(cluster.address)
|
||||
assert_nodes(
|
||||
cluster_status.idle_nodes,
|
||||
[
|
||||
ExpectedNodeInfo(
|
||||
worker_node_ids[0],
|
||||
"IDLE",
|
||||
lambda idle_ms: idle_ms > 0,
|
||||
total_resources={"CPU": 2.0},
|
||||
available_resources={"CPU": 2.0},
|
||||
),
|
||||
ExpectedNodeInfo(
|
||||
head_node_id,
|
||||
"IDLE",
|
||||
lambda idle_ms: idle_ms > 0,
|
||||
total_resources={"CPU": 1.0},
|
||||
available_resources={"CPU": 1.0},
|
||||
),
|
||||
],
|
||||
)
|
||||
return True
|
||||
|
||||
wait_for_condition(verify_nodes)
|
||||
|
||||
# Schedule a task running
|
||||
@ray.remote(num_cpus=2)
|
||||
def f():
|
||||
while True:
|
||||
pass
|
||||
|
||||
f.remote()
|
||||
|
||||
def verify_nodes_busy():
|
||||
cluster_status = get_cluster_status(cluster.address)
|
||||
assert_nodes(
|
||||
cluster_status.idle_nodes,
|
||||
[
|
||||
ExpectedNodeInfo(head_node_id, "IDLE", lambda idle_ms: idle_ms > 0),
|
||||
],
|
||||
)
|
||||
assert_nodes(
|
||||
cluster_status.active_nodes,
|
||||
[
|
||||
ExpectedNodeInfo(
|
||||
worker_node_ids[0],
|
||||
"RUNNING",
|
||||
lambda idle_ms: idle_ms == 0,
|
||||
total_resources={"CPU": 2.0},
|
||||
available_resources={"CPU": 0.0},
|
||||
),
|
||||
],
|
||||
)
|
||||
return True
|
||||
|
||||
wait_for_condition(verify_nodes_busy)
|
||||
|
||||
stub = _autoscaler_state_service_stub()
|
||||
state = autoscaler_pb2.AutoscalingState(
|
||||
last_seen_cluster_resource_state_version=0,
|
||||
# since the autoscaler will also update the autoscaler_state_version periodically,
|
||||
# we need to use a large number here, such as 10, to override it to avoid flaky test.
|
||||
autoscaler_state_version=10,
|
||||
pending_instance_requests=[
|
||||
autoscaler_pb2.PendingInstanceRequest(
|
||||
instance_type_name="m5.large",
|
||||
ray_node_type_name="worker",
|
||||
count=2,
|
||||
request_ts=1000,
|
||||
)
|
||||
],
|
||||
failed_instance_requests=[
|
||||
autoscaler_pb2.FailedInstanceRequest(
|
||||
instance_type_name="m5.large",
|
||||
ray_node_type_name="worker",
|
||||
count=2,
|
||||
start_ts=1000,
|
||||
failed_ts=2000,
|
||||
reason="insufficient quota",
|
||||
)
|
||||
],
|
||||
pending_instances=[
|
||||
autoscaler_pb2.PendingInstance(
|
||||
instance_id="instance-id",
|
||||
instance_type_name="m5.large",
|
||||
ray_node_type_name="worker",
|
||||
ip_address="10.10.10.10",
|
||||
details="launching",
|
||||
)
|
||||
],
|
||||
)
|
||||
report_autoscaling_state(stub, autoscaling_state=state)
|
||||
|
||||
def verify_autoscaler_state():
|
||||
# TODO(rickyx): Add infeasible asserts.
|
||||
|
||||
cluster_status = get_cluster_status(cluster.address)
|
||||
assert len(cluster_status.pending_launches) == 1
|
||||
assert_launches(
|
||||
cluster_status,
|
||||
expected_pending_launches=[
|
||||
LaunchRequest(
|
||||
instance_type_name="m5.large",
|
||||
ray_node_type_name="worker",
|
||||
count=2,
|
||||
state=LaunchRequest.Status.PENDING,
|
||||
request_ts_s=1000,
|
||||
)
|
||||
],
|
||||
expected_failed_launches=[
|
||||
LaunchRequest(
|
||||
instance_type_name="m5.large",
|
||||
ray_node_type_name="worker",
|
||||
count=2,
|
||||
state=LaunchRequest.Status.FAILED,
|
||||
request_ts_s=1000,
|
||||
failed_ts_s=2000,
|
||||
details="insufficient quota",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
assert_nodes(
|
||||
cluster_status.pending_nodes,
|
||||
[
|
||||
ExpectedNodeInfo(
|
||||
instance_id="instance-id",
|
||||
ray_node_type_name="worker",
|
||||
details="launching",
|
||||
ip_address="10.10.10.10",
|
||||
)
|
||||
],
|
||||
)
|
||||
return True
|
||||
|
||||
wait_for_condition(verify_autoscaler_state)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"env_val,enabled",
|
||||
[
|
||||
("1", True),
|
||||
("0", False),
|
||||
("", False),
|
||||
],
|
||||
)
|
||||
def test_is_autoscaler_v2_enabled(shutdown_only, monkeypatch, env_val, enabled):
|
||||
def reset_autoscaler_v2_enabled_cache():
|
||||
import ray.autoscaler.v2.utils as u
|
||||
|
||||
u.cached_is_autoscaler_v2 = None
|
||||
|
||||
reset_autoscaler_v2_enabled_cache()
|
||||
with monkeypatch.context() as m:
|
||||
m.setenv("RAY_enable_autoscaler_v2", env_val)
|
||||
ray.init()
|
||||
|
||||
def verify():
|
||||
assert ray.autoscaler.v2.utils.is_autoscaler_v2() == enabled
|
||||
return True
|
||||
|
||||
wait_for_condition(verify)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"token_state,setup_token,should_fail",
|
||||
[
|
||||
("valid", lambda: None, False),
|
||||
("invalid", lambda: _setup_invalid_token(), True),
|
||||
],
|
||||
)
|
||||
def test_autoscaler_api_with_token_auth(
|
||||
setup_cluster_with_token_auth,
|
||||
cleanup_auth_token_env,
|
||||
token_state,
|
||||
setup_token,
|
||||
should_fail,
|
||||
):
|
||||
"""Parametrized test for autoscaler API with different token states.
|
||||
|
||||
Tests request_cluster_resources with valid, invalid, and missing tokens.
|
||||
"""
|
||||
# Setup token state (this changes the client-side token)
|
||||
setup_token()
|
||||
|
||||
if should_fail:
|
||||
# API call should fail with invalid token
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
request_cluster_resources(
|
||||
ray.get_runtime_context().gcs_address,
|
||||
[{"resources": {"CPU": 1}, "label_selector": {}}],
|
||||
)
|
||||
|
||||
# Verify it's an authentication error
|
||||
error_str = str(exc_info.value).lower()
|
||||
assert (
|
||||
"unauthenticated" in error_str or "invalidauthtoken" in error_str
|
||||
), f"request_cluster_resources with {token_state} token should return auth error, got: {exc_info.value}"
|
||||
else:
|
||||
# API call should succeed with valid token
|
||||
request_cluster_resources(
|
||||
ray.get_runtime_context().gcs_address,
|
||||
[{"resources": {"CPU": 1}, "label_selector": {}}],
|
||||
)
|
||||
|
||||
# Verify the request was successful using the autoscaler state service stub
|
||||
stub = _autoscaler_state_service_stub()
|
||||
state = get_cluster_resource_state(stub)
|
||||
assert (
|
||||
len(state.cluster_resource_constraints) > 0
|
||||
), f"request_cluster_resources with {token_state} token should succeed"
|
||||
|
||||
|
||||
def _setup_invalid_token():
|
||||
"""Helper to set up an invalid authentication token."""
|
||||
|
||||
invalid_token = "invalid_token_value"
|
||||
authentication_test_utils.set_env_auth_token(invalid_token)
|
||||
authentication_test_utils.reset_auth_token_state()
|
||||
|
||||
|
||||
def _clear_token():
|
||||
"""Helper to clear authentication token sources."""
|
||||
|
||||
authentication_test_utils.clear_auth_token_sources()
|
||||
authentication_test_utils.reset_auth_token_state()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if os.environ.get("PARALLEL_CI"):
|
||||
sys.exit(pytest.main(["-n", "auto", "--boxed", "-vs", __file__]))
|
||||
else:
|
||||
sys.exit(pytest.main(["-sv", __file__]))
|
||||
Reference in New Issue
Block a user