Files
ray-project--ray/python/ray/serve/tests/unit/test_common.py
T
2026-07-13 13:17:40 +08:00

258 lines
8.5 KiB
Python

import pytest
from ray.serve._private.common import (
REPLICA_ID_FULL_ID_STR_PREFIX,
DeploymentID,
DeploymentStatus,
DeploymentStatusInfo,
DeploymentStatusInternalTrigger,
DeploymentStatusTrigger,
ReplicaID,
RunningReplicaInfo,
)
from ray.serve._private.constants import SERVE_DEPLOYMENT_ACTOR_PREFIX
from ray.serve._private.utils import get_deployment_actor_name, get_random_string
from ray.serve.generated.serve_pb2 import (
DeploymentStatusInfo as DeploymentStatusInfoProto,
)
def test_get_deployment_actor_name():
"""Test deterministic actor name for deployment-scoped actors."""
dep_id = DeploymentID(name="MyDeployment", app_name="my_app")
assert get_deployment_actor_name(dep_id, "prefix_tree", "v1") == (
f"{SERVE_DEPLOYMENT_ACTOR_PREFIX}my_app::MyDeployment::v1::prefix_tree"
)
dep_id_default_app = DeploymentID(name="Other") # app_name="default"
assert get_deployment_actor_name(dep_id_default_app, "x", "abc") == (
f"{SERVE_DEPLOYMENT_ACTOR_PREFIX}default::Other::abc::x"
)
def test_replica_id_formatting():
deployment = "DeploymentA"
unique_id = get_random_string()
app = "my_app"
replica_id = ReplicaID(
unique_id, deployment_id=DeploymentID(name=deployment, app_name=app)
)
assert (
replica_id.to_full_id_str()
== f"{REPLICA_ID_FULL_ID_STR_PREFIX}{app}#{deployment}#{unique_id}"
)
assert (
str(replica_id)
== f"Replica(id='{unique_id}', deployment='{deployment}', app='{app}')"
)
def test_replica_id_from_str():
unique_id = get_random_string()
# Test without app name.
full_id_str = f"{REPLICA_ID_FULL_ID_STR_PREFIX}DeploymentA#{unique_id}"
replica_id = ReplicaID.from_full_id_str(full_id_str)
assert replica_id.unique_id == unique_id
assert replica_id.deployment_id == DeploymentID(name="DeploymentA", app_name="")
# Test with app name.
full_id_str = f"{REPLICA_ID_FULL_ID_STR_PREFIX}App1#DeploymentA#{unique_id}"
replica_id = ReplicaID.from_full_id_str(full_id_str)
assert replica_id.unique_id == unique_id
assert replica_id.deployment_id == DeploymentID(name="DeploymentA", app_name="App1")
def test_invalid_id_from_str():
unique_id = get_random_string()
# Missing prefix.
full_id_str = f"DeploymentA#{unique_id}"
with pytest.raises(AssertionError):
ReplicaID.from_full_id_str(full_id_str)
# Too many delimiters.
full_id_str = f"{REPLICA_ID_FULL_ID_STR_PREFIX}DeploymentA###{unique_id}"
with pytest.raises(ValueError):
ReplicaID.from_full_id_str(full_id_str)
def test_is_replica_id():
unique_id = get_random_string()
assert not ReplicaID.is_full_id_str(f"DeploymentA##{unique_id}")
assert not ReplicaID.is_full_id_str(f"DeploymentA#{unique_id}")
assert ReplicaID.is_full_id_str(
f"{REPLICA_ID_FULL_ID_STR_PREFIX}DeploymentA#{unique_id}"
)
assert ReplicaID.is_full_id_str(
f"{REPLICA_ID_FULL_ID_STR_PREFIX}#App1#DeploymentA#{unique_id}"
)
class TestDeploymentStatusInfo:
def test_name_required(self):
with pytest.raises(TypeError):
DeploymentStatusInfo(
status=DeploymentStatus.HEALTHY,
status_trigger=DeploymentStatusTrigger.CONFIG_UPDATE_STARTED,
)
def test_deployment_status_required(self):
with pytest.raises(TypeError):
DeploymentStatusInfo(
name="test_name",
status_trigger=DeploymentStatusTrigger.CONFIG_UPDATE_STARTED,
)
@pytest.mark.parametrize(
"status,status_trigger",
list(zip(list(DeploymentStatus), list(DeploymentStatusTrigger))),
)
def test_proto(self, status, status_trigger):
deployment_status_info = DeploymentStatusInfo(
name="test_name",
status=status,
status_trigger=status_trigger,
message="context about status",
)
serialized_proto = deployment_status_info.to_proto().SerializeToString()
deserialized_proto = DeploymentStatusInfoProto.FromString(serialized_proto)
reconstructed_info = DeploymentStatusInfo.from_proto(deserialized_proto)
assert deployment_status_info == reconstructed_info
def test_handle_transition_deployment_actor_failed_when_already_deploy_failed(
self,
):
"""DEPLOYMENT_ACTOR_FAILED must be handled in DEPLOY_FAILED block.
When status is already DEPLOY_FAILED, repeated ticks call handle_transition
with DEPLOYMENT_ACTOR_FAILED. Without handling, the trigger falls through
and returns self (old message). With handling, returns updated copy.
"""
info = DeploymentStatusInfo(
name="test",
status=DeploymentStatus.DEPLOY_FAILED,
status_trigger=DeploymentStatusTrigger.DEPLOYMENT_ACTOR_FAILED,
message="original error message",
)
new_message = (
"The deployment failed to start deployment actors 2 times in a row."
)
result = info.handle_transition(
trigger=DeploymentStatusInternalTrigger.DEPLOYMENT_ACTOR_FAILED,
message=new_message,
)
assert result is not None
assert result.status == DeploymentStatus.DEPLOY_FAILED
assert result.status_trigger == DeploymentStatusTrigger.DEPLOYMENT_ACTOR_FAILED
assert result.message == new_message
def test_running_replica_info():
"""Test hash value of RunningReplicaInfo"""
replica_id = ReplicaID("asdf123", deployment_id=DeploymentID(name="my_deployment"))
actor_name = replica_id.to_full_id_str()
# Test that replicas with same attributes have same hash
replica1 = RunningReplicaInfo(
replica_id=replica_id,
node_id="node_id",
node_ip="node_ip",
availability_zone="some-az",
actor_name=actor_name,
max_ongoing_requests=1,
is_cross_language=False,
)
replica2 = RunningReplicaInfo(
replica_id=replica_id,
node_id="node_id",
node_ip="node_ip",
availability_zone="some-az",
actor_name=actor_name,
max_ongoing_requests=1,
is_cross_language=False,
)
# Test that cross-language setting affects hash
replica3 = RunningReplicaInfo(
replica_id=replica_id,
node_id="node_id",
node_ip="node_ip",
availability_zone="some-az",
actor_name=actor_name,
max_ongoing_requests=1,
is_cross_language=True,
)
assert replica1._hash == replica2._hash
assert replica3._hash != replica1._hash
# Test that backend_http_port affects hash so long-poll updates
# propagate when the backend HTTP port changes.
replica4 = RunningReplicaInfo(
replica_id=replica_id,
node_id="node_id",
node_ip="node_ip",
availability_zone="some-az",
actor_name=actor_name,
max_ongoing_requests=1,
is_cross_language=False,
backend_http_port=8001,
)
replica5 = RunningReplicaInfo(
replica_id=replica_id,
node_id="node_id",
node_ip="node_ip",
availability_zone="some-az",
actor_name=actor_name,
max_ongoing_requests=1,
is_cross_language=False,
backend_http_port=8002,
)
assert replica4._hash != replica1._hash
assert replica4._hash != replica5._hash
# Test that network endpoint changes affect hash so wrappers and
# long-poll consumers refresh when the replica moves or its gRPC
# port changes.
replica6 = RunningReplicaInfo(
replica_id=replica_id,
node_id="node_id",
node_ip="node_ip_a",
availability_zone="some-az",
actor_name=actor_name,
max_ongoing_requests=1,
is_cross_language=False,
port=9000,
)
replica7 = RunningReplicaInfo(
replica_id=replica_id,
node_id="node_id",
node_ip="node_ip_b",
availability_zone="some-az",
actor_name=actor_name,
max_ongoing_requests=1,
is_cross_language=False,
port=9000,
)
replica8 = RunningReplicaInfo(
replica_id=replica_id,
node_id="node_id",
node_ip="node_ip_a",
availability_zone="some-az",
actor_name=actor_name,
max_ongoing_requests=1,
is_cross_language=False,
port=9001,
)
assert replica6._hash != replica7._hash
assert replica6._hash != replica8._hash
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", "-s", __file__]))