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__]))