231 lines
6.9 KiB
Python
231 lines
6.9 KiB
Python
import logging
|
|
import re
|
|
|
|
import pytest
|
|
|
|
import ray
|
|
from ray import serve
|
|
from ray._common.test_utils import SignalActor, wait_for_condition
|
|
from ray.serve._private.common import (
|
|
OBJ_REF_NOT_SUPPORTED_ERROR,
|
|
DeploymentID,
|
|
RequestMetadata,
|
|
)
|
|
from ray.serve._private.replica_result import (
|
|
ActorReplicaResult,
|
|
ReplicaResult,
|
|
gRPCReplicaResult,
|
|
)
|
|
from ray.serve._private.request_router.replica_wrapper import ReplicaSelection
|
|
from ray.serve.handle import DeploymentHandle
|
|
from ray.serve.tests.conftest import * # noqa
|
|
from ray.serve.tests.conftest import _shared_serve_instance # noqa
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"by_reference,expected_result",
|
|
[(True, ActorReplicaResult), (False, gRPCReplicaResult)],
|
|
)
|
|
def test_init_by_reference(
|
|
serve_instance, by_reference: bool, expected_result: ReplicaResult
|
|
):
|
|
@serve.deployment
|
|
def f():
|
|
return "hi"
|
|
|
|
h = serve.run(f.bind())
|
|
|
|
resp = h.options(_by_reference=by_reference).remote()
|
|
assert resp.result() == "hi"
|
|
assert isinstance(resp._replica_result, expected_result)
|
|
|
|
|
|
@pytest.mark.timeout(60)
|
|
async def test_by_reference_false_raises_error(serve_instance):
|
|
signal = SignalActor.remote()
|
|
|
|
@serve.deployment
|
|
async def f():
|
|
await signal.wait.remote()
|
|
return "hi"
|
|
|
|
h = serve.run(f.bind())
|
|
with pytest.raises(
|
|
RuntimeError, match=re.escape(OBJ_REF_NOT_SUPPORTED_ERROR.args[0])
|
|
):
|
|
response = h.options(_by_reference=False).remote()
|
|
await response._to_object_ref()
|
|
|
|
await signal.send.remote()
|
|
|
|
|
|
@pytest.mark.parametrize("inner_by_reference", [True, False])
|
|
@pytest.mark.parametrize("outer_by_reference", [True, False])
|
|
def test_compose_deployments_in_app(
|
|
serve_instance, inner_by_reference, outer_by_reference
|
|
):
|
|
"""Test composing deployment handle refs within a deployment."""
|
|
|
|
@serve.deployment
|
|
class Downstream:
|
|
def __init__(self, msg: str):
|
|
self._msg = msg
|
|
|
|
def __call__(self, inp1: str, inp2: str):
|
|
return f"{self._msg}|{inp1}|{inp2}"
|
|
|
|
@serve.deployment
|
|
class Deployment:
|
|
def __init__(self, handle1: DeploymentHandle, handle2: DeploymentHandle):
|
|
self._handle1 = handle1.options(_by_reference=outer_by_reference)
|
|
self._handle2 = handle2.options(_by_reference=inner_by_reference)
|
|
|
|
async def __call__(self):
|
|
result = await self._handle1.remote(
|
|
self._handle2.remote("hi1", inp2="hi2"),
|
|
inp2=self._handle2.remote("hi3", inp2="hi4"),
|
|
)
|
|
return f"driver|{result}"
|
|
|
|
handle = serve.run(
|
|
Deployment.bind(
|
|
Downstream.options(name="downstream1").bind("downstream1"),
|
|
Downstream.options(name="downstream2").bind("downstream2"),
|
|
),
|
|
)
|
|
assert (
|
|
handle.remote().result()
|
|
== "driver|downstream1|downstream2|hi1|hi2|downstream2|hi3|hi4"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("inner_by_reference", [True, False])
|
|
@pytest.mark.parametrize("outer_by_reference", [True, False])
|
|
def test_compose_apps(serve_instance, inner_by_reference, outer_by_reference):
|
|
"""Test composing deployment handle refs outside of a deployment."""
|
|
|
|
@serve.deployment
|
|
class Deployment:
|
|
def __init__(self, msg: str):
|
|
self._msg = msg
|
|
|
|
def __call__(self, inp1: str, inp2: str):
|
|
return f"{self._msg}|{inp1}|{inp2}"
|
|
|
|
handle1 = serve.run(
|
|
Deployment.bind("app1"), name="app1", route_prefix="/app1"
|
|
).options(_by_reference=outer_by_reference)
|
|
handle2 = serve.run(
|
|
Deployment.bind("app2"), name="app2", route_prefix="/app2"
|
|
).options(_by_reference=inner_by_reference)
|
|
|
|
assert (
|
|
handle1.remote(
|
|
handle2.remote("hi1", inp2="hi2"),
|
|
inp2=handle2.remote("hi3", inp2="hi4"),
|
|
).result()
|
|
== "app1|app2|hi1|hi2|app2|hi3|hi4"
|
|
)
|
|
|
|
|
|
def test_dispatch_rejects_selection_from_different_deployment():
|
|
handle = DeploymentHandle("deployment-a", "app")
|
|
selection = ReplicaSelection(
|
|
replica_id="replica-1",
|
|
node_ip="127.0.0.1",
|
|
port=None,
|
|
node_id="node-1",
|
|
availability_zone=None,
|
|
replica_metadata={},
|
|
_replica=object(),
|
|
_deployment_id=DeploymentID(name="deployment-b", app_name="app"),
|
|
_request_metadata=RequestMetadata(
|
|
request_id="request-id",
|
|
internal_request_id="internal-request-id",
|
|
call_method="__call__",
|
|
),
|
|
_method_name="__call__",
|
|
_slot_token="slot-1",
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="different deployment"):
|
|
handle.dispatch(selection)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.timeout(30)
|
|
async def test_non_grpc_exception_no_self_cause(serve_instance):
|
|
"""Regression test for https://github.com/ray-project/ray/issues/62358."""
|
|
|
|
@ray.remote
|
|
class _ExceptionChainDetector:
|
|
def __init__(self):
|
|
self._processed = False
|
|
self._cycle_detected = False
|
|
|
|
def record_result(self, cycle_detected: bool):
|
|
self._processed = True
|
|
self._cycle_detected = cycle_detected
|
|
|
|
def processed(self):
|
|
return self._processed
|
|
|
|
def cycle_detected(self):
|
|
return self._cycle_detected
|
|
|
|
detector = _ExceptionChainDetector.remote()
|
|
|
|
class CauseWalker(logging.Handler):
|
|
"""Walk the exception chain to catch self-referential causes."""
|
|
|
|
def __init__(self, detector_handle):
|
|
super().__init__()
|
|
self._detector = detector_handle
|
|
|
|
def emit(self, record):
|
|
if not record.exc_info or record.exc_info[1] is None:
|
|
return
|
|
|
|
exc = record.exc_info[1]
|
|
if not isinstance(exc, ValueError) or str(exc) != "user code exception":
|
|
return
|
|
|
|
cycle_detected = False
|
|
try:
|
|
seen = set()
|
|
while exc.__cause__ is not None:
|
|
if id(exc) in seen:
|
|
cycle_detected = True
|
|
return
|
|
|
|
seen.add(id(exc))
|
|
exc = exc.__cause__
|
|
finally:
|
|
self._detector.record_result.remote(cycle_detected)
|
|
|
|
@serve.deployment
|
|
class App:
|
|
def __init__(self, detector_handle):
|
|
logging.getLogger("ray.serve").addHandler(CauseWalker(detector_handle))
|
|
|
|
async def fail(self):
|
|
raise ValueError("user code exception")
|
|
|
|
handle = serve.run(App.bind(detector))
|
|
|
|
with pytest.raises(ValueError, match="user code exception"):
|
|
await handle.fail.remote()
|
|
|
|
wait_for_condition(lambda: ray.get(detector.processed.remote()), timeout=10)
|
|
|
|
assert not ray.get(detector.cycle_detected.remote()), (
|
|
"Self-referential __cause__ chain detected: "
|
|
"raise e from e was used on a non-gRPC request"
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
sys.exit(pytest.main(["-v", "-s", __file__]))
|