Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Serve] Revisiting ProxyState to fix draining sequence #41722

Merged
merged 29 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
39c1447
Merge `start_new_ready_check` into `is_ready`
alexeykudinkin Dec 7, 2023
0231ccf
Cleaned up `try_update_status`;
alexeykudinkin Dec 7, 2023
0ebfe1b
Streamlined `is_healthy`, `is_ready` checks in `ProxyActorWrapper`
alexeykudinkin Dec 7, 2023
b6b3660
Tidying up
alexeykudinkin Dec 7, 2023
feffe80
Simplified `is_healthy`, `is_ready` checks even more
alexeykudinkin Dec 7, 2023
2144898
Fixed `is_drained` check to properly return whether proxy actor is pr…
alexeykudinkin Dec 7, 2023
d04d010
Streamlined proxy's state reconciliation flow
alexeykudinkin Dec 7, 2023
ab1cc40
Simplified readiness, health, drain checks;
alexeykudinkin Dec 8, 2023
5ba6068
Make sure exceptions are properly handled in proxy state reconciliation
alexeykudinkin Dec 8, 2023
d9ef34b
Cleaned up docs
alexeykudinkin Dec 8, 2023
e7c5e1e
Added test for ActorProxyWrapper
alexeykudinkin Dec 8, 2023
759d904
Fixed typos;
alexeykudinkin Dec 8, 2023
953343a
Fixed tests
alexeykudinkin Dec 8, 2023
73d881e
Fixed more tests
alexeykudinkin Dec 8, 2023
9e05832
Fixed some more tests
alexeykudinkin Dec 8, 2023
8d0cdcd
`lint`
alexeykudinkin Dec 8, 2023
136df1c
Fixing typo
alexeykudinkin Dec 8, 2023
189cad4
After rebase clean-up
alexeykudinkin Jan 10, 2024
c8dcb57
Tidying up
alexeykudinkin Jan 10, 2024
06b1755
Make proxy health-check configurable
alexeykudinkin Jan 11, 2024
f4283df
Unify `is_drained` API to require timeout be explicitly provided
alexeykudinkin Jan 11, 2024
b787188
Missing log statement
alexeykudinkin Jan 11, 2024
adfd2f3
Tidying up
alexeykudinkin Jan 18, 2024
eff6f23
Cleaned up exception handling
alexeykudinkin Jan 18, 2024
959feba
`_try_cancel_future` -> `_try_set_exception`
alexeykudinkin Jan 18, 2024
c5d2034
Make timeout_s optional
alexeykudinkin Jan 18, 2024
7002e11
Fixing typo
alexeykudinkin Jan 18, 2024
cc99b4a
Fixing another typo
alexeykudinkin Jan 18, 2024
8a1e954
Fixing tests
alexeykudinkin Jan 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Added test for ActorProxyWrapper
Signed-off-by: Alexey Kudinkin <ak@anyscale.com>
  • Loading branch information
alexeykudinkin committed Jan 20, 2024
commit e7c5e1ed5495975cfac35f36c478d9147a00aef8
1 change: 1 addition & 0 deletions python/ray/serve/tests/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ py_test_module_list(
"test_constructor_failure.py",
"test_controller.py",
"test_proxy_router.py",
"test_proxy_actor_wrapper.py",
"test_deployment_scheduler.py",
"test_deployment_version.py",
"test_kv_store.py",
Expand Down
269 changes: 269 additions & 0 deletions python/ray/serve/tests/test_proxy_actor_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import asyncio
import concurrent.futures
from unittest.mock import Mock, patch

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"
Comment on lines +57 to +61
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use ray._private.test_utils.async_wait_for_condition as a common pattern for adding these assertions that require the loop to be yielded


# 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.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"), [
(None, True),
(RayTaskError("check_health", "<traceback>", "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", "<traceback>", "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() 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() 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