207 lines
5.4 KiB
Python
207 lines
5.4 KiB
Python
import os
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from ray import serve
|
|
|
|
|
|
def test_broadcast_basic(serve_instance):
|
|
"""Test that broadcast() calls every replica."""
|
|
|
|
@serve.deployment(num_replicas=3)
|
|
class D:
|
|
def get_pid(self):
|
|
return os.getpid()
|
|
|
|
serve.run(D.bind())
|
|
handle = serve.get_deployment_handle("D", "default")
|
|
|
|
# broadcast() internally waits for replicas to be available.
|
|
pids = handle.broadcast("get_pid").results(timeout_s=10)
|
|
|
|
assert len(pids) == 3
|
|
# Each replica should have a unique PID.
|
|
assert len(set(pids)) == 3
|
|
|
|
|
|
def test_broadcast_with_args(serve_instance):
|
|
"""Test broadcast with positional and keyword arguments."""
|
|
|
|
@serve.deployment(num_replicas=2)
|
|
class D:
|
|
def add(self, a, b=0):
|
|
return a + b
|
|
|
|
serve.run(D.bind())
|
|
handle = serve.get_deployment_handle("D", "default")
|
|
|
|
results = handle.broadcast("add", 1, b=2).results(timeout_s=10)
|
|
|
|
assert len(results) == 2
|
|
assert all(r == 3 for r in results)
|
|
|
|
|
|
def test_broadcast_stateful(serve_instance):
|
|
"""Test broadcast for state mutation (the cache-reset use case)."""
|
|
|
|
@serve.deployment(num_replicas=2)
|
|
class D:
|
|
def __init__(self):
|
|
self.cache = {"key": "value"}
|
|
|
|
def reset_cache(self):
|
|
self.cache.clear()
|
|
return "cleared"
|
|
|
|
def get_cache_size(self):
|
|
return len(self.cache)
|
|
|
|
serve.run(D.bind())
|
|
handle = serve.get_deployment_handle("D", "default")
|
|
|
|
# All replicas should start with cache size 1.
|
|
sizes = handle.broadcast("get_cache_size").results(timeout_s=10)
|
|
assert all(s == 1 for s in sizes)
|
|
|
|
# Broadcast cache reset.
|
|
results = handle.broadcast("reset_cache").results(timeout_s=10)
|
|
assert all(r == "cleared" for r in results)
|
|
|
|
# All replicas should now have empty caches.
|
|
sizes = handle.broadcast("get_cache_size").results(timeout_s=10)
|
|
assert all(s == 0 for s in sizes)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_broadcast_async(serve_instance):
|
|
"""Test the async results path."""
|
|
|
|
@serve.deployment(num_replicas=2)
|
|
class D:
|
|
def get_pid(self):
|
|
return os.getpid()
|
|
|
|
serve.run(D.bind())
|
|
handle = serve.get_deployment_handle("D", "default")
|
|
|
|
pids = await handle.broadcast("get_pid").results_async()
|
|
|
|
assert len(pids) == 2
|
|
assert len(set(pids)) == 2
|
|
|
|
|
|
def test_broadcast_single_replica(serve_instance):
|
|
"""Test broadcast with a single replica."""
|
|
|
|
@serve.deployment(num_replicas=1)
|
|
class D:
|
|
def ping(self):
|
|
return "pong"
|
|
|
|
serve.run(D.bind())
|
|
handle = serve.get_deployment_handle("D", "default")
|
|
|
|
results = handle.broadcast("ping").results(timeout_s=10)
|
|
assert results == ["pong"]
|
|
|
|
|
|
def test_broadcast_ignores_streaming_handle_option(serve_instance):
|
|
"""Test broadcast on a handle configured with stream=True."""
|
|
|
|
@serve.deployment(num_replicas=2)
|
|
class D:
|
|
def ping(self):
|
|
return "pong"
|
|
|
|
serve.run(D.bind())
|
|
handle = serve.get_deployment_handle("D", "default").options(stream=True)
|
|
|
|
results = handle.broadcast("ping").results(timeout_s=10)
|
|
|
|
assert len(results) == 2
|
|
assert all(r == "pong" for r in results)
|
|
|
|
|
|
def test_broadcast_return_exceptions_sync(serve_instance):
|
|
"""Test best-effort sync result collection with exceptions."""
|
|
|
|
@serve.deployment(num_replicas=2)
|
|
class D:
|
|
def fail(self):
|
|
raise RuntimeError("boom")
|
|
|
|
serve.run(D.bind())
|
|
handle = serve.get_deployment_handle("D", "default")
|
|
|
|
with pytest.raises(Exception):
|
|
handle.broadcast("fail").results(timeout_s=10)
|
|
|
|
results = handle.broadcast("fail").results(
|
|
timeout_s=10,
|
|
return_exceptions=True,
|
|
)
|
|
assert len(results) == 2
|
|
assert all(isinstance(r, Exception) for r in results)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_broadcast_return_exceptions_async(serve_instance):
|
|
"""Test best-effort async result collection with exceptions."""
|
|
|
|
@serve.deployment(num_replicas=2)
|
|
class D:
|
|
def fail(self):
|
|
raise RuntimeError("boom")
|
|
|
|
serve.run(D.bind())
|
|
handle = serve.get_deployment_handle("D", "default")
|
|
|
|
with pytest.raises(Exception):
|
|
await handle.broadcast("fail").results_async()
|
|
|
|
results = await handle.broadcast("fail").results_async(return_exceptions=True)
|
|
assert len(results) == 2
|
|
assert all(isinstance(r, Exception) for r in results)
|
|
|
|
|
|
def test_broadcast_sync_timeout(serve_instance):
|
|
"""Test sync timeout handling for broadcast results collection."""
|
|
|
|
@serve.deployment(num_replicas=2)
|
|
class D:
|
|
def slow(self):
|
|
import time
|
|
|
|
time.sleep(10)
|
|
return "done"
|
|
|
|
serve.run(D.bind())
|
|
handle = serve.get_deployment_handle("D", "default")
|
|
|
|
with pytest.raises(TimeoutError):
|
|
handle.broadcast("slow").results(timeout_s=0.5)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_broadcast_async_timeout(serve_instance):
|
|
"""Test async timeout handling for broadcast results collection."""
|
|
|
|
@serve.deployment(num_replicas=2)
|
|
class D:
|
|
def slow(self):
|
|
import time
|
|
|
|
time.sleep(10)
|
|
return "done"
|
|
|
|
serve.run(D.bind())
|
|
handle = serve.get_deployment_handle("D", "default")
|
|
|
|
with pytest.raises(TimeoutError):
|
|
await handle.broadcast("slow").results_async(timeout_s=0.5)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(pytest.main(["-v", "-s", __file__]))
|