Skip to content

Commit 260b7c5

Browse files
Cleaned up redundant test cases
Signed-off-by: YashwanthRanjanSingaravel <yashwanth.ranjansingaravel@anyscale.com>
1 parent 36660da commit 260b7c5

1 file changed

Lines changed: 62 additions & 122 deletions

File tree

python/ray/serve/tests/test_dependency_ordered_shutdown.py

Lines changed: 62 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import asyncio
22
import sys
3-
from typing import List, Set, Tuple
3+
from typing import Dict, List, Set, Tuple
44

55
import pytest
66

@@ -15,8 +15,8 @@
1515

1616

1717
@pytest.fixture
18-
def serve_shutdown_instance(request, monkeypatch):
19-
"""A Serve instance the test itself is allowed to shut down."""
18+
def shutdown_test_cluster(request, monkeypatch):
19+
"""A Ray cluster whose Serve instance the test itself shuts down."""
2020
for name, value in getattr(request, "param", {}).items():
2121
monkeypatch.setenv(name, value)
2222

@@ -95,14 +95,12 @@ async def __call__(self) -> str:
9595

9696
@serve.deployment
9797
class PlainNode:
98-
"""Same shape as Node but does not record its teardown."""
98+
"""A placeholder for a deployment a LinkedNode needs to already exist."""
9999

100100
def __init__(self, *downstream: DeploymentHandle):
101101
self._downstream = downstream
102102

103-
async def __call__(self) -> str:
104-
for handle in self._downstream:
105-
await handle.remote()
103+
def __call__(self) -> str:
106104
return "plain"
107105

108106

@@ -113,9 +111,7 @@ class WedgedNode:
113111
def __init__(self, *downstream: DeploymentHandle):
114112
self._downstream = downstream
115113

116-
async def __call__(self) -> str:
117-
for handle in self._downstream:
118-
await handle.remote()
114+
def __call__(self) -> str:
119115
return "wedged"
120116

121117
async def __del__(self):
@@ -152,6 +148,20 @@ def _outbound(app_name: str, deployment_name: str) -> Set[Tuple[str, str]]:
152148
}
153149

154150

151+
def _wait_for_topology(expected: Dict[Tuple[str, str], Set[Tuple[str, str]]]):
152+
"""Wait until the controller sees exactly these caller to callee edges."""
153+
seen = {}
154+
155+
def _matches() -> bool:
156+
seen.update({node: _outbound(*node) for node in expected})
157+
return seen == expected
158+
159+
try:
160+
wait_for_condition(_matches, timeout=20)
161+
except RuntimeError:
162+
raise AssertionError(f"Expected topology {expected}, controller sees {seen}.")
163+
164+
155165
def _shutdown_order(recorder: ray.actor.ActorHandle) -> List[str]:
156166
return ray.get(recorder.get.remote())
157167

@@ -166,10 +176,10 @@ def _replica_actor(deployment_name: str, app_name: str) -> ray.actor.ActorHandle
166176
raise RuntimeError(f"No live replica for {deployment_name} in app {app_name}.")
167177

168178

169-
class TestDependencyOrderedShutdown:
179+
class TestKnownTopologyShutdown:
170180
"""Teardown order for topologies the controller fully knows."""
171181

172-
def test_linear_chain(self, serve_shutdown_instance):
182+
def test_linear_chain(self, shutdown_test_cluster):
173183
recorder = Accumulator.remote()
174184

175185
handle = serve.run(
@@ -180,73 +190,19 @@ def test_linear_chain(self, serve_shutdown_instance):
180190
)
181191
assert handle.remote().result() == "Ingress/Middle/Leaf"
182192

183-
wait_for_condition(
184-
lambda: _outbound("chain", "Ingress") == {("chain", "Middle")}
185-
and _outbound("chain", "Middle") == {("chain", "Leaf")}
186-
and _outbound("chain", "Leaf") == set()
193+
_wait_for_topology(
194+
{
195+
("chain", "Ingress"): {("chain", "Middle")},
196+
("chain", "Middle"): {("chain", "Leaf")},
197+
("chain", "Leaf"): set(),
198+
}
187199
)
188200

189201
serve.shutdown()
190202

191203
assert _shutdown_order(recorder) == ["Ingress", "Middle", "Leaf"]
192204

193-
def test_diamond_shared_leaf_last(self, serve_shutdown_instance):
194-
recorder = Accumulator.remote()
195-
196-
leaf = _node("Leaf", recorder)
197-
handle = serve.run(
198-
_node(
199-
"Ingress",
200-
recorder,
201-
_node("M1", recorder, leaf),
202-
_node("M2", recorder, leaf),
203-
),
204-
name="diamond",
205-
)
206-
assert handle.remote().result() == "Ingress/M1/Leaf/M2/Leaf"
207-
208-
wait_for_condition(
209-
lambda: _outbound("diamond", "Ingress")
210-
== {("diamond", "M1"), ("diamond", "M2")}
211-
and _outbound("diamond", "M1") == {("diamond", "Leaf")}
212-
and _outbound("diamond", "M2") == {("diamond", "Leaf")}
213-
)
214-
215-
serve.shutdown()
216-
217-
order = _shutdown_order(recorder)
218-
assert order[0] == "Ingress"
219-
assert order[-1] == "Leaf"
220-
assert set(order[1:3]) == {"M1", "M2"}
221-
222-
def test_independent_apps(self, serve_shutdown_instance):
223-
recorder = Accumulator.remote()
224-
225-
serve.run(
226-
_node("Ingress1", recorder, _node("Backend1", recorder)),
227-
name="app1",
228-
route_prefix="/app1",
229-
)
230-
serve.run(
231-
_node("Ingress2", recorder, _node("Backend2", recorder)),
232-
name="app2",
233-
route_prefix="/app2",
234-
)
235-
236-
wait_for_condition(
237-
lambda: _outbound("app1", "Ingress1") == {("app1", "Backend1")}
238-
and _outbound("app2", "Ingress2") == {("app2", "Backend2")}
239-
)
240-
241-
serve.shutdown()
242-
243-
order = _shutdown_order(recorder)
244-
assert set(order) == {"Ingress1", "Backend1", "Ingress2", "Backend2"}
245-
assert max(order.index("Ingress1"), order.index("Ingress2")) < min(
246-
order.index("Backend1"), order.index("Backend2")
247-
)
248-
249-
def test_cross_app_chain(self, serve_shutdown_instance):
205+
def test_cross_app_chain(self, shutdown_test_cluster):
250206
"""A caller in one app is torn down before its callee in another."""
251207
recorder = Accumulator.remote()
252208

@@ -262,9 +218,11 @@ def test_cross_app_chain(self, serve_shutdown_instance):
262218
)
263219
assert handle.remote().result() == "Caller/Middle/Leaf"
264220

265-
wait_for_condition(
266-
lambda: _outbound("frontend", "Caller") == {("backend", "Middle")}
267-
and _outbound("backend", "Middle") == {("backend", "Leaf")}
221+
_wait_for_topology(
222+
{
223+
("frontend", "Caller"): {("backend", "Middle")},
224+
("backend", "Middle"): {("backend", "Leaf")},
225+
}
268226
)
269227

270228
serve.shutdown()
@@ -275,7 +233,7 @@ def test_cross_app_chain(self, serve_shutdown_instance):
275233
class TestBestEffortTopologyShutdown:
276234
"""Shutdown when the topology is incomplete, cyclic, or cannot drain."""
277235

278-
def test_incomplete_topology(self, serve_shutdown_instance):
236+
def test_incomplete_topology(self, shutdown_test_cluster):
279237
"""Handles created at request time are missing from the topology."""
280238
recorder = Accumulator.remote()
281239

@@ -288,8 +246,12 @@ def test_incomplete_topology(self, serve_shutdown_instance):
288246

289247
assert handle.remote().result() == "Ingress/Middle/Leaf"
290248

291-
wait_for_condition(lambda: _outbound("main", "Ingress") == {("main", "Middle")})
292-
assert _outbound("main", "Middle") == set()
249+
_wait_for_topology(
250+
{
251+
("main", "Ingress"): {("main", "Middle")},
252+
("main", "Middle"): set(),
253+
}
254+
)
293255

294256
serve.shutdown()
295257

@@ -299,26 +261,7 @@ def test_incomplete_topology(self, serve_shutdown_instance):
299261
# The known edge is still respected.
300262
assert order.index("Ingress") < order.index("Middle")
301263

302-
def test_cycle(self, serve_shutdown_instance):
303-
"""A cycle has no caller first order, so it is torn down as a group."""
304-
recorder = Accumulator.remote()
305-
306-
# Redeploy A after B is up to build the cycle.
307-
serve.run(_plain("A"), name="app_a", route_prefix="/a")
308-
serve.run(_linked("B", recorder, "A", "app_a"), name="app_b", route_prefix="/b")
309-
serve.run(_linked("A", recorder, "B", "app_b"), name="app_a", route_prefix="/a")
310-
311-
wait_for_condition(
312-
lambda: _outbound("app_a", "A") == {("app_b", "B")}
313-
and _outbound("app_b", "B") == {("app_a", "A")},
314-
timeout=20,
315-
)
316-
317-
serve.shutdown()
318-
319-
assert sorted(_shutdown_order(recorder)) == ["A", "B"]
320-
321-
def test_ingress_into_cycle(self, serve_shutdown_instance):
264+
def test_ingress_into_cycle(self, shutdown_test_cluster):
322265
"""An ingress feeding a cycle drains before the cyclic remainder."""
323266
recorder = Accumulator.remote()
324267

@@ -330,11 +273,12 @@ def test_ingress_into_cycle(self, serve_shutdown_instance):
330273
route_prefix="/a",
331274
)
332275

333-
wait_for_condition(
334-
lambda: _outbound("app_a", "Ingress") == {("app_a", "A")}
335-
and _outbound("app_a", "A") == {("app_b", "B")}
336-
and _outbound("app_b", "B") == {("app_a", "A")},
337-
timeout=20,
276+
_wait_for_topology(
277+
{
278+
("app_a", "Ingress"): {("app_a", "A")},
279+
("app_a", "A"): {("app_b", "B")},
280+
("app_b", "B"): {("app_a", "A")},
281+
}
338282
)
339283

340284
serve.shutdown()
@@ -344,11 +288,11 @@ def test_ingress_into_cycle(self, serve_shutdown_instance):
344288
assert sorted(order[1:]) == ["A", "B"]
345289

346290
@pytest.mark.parametrize(
347-
"serve_shutdown_instance",
291+
"shutdown_test_cluster",
348292
[{"RAY_SERVE_SHUTDOWN_TIER_TIMEOUT_S": "2"}],
349293
indirect=True,
350294
)
351-
def test_tier_that_never_drains(self, serve_shutdown_instance):
295+
def test_tier_that_never_drains(self, shutdown_test_cluster):
352296
"""A replica that refuses to stop does not block the tiers behind it."""
353297
recorder = Accumulator.remote()
354298

@@ -362,29 +306,25 @@ def test_tier_that_never_drains(self, serve_shutdown_instance):
362306
)
363307
assert handle.remote().result() == "Ingress/wedged"
364308

309+
_wait_for_topology(
310+
{
311+
("chain", "Ingress"): {("chain", "Middle")},
312+
("chain", "Middle"): {("chain", "Leaf")},
313+
}
314+
)
315+
365316
# Start the shutdown without blocking the driver on the stuck replica.
366317
client = _get_global_client()
367318
ray.get(client._controller.graceful_shutdown.remote(False))
368319

369-
wait_for_condition(lambda: "Leaf" in _shutdown_order(recorder), timeout=60)
320+
# Leaf can only be torn down while Middle is still stopping, so its
321+
# teardown proves shutdown advanced past the tier that never drained.
322+
wait_for_condition(lambda: "Leaf" in _shutdown_order(recorder), timeout=20)
370323
assert _shutdown_order(recorder) == ["Ingress", "Leaf"]
371-
372-
# Leaf was torn down while Middle was still stopping, which is only
373-
# possible if shutdown advanced past the tier that never drained.
374324
middle_replica = _replica_actor("Middle", "chain")
375-
ray.kill(middle_replica)
376-
377-
def test_no_applications(self, serve_shutdown_instance):
378-
"""Shutting down an instance with nothing deployed completes."""
379-
serve.start()
380325

381-
serve.shutdown()
382-
383-
assert not [
384-
actor
385-
for actor in ray.util.list_named_actors(all_namespaces=True)
386-
if actor["name"].startswith("SERVE")
387-
]
326+
# Cleanup Middle so it doesn't sit in __del__ for its full 1000s grace period
327+
ray.kill(middle_replica)
388328

389329

390330
if __name__ == "__main__":

0 commit comments

Comments
 (0)