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

192 lines
6.1 KiB
Python

import asyncio
import sys
import pytest
from ray.serve._private.common import (
DeploymentHandleSource,
RequestMetadata,
)
from ray.serve._private.request_router import PendingRequest
from ray.serve._private.test_utils import (
FAKE_REPLICA_DEPLOYMENT_ID as DEPLOYMENT_ID,
FakeRunningReplica,
MockTimer,
)
from ray.serve._private.utils import generate_request_id
from ray.serve.experimental.round_robin_router import RoundRobinRouter
TIMER = MockTimer()
def _make_request(model_id: str = "") -> PendingRequest:
return PendingRequest(
args=[],
kwargs={},
metadata=RequestMetadata(
request_id=generate_request_id(),
internal_request_id=generate_request_id(),
multiplexed_model_id=model_id,
),
)
@pytest.fixture
def router(request) -> RoundRobinRouter:
if not hasattr(request, "param"):
request.param = {}
params = dict(request.param)
use_replica_queue_len_cache = params.pop("use_replica_queue_len_cache", False)
TIMER.reset()
request_router = RoundRobinRouter(
deployment_id=DEPLOYMENT_ID,
handle_source=DeploymentHandleSource.REPLICA,
self_actor_id="fake-actor-id",
self_actor_handle=None,
use_replica_queue_len_cache=use_replica_queue_len_cache,
get_curr_time_s=TIMER.time,
initial_backoff_s=0.001,
backoff_multiplier=1,
max_backoff_s=0.001,
)
request_router.initialize_state(**params)
yield request_router
assert request_router.curr_num_routing_tasks == 0
assert request_router.num_pending_requests == 0
def _make_replicas(*replica_ids: str, **kwargs):
replicas = [FakeRunningReplica(replica_id, **kwargs) for replica_id in replica_ids]
for replica in replicas:
replica.set_queue_len_response(0)
return replicas
def _ranked_replica_unique_ids(ranks):
return [[replica.replica_id.unique_id for replica in rank] for rank in ranks]
def test_initialize_state_ignores_router_kwargs(router):
router.initialize_state(unused=True)
def test_round_robin_counter_initialized_randomly(router):
assert 0 <= router._round_robin_counter < 2**31
@pytest.mark.asyncio
async def test_choose_replicas_round_robin_order(router):
replicas = _make_replicas("r2", "r0", "r1")
router.update_replicas(replicas)
router._round_robin_counter = 0
await asyncio.sleep(0)
assert _ranked_replica_unique_ids(
await router.choose_replicas(replicas, _make_request())
) == [["r2"], ["r0"], ["r1"]]
assert _ranked_replica_unique_ids(
await router.choose_replicas(replicas, _make_request())
) == [["r0"], ["r1"], ["r2"]]
assert _ranked_replica_unique_ids(
await router.choose_replicas(replicas, _make_request())
) == [["r1"], ["r2"], ["r0"]]
assert _ranked_replica_unique_ids(
await router.choose_replicas(replicas, _make_request())
) == [["r2"], ["r0"], ["r1"]]
@pytest.mark.asyncio
async def test_choose_replicas_ignores_request_metadata(router):
replicas = [
FakeRunningReplica("r0", model_ids={"model-a"}),
FakeRunningReplica("r1"),
FakeRunningReplica("r2", model_ids={"model-a"}),
]
for replica in replicas:
replica.set_queue_len_response(0)
router.update_replicas(replicas)
router._round_robin_counter = 0
await asyncio.sleep(0)
assert _ranked_replica_unique_ids(
await router.choose_replicas(replicas, _make_request(model_id="model-a"))
) == [["r0"], ["r1"], ["r2"]]
assert _ranked_replica_unique_ids(
await router.choose_replicas(replicas, _make_request(model_id="model-a"))
) == [["r1"], ["r2"], ["r0"]]
assert _ranked_replica_unique_ids(
await router.choose_replicas(replicas, _make_request(model_id="model-a"))
) == [["r2"], ["r0"], ["r1"]]
@pytest.mark.parametrize(
"router", [{"use_replica_queue_len_cache": True}], indirect=True
)
@pytest.mark.asyncio
async def test_choose_replicas_includes_cached_full_replica_first(router):
replicas = [
FakeRunningReplica("r0", max_ongoing_requests=1),
FakeRunningReplica("r1", max_ongoing_requests=1),
FakeRunningReplica("r2", max_ongoing_requests=1),
]
for replica in replicas:
replica.set_queue_len_response(0)
router.update_replicas(replicas)
router.replica_queue_len_cache.update(replicas[0].replica_id, 1)
router._round_robin_counter = 0
await asyncio.sleep(0)
# Selection should preserve strict round-robin order even if fulfillment
# later rejects the cached-full replica and advances to the next rank.
assert _ranked_replica_unique_ids(
await router.choose_replicas(replicas, _make_request())
) == [["r0"], ["r1"], ["r2"]]
@pytest.mark.asyncio
async def test_choose_replica_falls_back_when_current_candidate_full(router):
replicas = [
FakeRunningReplica("r0", max_ongoing_requests=1),
FakeRunningReplica("r1"),
FakeRunningReplica("r2"),
]
replicas[0].set_queue_len_response(1)
replicas[1].set_queue_len_response(0)
replicas[2].set_queue_len_response(0)
router.update_replicas(replicas)
router._round_robin_counter = 0
await asyncio.sleep(0)
chosen = await router._choose_replica_for_request(_make_request())
assert chosen.replica_id.unique_id == "r1"
await asyncio.sleep(0)
@pytest.mark.asyncio
async def test_choose_replicas_empty_candidates(router):
counter = router._round_robin_counter
assert await router.choose_replicas([], _make_request()) == []
assert router._round_robin_counter == counter
@pytest.mark.asyncio
async def test_choose_replicas_sets_should_backoff(router):
# Without should_backoff, the base class tight-loops choose_replicas when
# every replica is at capacity (see _choose_replicas_with_backoff).
replicas = _make_replicas("r0", "r1")
router.update_replicas(replicas)
await asyncio.sleep(0)
request = _make_request()
assert request.routing_context.should_backoff is False
await router.choose_replicas(replicas, request)
assert request.routing_context.should_backoff is True
if __name__ == "__main__":
sys.exit(pytest.main(["-v", "-s", __file__]))