236 lines
6.9 KiB
Python
236 lines
6.9 KiB
Python
import asyncio
|
|
import sys
|
|
|
|
import httpx
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from starlette.requests import Request
|
|
|
|
import ray
|
|
from ray import serve
|
|
from ray._common.test_utils import SignalActor, wait_for_condition
|
|
from ray.serve._private.test_utils import (
|
|
get_application_url,
|
|
send_signal_on_cancellation,
|
|
)
|
|
from ray.serve.exceptions import RequestCancelledError
|
|
|
|
|
|
@ray.remote
|
|
class Collector:
|
|
def __init__(self):
|
|
self.items = []
|
|
|
|
def add(self, item):
|
|
self.items.append(item)
|
|
|
|
def get(self):
|
|
return self.items
|
|
|
|
|
|
def test_collector_class(serve_instance):
|
|
collector = Collector.remote()
|
|
|
|
random_items = ["this", "is", 1, "demo", "string"]
|
|
|
|
for item in random_items:
|
|
collector.add.remote(item)
|
|
|
|
result = ray.get(collector.get.remote())
|
|
|
|
assert len(result) == len(random_items)
|
|
|
|
for i in range(0, len(result)):
|
|
assert result[i] == random_items[i]
|
|
|
|
|
|
@pytest.mark.parametrize("use_fastapi", [False, True])
|
|
def test_cancel_on_http_client_disconnect_during_execution(
|
|
serve_instance, use_fastapi: bool
|
|
):
|
|
"""Test the client disconnecting while the handler is executing."""
|
|
inner_signal_actor = SignalActor.remote()
|
|
outer_signal_actor = SignalActor.remote()
|
|
|
|
@serve.deployment
|
|
async def inner():
|
|
async with send_signal_on_cancellation(inner_signal_actor):
|
|
pass
|
|
|
|
if use_fastapi:
|
|
app = FastAPI()
|
|
|
|
@serve.deployment
|
|
@serve.ingress(app)
|
|
class Ingress:
|
|
def __init__(self, handle):
|
|
self._handle = handle
|
|
|
|
@app.get("/")
|
|
async def wait_for_cancellation(self):
|
|
_ = self._handle.remote()
|
|
async with send_signal_on_cancellation(outer_signal_actor):
|
|
pass
|
|
|
|
else:
|
|
|
|
@serve.deployment
|
|
class Ingress:
|
|
def __init__(self, handle):
|
|
self._handle = handle
|
|
|
|
async def __call__(self, request: Request):
|
|
_ = self._handle.remote()
|
|
async with send_signal_on_cancellation(outer_signal_actor):
|
|
pass
|
|
|
|
serve.run(Ingress.bind(inner.bind()))
|
|
|
|
# Intentionally time out on the client, causing it to disconnect.
|
|
with pytest.raises(httpx.ReadTimeout):
|
|
httpx.get(get_application_url("HTTP"), timeout=0.5)
|
|
|
|
# Both the HTTP handler and the inner deployment handle call should be cancelled.
|
|
ray.get(inner_signal_actor.wait.remote(), timeout=10)
|
|
ray.get(outer_signal_actor.wait.remote(), timeout=10)
|
|
|
|
|
|
def test_cancel_on_http_client_disconnect_during_assignment(serve_instance):
|
|
"""Test the client disconnecting while the proxy is assigning the request."""
|
|
signal_actor = SignalActor.remote()
|
|
|
|
@serve.deployment(max_ongoing_requests=1)
|
|
class Ingress:
|
|
def __init__(self):
|
|
self._num_requests = 0
|
|
|
|
async def __call__(self, *args):
|
|
self._num_requests += 1
|
|
await signal_actor.wait.remote()
|
|
|
|
return self._num_requests
|
|
|
|
h = serve.run(Ingress.bind())
|
|
|
|
# Send a request and wait for it to be ongoing so we know that further requests
|
|
# will block trying to assign a replica.
|
|
initial_response = h.remote()
|
|
wait_for_condition(lambda: ray.get(signal_actor.cur_num_waiters.remote()) == 1)
|
|
|
|
# Intentionally time out on the client, causing it to disconnect.
|
|
with pytest.raises(httpx.ReadTimeout):
|
|
httpx.get(get_application_url("HTTP"), timeout=0.5)
|
|
|
|
# Now signal the initial request to finish and check that the request sent via HTTP
|
|
# never reaches the replica.
|
|
ray.get(signal_actor.send.remote())
|
|
assert initial_response.result() == 1
|
|
for i in range(2, 12):
|
|
assert h.remote().result() == i
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_cancelled_error_on_http_client_disconnect_during_execution(
|
|
serve_instance,
|
|
):
|
|
"""Test the exception thrown for executing request on http client disconnect"""
|
|
collector = Collector.remote()
|
|
child_signal = SignalActor.remote()
|
|
|
|
@serve.deployment(max_ongoing_requests=1)
|
|
class Child:
|
|
async def __call__(self):
|
|
try:
|
|
await child_signal.wait.remote()
|
|
except asyncio.CancelledError:
|
|
await collector.add.remote("Child_CancelledError")
|
|
raise
|
|
|
|
@serve.deployment
|
|
class Parent:
|
|
def __init__(self, child):
|
|
self.child = child
|
|
|
|
async def __call__(self):
|
|
try:
|
|
await self.child.remote()
|
|
except asyncio.CancelledError:
|
|
await collector.add.remote("Parent_AsyncioCancelledError")
|
|
raise
|
|
except RequestCancelledError:
|
|
await collector.add.remote("Parent_RequestCancelledError")
|
|
raise
|
|
|
|
serve.run(Parent.bind(Child.bind()))
|
|
|
|
# Make a request with short timeout that will cause disconnection
|
|
try:
|
|
await httpx.AsyncClient(timeout=0.5).get(get_application_url("HTTP"))
|
|
except httpx.ReadTimeout:
|
|
pass
|
|
|
|
wait_for_condition(
|
|
lambda: set(ray.get(collector.get.remote()))
|
|
== {"Child_CancelledError", "Parent_AsyncioCancelledError"}
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_cancelled_error_on_http_client_disconnect_during_assignment(
|
|
serve_instance,
|
|
):
|
|
"""Test the exception thrown for queued request on http client disconnect"""
|
|
collector = Collector.remote()
|
|
child_signal = SignalActor.remote()
|
|
|
|
@serve.deployment(max_ongoing_requests=1)
|
|
class Child:
|
|
async def __call__(self):
|
|
try:
|
|
await child_signal.wait.remote()
|
|
except asyncio.CancelledError:
|
|
await collector.add.remote("Child_CancelledError")
|
|
raise
|
|
|
|
@serve.deployment
|
|
class Parent:
|
|
def __init__(self, child):
|
|
self.child = child
|
|
|
|
async def __call__(self):
|
|
try:
|
|
await self.child.remote()
|
|
except asyncio.CancelledError:
|
|
await collector.add.remote("Parent_AsyncioCancelledError")
|
|
raise
|
|
except RequestCancelledError:
|
|
await collector.add.remote("Parent_RequestCancelledError")
|
|
raise
|
|
|
|
h = serve.run(Parent.bind(Child.bind()))
|
|
|
|
# Block Child with first request
|
|
r = h.remote()
|
|
wait_for_condition(lambda: ray.get(child_signal.cur_num_waiters.remote()) == 1)
|
|
|
|
# Make a second request with short timeout that will cause disconnection
|
|
try:
|
|
await httpx.AsyncClient(timeout=0.5).get(get_application_url("HTTP"))
|
|
except httpx.ReadTimeout:
|
|
pass
|
|
|
|
wait_for_condition(
|
|
lambda: ray.get(collector.get.remote()) == ["Parent_AsyncioCancelledError"]
|
|
)
|
|
|
|
# Clean up first request
|
|
r.cancel()
|
|
try:
|
|
await r
|
|
except RequestCancelledError:
|
|
pass
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-v", "-s", __file__]))
|