-
Notifications
You must be signed in to change notification settings - Fork 8k
[12/N][HAProxy stability] - Close direct-ingress listeners once the deregistration window ends #65698
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
Open
harshit-anyscale
wants to merge
9
commits into
ray-project:master
Choose a base branch
from
harshit-anyscale:serve-close-ingress-listeners-after-deregistration-window
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
[12/N][HAProxy stability] - Close direct-ingress listeners once the deregistration window ends #65698
Changes from 3 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
4f969b7
[serve] Close direct-ingress listeners once the deregistration window…
harshit-anyscale d85fe8b
Merge branch 'master' into serve-close-ingress-listeners-after-deregi…
harshit-anyscale c72443a
<same message, plus: drop the now-unused Replica._is_direct_ingress p…
harshit-anyscale 9635735
[serve] Check ongoing requests before sleeping in the post-window drain
harshit-anyscale 32de3ba
Merge branch 'master' into serve-close-ingress-listeners-after-deregi…
harshit-anyscale 945c3df
[serve] Wait for zero replicas before the second upscale in test_e2e_…
harshit-anyscale 43eb101
[serve] Make perform_graceful_shutdown idempotent
harshit-anyscale a70fbbd
Merge branch 'master' into serve-close-ingress-listeners-after-deregi…
harshit-anyscale ad1e27b
Merge branch 'master' into serve-close-ingress-listeners-after-deregi…
harshit-anyscale File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import asyncio | ||
| import sys | ||
| from unittest.mock import MagicMock | ||
| import time | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| import pytest | ||
|
|
||
|
|
@@ -50,6 +51,7 @@ def _make_shutdown_fake( | |
| grpc_task=None, | ||
| internal_grpc_port=12345, | ||
| grace_period_s: float = 1.0, | ||
| is_direct_ingress: bool = False, | ||
| ): | ||
| """Builds a minimal stand-in for `Replica` for `perform_graceful_shutdown`. | ||
|
|
||
|
|
@@ -61,7 +63,11 @@ def _make_shutdown_fake( | |
| fake._shutting_down = False | ||
| fake._quiescing = False | ||
| fake._user_callable_initialized = initialized | ||
| fake._ingress = False | ||
| # Set explicitly: on a MagicMock this would otherwise be truthy and silently | ||
| # select the behind-HAProxy drain path. Direct ingress is derived from | ||
| # `_ingress` + RAY_SERVE_ENABLE_DIRECT_INGRESS, so the tests that want the | ||
| # direct-ingress path patch that flag on as well. | ||
| fake._ingress = is_direct_ingress | ||
| fake._deployment_config.graceful_shutdown_timeout_s = grace_period_s | ||
| fake._direct_ingress_http_server = ( | ||
| FakeUvicornServer(events) if with_http_server else None | ||
|
|
@@ -74,14 +80,20 @@ def _make_shutdown_fake( | |
| fake._internal_grpc_port = internal_grpc_port | ||
| fake._server = FakeGrpcServer(events, "inter_deployment_stop") | ||
|
|
||
| async def drain(min_draining_period_s): | ||
| async def drain(min_draining_period_s=0.0): | ||
| # Quiescing must only start AFTER the drain: during the drain the | ||
| # replica must keep serving normally. | ||
| assert fake._quiescing is False | ||
| events.append(("drain", min_draining_period_s)) | ||
|
|
||
| fake._drain_ongoing_requests = drain | ||
|
|
||
| async def drain_behind_haproxy(min_draining_period_s): | ||
| assert fake._quiescing is False | ||
| events.append(("drain_behind_haproxy", min_draining_period_s)) | ||
|
|
||
| fake._drain_behind_haproxy = drain_behind_haproxy | ||
|
|
||
| async def shutdown(): | ||
| events.append(("shutdown",)) | ||
|
|
||
|
|
@@ -230,6 +242,93 @@ async def test_uninitialized_replica_skips_drain_and_server_stops(self): | |
|
|
||
| assert events == [("shutdown",)] | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_behind_haproxy_uses_two_phase_drain(self): | ||
| """Behind HAProxy the two-phase drain is used instead of serving for as | ||
| long as requests keep arriving.""" | ||
| fake, events = _make_shutdown_fake(is_direct_ingress=True) | ||
|
|
||
| with patch( | ||
| "ray.serve._private.replica.RAY_SERVE_ENABLE_DIRECT_INGRESS", True | ||
| ), patch("ray.serve._private.replica.RAY_SERVE_ENABLE_HA_PROXY", True): | ||
| await Replica.perform_graceful_shutdown(fake) | ||
|
|
||
| assert [e[0] for e in events] == [ | ||
| "drain_behind_haproxy", | ||
| "inter_deployment_stop", | ||
| "shutdown", | ||
| ] | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_without_haproxy_keeps_serving_until_drained(self): | ||
| """Without a retrying party in front, the replica keeps accepting for | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we do a manual rewrite on these comments
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. have re-written them, ptal again, TIA :) |
||
| the whole drain: refusing would surface to the client.""" | ||
| fake, events = _make_shutdown_fake(is_direct_ingress=True) | ||
|
|
||
| with patch( | ||
| "ray.serve._private.replica.RAY_SERVE_ENABLE_DIRECT_INGRESS", True | ||
| ), patch("ray.serve._private.replica.RAY_SERVE_ENABLE_HA_PROXY", False): | ||
| await Replica.perform_graceful_shutdown(fake) | ||
|
|
||
| assert [e[0] for e in events] == [ | ||
| "drain", | ||
| "inter_deployment_stop", | ||
| "shutdown", | ||
| ] | ||
|
|
||
|
|
||
| class TestDrainBehindHAProxy: | ||
| @staticmethod | ||
| def _make_fake(): | ||
| events = [] | ||
| fake = MagicMock() | ||
| fake._direct_ingress_http_server = FakeUvicornServer(events) | ||
| fake._stop_accepting_direct_ingress = ( | ||
| lambda: Replica._stop_accepting_direct_ingress(fake) | ||
| ) | ||
|
|
||
| async def drain(min_draining_period_s=0.0): | ||
| events.append(("drain", min_draining_period_s)) | ||
|
|
||
| fake._drain_ongoing_requests = drain | ||
| return fake, events | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_closes_listeners_before_waiting_for_in_flight(self): | ||
| """The listeners close first, so a request arriving from a stale router | ||
| is refused (and retried elsewhere) rather than admitted and then severed | ||
| when the replica exits.""" | ||
| fake, events = self._make_fake() | ||
|
|
||
| await Replica._drain_behind_haproxy(fake, 0.0) | ||
|
|
||
| assert [e[0] for e in events] == ["http_should_exit", "drain"] | ||
| # The minimum period is already spent; it must not be applied again. | ||
| assert events[-1] == ("drain", 0.0) | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_serves_the_full_deregistration_window_first(self): | ||
| """The minimum draining period is honoured in full: listeners stay open | ||
| until load balancers have had time to deregister this replica.""" | ||
| fake, events = self._make_fake() | ||
|
|
||
| start = time.monotonic() | ||
| await Replica._drain_behind_haproxy(fake, 0.05) | ||
| elapsed = time.monotonic() - start | ||
|
|
||
| assert elapsed >= 0.05 | ||
| assert [e[0] for e in events] == ["http_should_exit", "drain"] | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_no_http_server_is_a_no_op(self): | ||
| """A replica without a direct ingress HTTP server still drains.""" | ||
| fake, events = self._make_fake() | ||
| fake._direct_ingress_http_server = None | ||
|
|
||
| await Replica._drain_behind_haproxy(fake, 0.0) | ||
|
|
||
| assert [e[0] for e in events] == ["drain"] | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(pytest.main(["-v", "-s", __file__])) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_drain_ongoing_requests() begins by sleeping for graceful_shutdown_wait_loop_s before checking the request count. That delay is redundant here because this path has already waited for deregistration and closed HTTP ingress.
With graceful_shutdown_wait_loop_s=10:
Could we pass check_immediately=True here so the request count is checked before entering the polling loop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for pointing this out @eicherseiji , pushed the changes.