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

247 lines
6.5 KiB
Python

import asyncio
import sys
import httpx
import pytest
from ray import serve
from ray._common.test_utils import SignalActor, async_wait_for_condition
from ray.serve._private.constants import (
RAY_SERVE_RUN_ROUTER_IN_SEPARATE_LOOP,
)
from ray.serve._private.test_utils import get_application_url
from ray.serve.exceptions import RequestCancelledError
from ray.serve.handle import (
DeploymentHandle,
)
@pytest.fixture
def _skip_test_if_router_running_in_separate_loop():
if RAY_SERVE_RUN_ROUTER_IN_SEPARATE_LOOP:
pytest.skip("Router is running in a separate loop.")
@pytest.mark.asyncio
async def test_deployment_handle_works_with_await_when_router_in_same_loop(
serve_instance_async, _skip_test_if_router_running_in_separate_loop
):
@serve.deployment
class F:
async def __call__(self):
return "hi"
h = serve.run(F.bind())
assert await h.remote() == "hi"
def test_deployment_handle_result_fails_when_driver_not_in_async_loop(
serve_instance, _skip_test_if_router_running_in_separate_loop
):
@serve.deployment
class F:
def __call__(self):
return "hi"
h = serve.run(F.bind())
with pytest.raises(RuntimeError):
h.remote().result()
@pytest.mark.asyncio
async def test_deployment_handle_result_fails_in_async_context_but_await_succeeds(
serve_instance_async, _skip_test_if_router_running_in_separate_loop
):
@serve.deployment
class F:
def __call__(self):
return "hi"
h = serve.run(F.bind())
with pytest.raises(RuntimeError):
h.remote().result()
assert await h.remote() == "hi"
def test_http_proxy_requests_work_when_router_in_same_loop(
serve_instance, _skip_test_if_router_running_in_separate_loop
):
@serve.deployment
class F:
def __call__(self):
return "hi"
serve.run(F.bind())
url = "http://localhost:8000/"
resp = httpx.get(url)
assert resp.status_code == 200
assert resp.text == "hi"
@pytest.mark.asyncio
async def test_deployment_handle_configured_for_same_loop_via_init(
serve_instance_async,
):
@serve.deployment
class F:
def __call__(self):
return "hi"
h = serve.run(F.bind())
h._init(_run_router_in_separate_loop=False)
assert await h.remote() == "hi"
with pytest.raises(RuntimeError):
h.remote().result()
def test_child_deployment_handle_configured_for_same_loop_communication(serve_instance):
@serve.deployment
class Child:
def __call__(self):
return "hi"
@serve.deployment
class Parent:
def __init__(self, child_handle: DeploymentHandle):
self.child_handle = child_handle
self.child_handle._init(_run_router_in_separate_loop=False)
async def __call__(self):
return await self.child_handle.remote()
serve.run(Parent.bind(Child.bind()))
url = get_application_url("HTTP")
resp = httpx.get(url)
assert resp.status_code == 200
assert resp.text == "hi"
@pytest.mark.asyncio
async def test_deployment_handle_exception_propagation_in_same_loop(
serve_instance_async, _skip_test_if_router_running_in_separate_loop
):
"""Test that exceptions are properly propagated when router runs in same loop."""
@serve.deployment
class FailingDeployment:
def __call__(self):
raise ValueError("Intentional test error")
h = serve.run(FailingDeployment.bind())
with pytest.raises(ValueError, match="Intentional test error"):
await h.remote()
@pytest.mark.asyncio
async def test_streaming_response_generator_in_same_loop(
serve_instance_async, _skip_test_if_router_running_in_separate_loop
):
"""Test that streaming responses work correctly when router runs in same loop."""
@serve.deployment
class StreamingDeployment:
def generate_numbers(self, limit: int):
for i in range(limit):
yield i
h = serve.run(StreamingDeployment.bind())
streaming_handle = h.options(stream=True)
gen = streaming_handle.generate_numbers.remote(5)
results = []
async for value in gen:
results.append(value)
assert results == [0, 1, 2, 3, 4]
@pytest.mark.asyncio
async def test_concurrent_requests_in_same_loop(
serve_instance_async, _skip_test_if_router_running_in_separate_loop
):
"""Test that multiple concurrent requests work correctly in same loop mode."""
@serve.deployment
class ConcurrentDeployment:
async def slow_operation(self, delay: float, value: str):
await asyncio.sleep(delay)
return f"result-{value}"
h = serve.run(ConcurrentDeployment.bind())
# Launch multiple concurrent requests
tasks = [
h.slow_operation.remote(0.1, "a"),
h.slow_operation.remote(0.1, "b"),
h.slow_operation.remote(0.1, "c"),
]
# All should complete successfully
results = await asyncio.gather(*tasks)
assert set(results) == {"result-a", "result-b", "result-c"}
@pytest.mark.asyncio
async def test_request_cancellation_in_same_loop(
serve_instance_async, _skip_test_if_router_running_in_separate_loop
):
"""Test that request cancellation works correctly when router runs in same loop."""
signal_actor = SignalActor.remote()
@serve.deployment
class SlowDeployment:
async def slow_operation(self):
await signal_actor.wait.remote()
return "should_not_reach_here"
h = serve.run(SlowDeployment.bind())
response = h.slow_operation.remote()
async def check_num_waiters():
assert await signal_actor.cur_num_waiters.remote() == 1
return True
# its important that we use async_wait_for_condition here because
# if we block the event loop then router wont be able to function
async_wait_for_condition(check_num_waiters, timeout=10)
# Cancel the request
response.cancel()
# Should raise CancelledError
with pytest.raises(RequestCancelledError):
await response
await signal_actor.send.remote(clear=True)
@pytest.mark.asyncio
async def test_multiple_awaits(serve_instance_async):
"""Test that multiple awaits doesn't call replica multiple times."""
a = 0
@serve.deployment
async def foo():
nonlocal a
a += 1
return a
app = serve.run(foo.bind())
response = app.remote()
assert await response == 1
assert await response == 1
response = app.remote()
assert await response == 2
assert await response == 2
if __name__ == "__main__":
sys.exit(pytest.main(["-v", "-s", __file__]))