Files
2026-07-13 13:17:40 +08:00

996 lines
30 KiB
Python

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__]))