247 lines
6.5 KiB
Python
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__]))
|