258 lines
8.5 KiB
Python
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__]))
|