import asyncio import concurrent.futures from unittest.mock import Mock import pytest from ray.exceptions import RayTaskError from ray.serve._private.proxy_state import ActorProxyWrapper, wrap_as_future from ray.serve.schema import LoggingConfig def _create_object_ref_mock(): fut = concurrent.futures.Future() ray_object_ref_mock = Mock(future=Mock(return_value=fut)) return ray_object_ref_mock, fut def _create_mocked_actor_proxy_wrapper(actor_handle_mock): return ActorProxyWrapper( logging_config=LoggingConfig(), actor_handle=actor_handle_mock, node_id="some_node_id", ) @pytest.mark.asyncio async def test_wrap_as_future_timeout(): object_ref_mock, fut = _create_object_ref_mock() aio_fut = wrap_as_future(ref=object_ref_mock, timeout_s=0) assert not aio_fut.done() # Yield the event-loop await asyncio.sleep(0.001) assert aio_fut.done() with pytest.raises(TimeoutError) as exc_info: aio_fut.result() assert "Future cancelled after timeout 0s" in str(exc_info.value) @pytest.mark.asyncio async def test_wrap_as_future_success(): # Test #1: Validate wrapped asyncio future completes, upon completion of the # ObjectRef's one object_ref_mock, fut = _create_object_ref_mock() aio_fut = wrap_as_future(ref=object_ref_mock, timeout_s=3600) assert not aio_fut.done() # Complete source future (ObjectRef one) fut.set_result("test") # Yield the event-loop await asyncio.sleep(0.001) assert aio_fut.done() assert aio_fut.result() == "test" # Test #2: Wrapped asyncio future completed before time-out expiration, # should not be affected by the cancellation callback object_ref_mock, fut = _create_object_ref_mock() # Purposefully set timeout to 0, ie future has to be cancelled upon next # event-loop iteration aio_fut = wrap_as_future(ref=object_ref_mock, timeout_s=0) assert not aio_fut.done() # Complete source future (ObjectRef one) fut.set_result("test") # Yield the event-loop await asyncio.sleep(0.001) assert aio_fut.done() assert aio_fut.result() == "test" @pytest.mark.asyncio async def test_wrap_as_future_timeout_does_not_assert_on_late_source_completion( monkeypatch, ): object_ref_mock, fut = _create_object_ref_mock() loop = asyncio.get_running_loop() queued_callbacks = [] def queue_callback(callback, *args, context=None): queued_callbacks.append((callback, args, context)) monkeypatch.setattr(loop, "call_soon_threadsafe", queue_callback) aio_fut = wrap_as_future(ref=object_ref_mock, timeout_s=0) fut.set_result("test") assert len(queued_callbacks) == 1 await asyncio.sleep(0.001) assert aio_fut.done() with pytest.raises(TimeoutError): aio_fut.result() callback, args, _ = queued_callbacks.pop() callback(*args) @pytest.mark.parametrize( ("response", "is_ready"), [ # ProxyActor.ready responds with an tuple/array of 2 strings ('["foo", "bar"]', True), ("malformed_json", False), (Exception(), False), ], ) @pytest.mark.asyncio async def test_is_ready_check_success(response, is_ready): """Tests calling is_ready method on ProxyActorWrapper, mocking out underlying ActorHandle response""" object_ref_mock, fut = _create_object_ref_mock() actor_handle_mock = Mock(ready=Mock(remote=Mock(return_value=object_ref_mock))) proxy_wrapper = _create_mocked_actor_proxy_wrapper(actor_handle_mock) for _ in range(10): assert proxy_wrapper.is_ready(timeout_s=1) is None # Yield loop! await asyncio.sleep(0.01) # Complete source future if isinstance(response, Exception): object_ref_mock.future().set_exception(response) else: object_ref_mock.future().set_result(response) # Yield loop! await asyncio.sleep(0) # NOTE: Timeout setting is only relevant, in case there's no pending request # and one will be issued assert proxy_wrapper.is_ready(timeout_s=1) is is_ready @pytest.mark.asyncio async def test_is_ready_check_timeout(): object_ref_mock, fut = _create_object_ref_mock() actor_handle_mock = Mock(ready=Mock(remote=Mock(return_value=object_ref_mock))) proxy_wrapper = _create_mocked_actor_proxy_wrapper(actor_handle_mock) # First call, invokes ProxyActor.ready call assert proxy_wrapper.is_ready(timeout_s=0) is None # Yield loop! await asyncio.sleep(0.001) assert proxy_wrapper.is_ready(timeout_s=0) is False @pytest.mark.parametrize( ("response", "is_healthy"), [ (True, True), (RayTaskError("check_health", "", "cuz"), False), ], ) @pytest.mark.asyncio async def test_is_healthy_check_success(response, is_healthy): """Tests calling is_healthy method on ProxyActorWrapper, mocking out underlying ActorHandle response""" object_ref_mock, fut = _create_object_ref_mock() actor_handle_mock = Mock( check_health=Mock(remote=Mock(return_value=object_ref_mock)) ) proxy_wrapper = _create_mocked_actor_proxy_wrapper(actor_handle_mock) for _ in range(10): assert proxy_wrapper.is_healthy(timeout_s=1) is None # Yield loop! await asyncio.sleep(0.01) object_ref_mock.future.assert_called_once() # Complete source future if isinstance(response, Exception): object_ref_mock.future.return_value.set_exception(response) else: object_ref_mock.future.return_value.set_result(response) # Yield loop! await asyncio.sleep(0) # NOTE: Timeout setting is only relevant, in case there's no pending request # and one will be issued assert proxy_wrapper.is_healthy(timeout_s=1) is is_healthy @pytest.mark.asyncio async def test_is_healthy_check_timeout(): object_ref_mock, fut = _create_object_ref_mock() actor_handle_mock = Mock( check_health=Mock(remote=Mock(return_value=object_ref_mock)) ) proxy_wrapper = _create_mocked_actor_proxy_wrapper(actor_handle_mock) # First call, invokes ProxyActor.ready call assert proxy_wrapper.is_healthy(timeout_s=0) is None # Yield loop! await asyncio.sleep(0.001) assert proxy_wrapper.is_healthy(timeout_s=0) is False @pytest.mark.parametrize( ("response", "is_drained"), [ (True, True), (False, False), (RayTaskError("is_drained", "", "cuz"), False), ], ) @pytest.mark.asyncio async def test_is_drained_check_success(response, is_drained): """Tests calling is_drained method on ProxyActorWrapper, mocking out underlying ActorHandle response""" object_ref_mock, fut = _create_object_ref_mock() actor_handle_mock = Mock(is_drained=Mock(remote=Mock(return_value=object_ref_mock))) proxy_wrapper = _create_mocked_actor_proxy_wrapper(actor_handle_mock) for _ in range(10): assert proxy_wrapper.is_drained(timeout_s=1) is None # Yield loop! await asyncio.sleep(0.01) object_ref_mock.future.assert_called_once() # Complete source future if isinstance(response, Exception): object_ref_mock.future.return_value.set_exception(response) else: object_ref_mock.future.return_value.set_result(response) # Yield loop! await asyncio.sleep(0) # NOTE: Timeout setting is only relevant, in case there's no pending request # and one will be issued assert proxy_wrapper.is_drained(timeout_s=1) is is_drained @pytest.mark.asyncio async def test_is_drained_check_timeout(): object_ref_mock, fut = _create_object_ref_mock() actor_handle_mock = Mock(is_drained=Mock(remote=Mock(return_value=object_ref_mock))) proxy_wrapper = _create_mocked_actor_proxy_wrapper(actor_handle_mock) # First call, invokes ProxyActor.ready call assert proxy_wrapper.is_drained(timeout_s=0) is None # Yield loop! await asyncio.sleep(0.001) assert proxy_wrapper.is_drained(timeout_s=0) is False if __name__ == "__main__": import sys sys.exit(pytest.main(["-v", "-s", __file__]))