736 lines
23 KiB
Python
736 lines
23 KiB
Python
import asyncio
|
|
import concurrent.futures
|
|
import sys
|
|
import threading
|
|
from contextlib import AsyncExitStack
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
import ray
|
|
from ray import serve
|
|
from ray.serve._private.common import DeploymentHandleSource
|
|
from ray.serve._private.constants import (
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
SERVE_DEFAULT_APP_NAME,
|
|
)
|
|
from ray.serve.api import get_replica_context
|
|
from ray.serve.exceptions import RayServeException
|
|
from ray.serve.handle import DeploymentHandle, DeploymentResponseGenerator
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
reason="local_testing_mode doesn't set handle source",
|
|
)
|
|
def test_replica_handle_source(serve_instance):
|
|
@serve.deployment
|
|
def f():
|
|
return "hi"
|
|
|
|
@serve.deployment
|
|
class Router:
|
|
def __init__(self, handle):
|
|
self.handle = handle
|
|
self.handle._init()
|
|
|
|
def check(self):
|
|
return self.handle.init_options._source == DeploymentHandleSource.REPLICA
|
|
|
|
h = serve.run(Router.bind(f.bind()))
|
|
assert h.check.remote().result()
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
reason="local_testing_mode work with tasks & actors",
|
|
)
|
|
def test_handle_serializable(serve_instance):
|
|
@serve.deployment
|
|
def f():
|
|
return "hello"
|
|
|
|
@ray.remote
|
|
class DelegateActor:
|
|
async def call_handle(self, handle):
|
|
return await handle.remote()
|
|
|
|
@serve.deployment
|
|
class Ingress:
|
|
def __init__(self, handle):
|
|
self._handle = handle
|
|
|
|
async def __call__(self):
|
|
# Test pickling handle via `actor.method.remote()`.
|
|
a = DelegateActor.remote()
|
|
return await a.call_handle.remote(self._handle)
|
|
|
|
app_handle = serve.run(Ingress.bind(f.bind()))
|
|
assert app_handle.remote().result() == "hello"
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
reason="local_testing_mode doesn't support get_app_handle/get_deployment_handle",
|
|
)
|
|
def test_get_and_call_handle_in_thread(serve_instance):
|
|
@serve.deployment
|
|
def f():
|
|
return "hello"
|
|
|
|
serve.run(f.bind())
|
|
|
|
def get_and_call_app_handle():
|
|
handle = serve.get_app_handle(SERVE_DEFAULT_APP_NAME)
|
|
return handle.remote().result()
|
|
|
|
def get_and_call_deployment_handle():
|
|
handle = serve.get_deployment_handle("f", SERVE_DEFAULT_APP_NAME)
|
|
return handle.remote().result()
|
|
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
fut1 = executor.submit(get_and_call_app_handle)
|
|
fut2 = executor.submit(get_and_call_deployment_handle)
|
|
assert fut1.result() == "hello"
|
|
assert fut2.result() == "hello"
|
|
|
|
|
|
def test_handle_option_chaining(serve_instance):
|
|
# https://github.com/ray-project/ray/issues/12802
|
|
# https://github.com/ray-project/ray/issues/12798
|
|
|
|
@serve.deployment
|
|
class MultiMethod:
|
|
def method_a(self):
|
|
return "method_a"
|
|
|
|
def method_b(self):
|
|
return "method_b"
|
|
|
|
def __call__(self):
|
|
return "__call__"
|
|
|
|
handle1 = serve.run(MultiMethod.bind())
|
|
counter = handle1.request_counter
|
|
counter_info = counter.info
|
|
assert handle1.remote().result() == "__call__"
|
|
|
|
handle2 = handle1.options(method_name="method_a")
|
|
|
|
assert handle2.remote().result() == "method_a"
|
|
assert handle2.request_counter == counter
|
|
assert handle2.request_counter.info == counter_info
|
|
|
|
handle3 = handle1.options(method_name="method_b")
|
|
|
|
assert handle3.remote().result() == "method_b"
|
|
assert handle3.request_counter == counter
|
|
assert handle2.request_counter.info == counter_info
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
reason="local_testing_mode doesn't support get_app_handle/get_deployment_handle",
|
|
)
|
|
def test_repeated_get_handle_cached(serve_instance):
|
|
@serve.deployment
|
|
def f(_):
|
|
return ""
|
|
|
|
serve.run(f.bind())
|
|
|
|
handle_sets = {serve.get_app_handle("default") for _ in range(100)}
|
|
assert len(handle_sets) == 1
|
|
|
|
handle_sets = {serve.get_deployment_handle("f", "default") for _ in range(100)}
|
|
assert len(handle_sets) == 1
|
|
|
|
|
|
def test_nonexistent_method_sync(serve_instance):
|
|
@serve.deployment
|
|
class A:
|
|
def exists(self):
|
|
pass
|
|
|
|
handle = serve.run(A.bind())
|
|
with pytest.raises(RayServeException) as excinfo:
|
|
handle.does_not_exist.remote().result()
|
|
|
|
exception_string = str(excinfo.value)
|
|
assert "'does_not_exist'" in exception_string
|
|
assert "Available methods: ['exists']" in exception_string
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_nonexistent_method_async(serve_instance):
|
|
@serve.deployment
|
|
class A:
|
|
def exists(self):
|
|
pass
|
|
|
|
handle = serve.run(A.bind())
|
|
with pytest.raises(RayServeException) as excinfo:
|
|
await handle.does_not_exist.remote()
|
|
|
|
exception_string = str(excinfo.value)
|
|
assert "'does_not_exist'" in exception_string
|
|
assert "Available methods: ['exists']" in exception_string
|
|
|
|
|
|
def _get_asyncio_loop_running_in_thread() -> asyncio.AbstractEventLoop:
|
|
loop = asyncio.new_event_loop()
|
|
threading.Thread(
|
|
daemon=True,
|
|
target=loop.run_forever,
|
|
).start()
|
|
return loop
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
reason="local_testing_mode doesn't support get_app_handle/get_deployment_handle",
|
|
)
|
|
@pytest.mark.asyncio
|
|
async def test_call_handle_across_asyncio_loops(serve_instance):
|
|
@serve.deployment
|
|
class A:
|
|
def exists(self):
|
|
return True
|
|
|
|
serve.run(A.bind())
|
|
|
|
async def refresh_get():
|
|
handle = serve.get_app_handle("default")
|
|
assert await handle.exists.remote()
|
|
|
|
for _ in range(10):
|
|
loop = _get_asyncio_loop_running_in_thread()
|
|
asyncio.run_coroutine_threadsafe(refresh_get(), loop).result()
|
|
|
|
handle = serve.get_app_handle("default")
|
|
|
|
async def cache_get():
|
|
assert await handle.exists.remote()
|
|
|
|
for _ in range(10):
|
|
loop = _get_asyncio_loop_running_in_thread()
|
|
asyncio.run_coroutine_threadsafe(cache_get(), loop).result()
|
|
|
|
|
|
def test_handle_typing(serve_instance):
|
|
@serve.deployment
|
|
class DeploymentClass:
|
|
pass
|
|
|
|
@serve.deployment
|
|
def deployment_func():
|
|
pass
|
|
|
|
@serve.deployment
|
|
class Ingress:
|
|
def __init__(
|
|
self, class_downstream: DeploymentHandle, func_downstream: DeploymentHandle
|
|
):
|
|
# serve.run()'ing this deployment fails if these assertions fail.
|
|
assert isinstance(class_downstream, DeploymentHandle)
|
|
assert isinstance(func_downstream, DeploymentHandle)
|
|
|
|
h = serve.run(Ingress.bind(DeploymentClass.bind(), deployment_func.bind()))
|
|
assert isinstance(h, DeploymentHandle)
|
|
|
|
|
|
def test_call_function_with_argument(serve_instance):
|
|
@serve.deployment
|
|
def echo(name: str):
|
|
return f"Hi {name}"
|
|
|
|
@serve.deployment
|
|
class Ingress:
|
|
def __init__(self, h: DeploymentHandle):
|
|
self._h = h
|
|
|
|
async def __call__(self, name: str):
|
|
return await self._h.remote(name)
|
|
|
|
h = serve.run(Ingress.bind(echo.bind()))
|
|
assert h.remote("sned").result() == "Hi sned"
|
|
|
|
|
|
def test_handle_options_with_same_router(serve_instance):
|
|
"""Make sure that multiple handles share same router object."""
|
|
|
|
@serve.deployment
|
|
def echo(name: str):
|
|
return f"Hi {name}"
|
|
|
|
handle = serve.run(echo.bind())
|
|
handle2 = handle.options(multiplexed_model_id="model2")
|
|
assert handle2._router is handle._router
|
|
|
|
|
|
def test_init(serve_instance):
|
|
@serve.deployment
|
|
def f():
|
|
return "hi"
|
|
|
|
h = serve.run(f.bind())
|
|
h._init(_prefer_local_routing=True)
|
|
for _ in range(10):
|
|
assert h.remote().result() == "hi"
|
|
|
|
|
|
def test_init_twice_fails(serve_instance):
|
|
@serve.deployment
|
|
def f():
|
|
return "hi"
|
|
|
|
h = serve.run(f.bind())
|
|
h._init()
|
|
|
|
with pytest.raises(RuntimeError):
|
|
h._init()
|
|
|
|
|
|
def test_init_after_options_fails(serve_instance):
|
|
@serve.deployment
|
|
def f():
|
|
return "hi"
|
|
|
|
h = serve.run(f.bind())
|
|
|
|
with pytest.raises(RuntimeError):
|
|
h.options(stream=True)._init(_prefer_local_routing=True)
|
|
|
|
|
|
def test_init_after_request_fails(serve_instance):
|
|
@serve.deployment
|
|
def f():
|
|
return "hi"
|
|
|
|
h = serve.run(f.bind())
|
|
assert h.remote().result() == "hi"
|
|
|
|
with pytest.raises(RuntimeError):
|
|
h._init(_prefer_local_routing=True)
|
|
|
|
|
|
def test_response_used_in_multiple_calls(serve_instance):
|
|
@serve.deployment(graceful_shutdown_timeout_s=0)
|
|
class F:
|
|
async def __call__(self, sleep_amt: int, x: Any):
|
|
await asyncio.sleep(sleep_amt)
|
|
return f"({x})"
|
|
|
|
@serve.deployment(graceful_shutdown_timeout_s=0)
|
|
class Ingress:
|
|
async def __init__(self, h):
|
|
self.h = h
|
|
|
|
async def __call__(self):
|
|
# r1 will take 5 seconds to finish. This makes sure when h.remote() is
|
|
# started for r2 and r3 (and both rely on r1), r1 is still executing.
|
|
r1 = self.h.remote(5, "r1")
|
|
|
|
# Neither of these should get stuck.
|
|
r2 = self.h.remote(0, r1)
|
|
r3 = self.h.remote(0, r1)
|
|
|
|
return await r2, await r3
|
|
|
|
h = serve.run(Ingress.bind(F.bind()))
|
|
assert h.remote().result(timeout_s=10) == ("((r1))", "((r1))")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skipif(
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
reason="local_testing_mode doesn't support choose_replica/dispatch",
|
|
)
|
|
async def test_choose_replica_and_dispatch_single(serve_instance):
|
|
"""Test choose_replica + dispatch for simple single selection pattern."""
|
|
|
|
@serve.deployment(num_replicas=2)
|
|
class Backend:
|
|
def process(self, msg: str):
|
|
replica_id = get_replica_context().replica_id.unique_id
|
|
return {"actual_replica_id": replica_id, "response": msg}
|
|
|
|
@serve.deployment
|
|
class SimpleProxy:
|
|
def __init__(self, backend: DeploymentHandle):
|
|
self.backend = backend
|
|
|
|
async def handle_request(self, request: str):
|
|
# Context manager ensures slot is released if dispatch fails or is skipped
|
|
async with self.backend.process.choose_replica(request) as selection:
|
|
assert selection.replica_id is not None
|
|
assert selection.node_ip is not None
|
|
|
|
# Dispatch to the selected replica
|
|
response = await self.backend.process.dispatch(selection, request)
|
|
|
|
# Return both the selection and the response for verification
|
|
return {"selected_replica_id": selection.replica_id, **response}
|
|
|
|
h = serve.run(SimpleProxy.bind(Backend.bind()))
|
|
result = await h.handle_request.remote("test_message")
|
|
|
|
# Verify the result contains the message
|
|
assert result["response"] == "test_message"
|
|
|
|
# Verify that dispatch sent the request to the replica we selected
|
|
assert result["actual_replica_id"] == result["selected_replica_id"], (
|
|
f"dispatch sent request to wrong replica: "
|
|
f"selected {result['selected_replica_id']}, but got response from {result['actual_replica_id']}"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skipif(
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
reason="local_testing_mode doesn't support choose_replica/dispatch",
|
|
)
|
|
async def test_choose_replica_early_return_releases_slot(serve_instance):
|
|
"""Early return from choose_replica should release the reserved slot."""
|
|
|
|
@serve.deployment(num_replicas=1, max_ongoing_requests=1)
|
|
class Backend:
|
|
def process(self, msg: str):
|
|
return msg
|
|
|
|
@serve.deployment
|
|
class Proxy:
|
|
def __init__(self, backend: DeploymentHandle):
|
|
self.backend = backend
|
|
|
|
async def handle_request(self, request: str):
|
|
async with self.backend.process.choose_replica(request):
|
|
pass
|
|
|
|
# Ensure the second choose_replica request work
|
|
async with self.backend.process.choose_replica(request) as selection:
|
|
response = await asyncio.wait_for(
|
|
self.backend.process.dispatch(selection, request), timeout=2
|
|
)
|
|
return response
|
|
|
|
h = serve.run(Proxy.bind(Backend.bind()))
|
|
assert await h.handle_request.remote("test_message") == "test_message"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skipif(
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
reason="local_testing_mode doesn't support choose_replica/dispatch",
|
|
)
|
|
async def test_choose_replica_exception_releases_slot(serve_instance):
|
|
"""Exception in choose_replica context should release the reserved slot."""
|
|
|
|
@serve.deployment(num_replicas=1, max_ongoing_requests=1)
|
|
class Backend:
|
|
def process(self, msg: str):
|
|
return msg
|
|
|
|
@serve.deployment
|
|
class Proxy:
|
|
def __init__(self, backend: DeploymentHandle):
|
|
self.backend = backend
|
|
|
|
async def handle_request(self, request: str):
|
|
try:
|
|
async with self.backend.process.choose_replica(request):
|
|
raise RuntimeError("test exception")
|
|
except RuntimeError:
|
|
pass
|
|
|
|
# Ensure the second choose_replica request work
|
|
async with self.backend.process.choose_replica(request) as selection:
|
|
response = await asyncio.wait_for(
|
|
self.backend.process.dispatch(selection, request), timeout=2
|
|
)
|
|
return response
|
|
|
|
h = serve.run(Proxy.bind(Backend.bind()))
|
|
assert await h.handle_request.remote("test_message") == "test_message"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skipif(
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
reason="local_testing_mode doesn't support choose_replica/dispatch",
|
|
)
|
|
async def test_choose_replica_and_dispatch_streaming(serve_instance):
|
|
"""Test choose_replica + dispatch with handle.options(stream=True)."""
|
|
|
|
@serve.deployment(num_replicas=2)
|
|
class Backend:
|
|
async def stream(self, msg: str):
|
|
replica_id = get_replica_context().replica_id.unique_id
|
|
for i in range(3):
|
|
yield {"actual_replica_id": replica_id, "chunk": f"{msg}-{i}"}
|
|
|
|
@serve.deployment
|
|
class StreamingProxy:
|
|
def __init__(self, backend: DeploymentHandle):
|
|
self.backend_stream = backend.stream.options(stream=True)
|
|
|
|
async def handle_request(self, request: str):
|
|
async with self.backend_stream.choose_replica(request) as selection:
|
|
gen = self.backend_stream.dispatch(selection, request)
|
|
chunks = [item async for item in gen]
|
|
return {"selected_replica_id": selection.replica_id, "chunks": chunks}
|
|
|
|
h = serve.run(StreamingProxy.bind(Backend.bind()))
|
|
result = await h.handle_request.remote("stream_test")
|
|
|
|
assert [item["chunk"] for item in result["chunks"]] == [
|
|
"stream_test-0",
|
|
"stream_test-1",
|
|
"stream_test-2",
|
|
]
|
|
assert all(
|
|
item["actual_replica_id"] == result["selected_replica_id"]
|
|
for item in result["chunks"]
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skipif(
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
reason="local_testing_mode doesn't support choose_replica/dispatch",
|
|
)
|
|
async def test_choose_replica_and_dispatch_parallel(serve_instance):
|
|
"""Test parallel selection pattern (e.g., PD proxy) using AsyncExitStack."""
|
|
|
|
@serve.deployment(num_replicas=2)
|
|
class PrefillServer:
|
|
def chat(self, msg: str):
|
|
replica_id = get_replica_context().replica_id.unique_id
|
|
return {"actual_replica_id": replica_id, "response": msg}
|
|
|
|
@serve.deployment(num_replicas=2)
|
|
class DecodeServer:
|
|
def chat(self, msg: str):
|
|
replica_id = get_replica_context().replica_id.unique_id
|
|
return {"actual_replica_id": replica_id, "response": msg}
|
|
|
|
@serve.deployment
|
|
class PDProxy:
|
|
def __init__(
|
|
self,
|
|
prefill_server: DeploymentHandle,
|
|
decode_server: DeploymentHandle,
|
|
):
|
|
self.prefill = prefill_server
|
|
self.decode = decode_server
|
|
|
|
async def handle_request(self, request: str):
|
|
# Use AsyncExitStack to manage multiple context managers in parallel
|
|
async with AsyncExitStack() as stack:
|
|
# Select and RESERVE replicas from BOTH deployments in parallel
|
|
p_selection, d_selection = await asyncio.gather(
|
|
stack.enter_async_context(self.prefill.chat.choose_replica()),
|
|
stack.enter_async_context(self.decode.chat.choose_replica()),
|
|
)
|
|
|
|
p_msg = f"prefill:{request}"
|
|
d_msg = f"decode:{request}"
|
|
|
|
# Dispatch to both selected replicas
|
|
p_result, d_result = await asyncio.gather(
|
|
self.prefill.chat.dispatch(p_selection, p_msg),
|
|
self.decode.chat.dispatch(d_selection, d_msg),
|
|
)
|
|
return {
|
|
"prefill": {
|
|
"selected_replica_id": p_selection.replica_id,
|
|
**p_result,
|
|
},
|
|
"decode": {
|
|
"selected_replica_id": d_selection.replica_id,
|
|
**d_result,
|
|
},
|
|
}
|
|
|
|
h = serve.run(PDProxy.bind(PrefillServer.bind(), DecodeServer.bind()))
|
|
result = await h.handle_request.remote("test_parallel")
|
|
|
|
assert result["prefill"]["response"] == "prefill:test_parallel"
|
|
assert result["decode"]["response"] == "decode:test_parallel"
|
|
|
|
# Verify that dispatch sent the request to the replica we selected
|
|
assert (
|
|
result["prefill"]["actual_replica_id"]
|
|
== result["prefill"]["selected_replica_id"]
|
|
), (
|
|
f"dispatch sent request to wrong replica for prefill: "
|
|
f"selected {result['prefill']['selected_replica_id']}, but got response from {result['prefill']['actual_replica_id']}"
|
|
)
|
|
assert (
|
|
result["decode"]["actual_replica_id"] == result["decode"]["selected_replica_id"]
|
|
), (
|
|
f"dispatch sent request to wrong replica for decode: "
|
|
f"selected {result['decode']['selected_replica_id']}, but got response from {result['decode']['actual_replica_id']}"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skipif(
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
reason="local_testing_mode doesn't support choose_replica/dispatch",
|
|
)
|
|
async def test_choose_replica_cancel_releases_slot_across_loop_boundary(
|
|
serve_instance,
|
|
):
|
|
"""Cancelling a task holding a selection must release the slot, even when
|
|
the router lives on a different thread (SingletonThreadRouter)."""
|
|
|
|
@serve.deployment(num_replicas=1, max_ongoing_requests=1)
|
|
class Backend:
|
|
def process(self, msg: str):
|
|
return msg
|
|
|
|
handle = serve.run(Backend.bind())
|
|
# Warm up so the cancellation isn't racing initialization.
|
|
assert await handle.process.remote("warmup") == "warmup"
|
|
|
|
ready = asyncio.Event()
|
|
never_set = asyncio.Event()
|
|
|
|
async def hold():
|
|
async with handle.process.choose_replica("first"):
|
|
ready.set()
|
|
await never_set.wait()
|
|
|
|
task = asyncio.create_task(hold())
|
|
await asyncio.wait_for(ready.wait(), timeout=5)
|
|
|
|
task.cancel()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await task
|
|
|
|
# Hangs if the cancelled task leaked the only slot.
|
|
async with handle.process.choose_replica("second") as selection:
|
|
result = await asyncio.wait_for(
|
|
handle.process.dispatch(selection, "second"),
|
|
timeout=5,
|
|
)
|
|
assert result == "second"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skipif(
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
reason="local_testing_mode doesn't support choose_replica/dispatch",
|
|
)
|
|
async def test_dispatch_wrapper_uses_choose_replica_stream_flag(serve_instance):
|
|
"""Wrapper type follows metadata.is_streaming, not the dispatching handle."""
|
|
|
|
@serve.deployment
|
|
class Backend:
|
|
async def stream(self, msg: str):
|
|
yield f"{msg}-0"
|
|
yield f"{msg}-1"
|
|
|
|
@serve.deployment
|
|
class Proxy:
|
|
def __init__(self, backend: DeploymentHandle):
|
|
self.stream_handle = backend.stream.options(stream=True)
|
|
self.dispatch_handle = backend.stream # default stream=False
|
|
|
|
async def run(self, msg: str) -> bool:
|
|
async with self.stream_handle.choose_replica(msg) as sel:
|
|
resp = self.dispatch_handle.dispatch(sel, msg)
|
|
is_gen = isinstance(resp, DeploymentResponseGenerator)
|
|
if is_gen:
|
|
async for _ in resp:
|
|
pass
|
|
else:
|
|
try:
|
|
await resp
|
|
except Exception:
|
|
pass
|
|
return is_gen
|
|
|
|
h = serve.run(Proxy.bind(Backend.bind()))
|
|
assert await h.run.remote("test") is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skipif(
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
reason="local_testing_mode doesn't support choose_replica/dispatch",
|
|
)
|
|
async def test_dispatch_rejects_selection_without_deployment_id(serve_instance):
|
|
"""dispatch must reject a selection whose deployment_id is unset."""
|
|
|
|
@serve.deployment
|
|
class Backend:
|
|
def process(self, msg: str):
|
|
return msg
|
|
|
|
@serve.deployment
|
|
class Caller:
|
|
def __init__(self, backend: DeploymentHandle):
|
|
self.handle = backend.process
|
|
|
|
async def run(self) -> str:
|
|
async with self.handle.choose_replica("msg") as sel:
|
|
sel._deployment_id = None
|
|
try:
|
|
resp = self.handle.dispatch(sel, "msg")
|
|
except ValueError:
|
|
return "raised"
|
|
await resp
|
|
return "no_raise"
|
|
|
|
h = serve.run(Caller.bind(Backend.bind()))
|
|
assert await h.run.remote() == "raised"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.skipif(
|
|
RAY_SERVE_FORCE_LOCAL_TESTING_MODE,
|
|
reason="local_testing_mode doesn't support choose_replica/dispatch",
|
|
)
|
|
async def test_request_counter_only_increments_on_dispatch(serve_instance):
|
|
"""request_counter ticks per dispatched request, not per choose_replica."""
|
|
|
|
@serve.deployment
|
|
class Backend:
|
|
def process(self, msg: str):
|
|
return msg
|
|
|
|
@serve.deployment
|
|
class Caller:
|
|
def __init__(self, backend: DeploymentHandle):
|
|
self.handle = backend.process
|
|
|
|
async def run(self, dispatch_too: bool) -> int:
|
|
inc_calls: list = []
|
|
original_inc = self.handle.request_counter.inc
|
|
|
|
def tracking_inc(*args, **kwargs):
|
|
inc_calls.append(1)
|
|
return original_inc(*args, **kwargs)
|
|
|
|
self.handle.request_counter.inc = tracking_inc
|
|
try:
|
|
if dispatch_too:
|
|
async with self.handle.choose_replica("msg") as sel:
|
|
await self.handle.dispatch(sel, "msg")
|
|
else:
|
|
async with self.handle.choose_replica("msg"):
|
|
pass
|
|
return len(inc_calls)
|
|
finally:
|
|
self.handle.request_counter.inc = original_inc
|
|
|
|
h = serve.run(Caller.bind(Backend.bind()))
|
|
assert await h.run.remote(False) == 0
|
|
assert await h.run.remote(True) == 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-v", "-s", __file__]))
|