Files
ray-project--ray/python/ray/serve/tests/test_websockets.py
T
2026-07-13 13:17:40 +08:00

159 lines
4.4 KiB
Python

import asyncio
import sys
import httpx
import pytest
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from starlette.responses import StreamingResponse
from websockets.exceptions import ConnectionClosed
from websockets.sync.client import connect
import ray
from ray import serve
from ray._common.test_utils import SignalActor
from ray.serve._private.test_utils import get_application_url
@pytest.mark.parametrize("route_prefix", [None, "/prefix"])
def test_send_recv_text_and_binary(serve_instance, route_prefix: str):
app = FastAPI()
@serve.deployment
@serve.ingress(app)
class WebSocketServer:
@app.websocket("/")
async def ws_handler(self, ws: WebSocket):
await ws.accept()
text = await ws.receive_text()
await ws.send_text(text)
bytes = await ws.receive_bytes()
await ws.send_bytes(bytes)
serve.run(WebSocketServer.bind(), route_prefix=route_prefix or "/")
msg = "Hello world!"
url = f"{get_application_url(is_websocket=True, use_localhost=True)}/"
with connect(url) as websocket:
websocket.send(msg)
assert websocket.recv() == msg
websocket.send(msg.encode("utf-8"))
assert websocket.recv().decode("utf-8") == msg
def test_client_disconnect(serve_instance):
app = FastAPI()
@serve.deployment
@serve.ingress(app)
class WebSocketServer:
def __init__(self):
self._disconnected = asyncio.Event()
async def wait_for_disconnect(self):
await self._disconnected.wait()
@app.websocket("/")
async def ws_handler(self, ws: WebSocket):
await ws.accept()
try:
await ws.receive_text()
except WebSocketDisconnect:
print("Client disconnected.")
self._disconnected.set()
h = serve.run(WebSocketServer.bind())
wait_response = h.wait_for_disconnect.remote()
url = f"{get_application_url(is_websocket=True)}/"
with connect(url):
print("Client connected.")
wait_response.result()
@pytest.mark.skipif(sys.platform == "win32", reason="Hanging on Windows.")
def test_server_disconnect(serve_instance):
"""Test that server can properly close WebSocket connections."""
app = FastAPI()
@serve.deployment
@serve.ingress(app)
class WebSocketServer:
@app.websocket("/")
async def ws_handler(self, ws: WebSocket):
await ws.accept()
# Wait for client message, then close with specific code
message = await ws.receive_text()
close_code = int(message)
await ws.close(code=close_code)
serve.run(WebSocketServer.bind())
url = f"{get_application_url(is_websocket=True)}/"
# Test normal close (code 1000)
with connect(url) as websocket:
websocket.send("1000")
with pytest.raises(ConnectionClosed):
websocket.recv()
# Test abnormal close (code 1011)
with connect(url) as websocket:
websocket.send("1011")
with pytest.raises(ConnectionClosed):
websocket.recv()
def test_unary_streaming_websocket_same_deployment(serve_instance):
app = FastAPI()
signal_actor = SignalActor.remote()
@serve.deployment
@serve.ingress(app)
class RenaissanceMan:
@app.get("/")
def say_hi(self):
return "hi"
@app.get("/stream")
def gen_hi(self) -> StreamingResponse:
def gen():
for i in range(5):
yield "hi"
ray.get(signal_actor.wait.remote())
return StreamingResponse(gen(), media_type="text/plain")
@app.websocket("/ws")
async def ws_hi(self, ws: WebSocket):
try:
await ws.accept()
await ws.send_text(await ws.receive_text())
except WebSocketDisconnect:
pass
serve.run(RenaissanceMan.bind())
http_url = get_application_url()
assert httpx.get(http_url).json() == "hi"
with httpx.stream("GET", f"{http_url}/stream") as r:
r.raise_for_status()
for chunk in r.iter_text():
assert chunk == "hi"
ray.get(signal_actor.send.remote())
url = get_application_url(is_websocket=True)
with connect(f"{url}/ws") as ws:
ws.send("hi")
assert ws.recv() == "hi"
if __name__ == "__main__":
sys.exit(pytest.main(["-v", "-s", __file__]))