Files
2026-07-13 13:17:40 +08:00

125 lines
3.7 KiB
Python

import sys
import threading
from time import sleep
import pytest
from ray._common.constants import HEAD_NODE_RESOURCE_NAME
from ray._common.test_utils import wait_for_condition
from ray.tests.conftest_docker import * # noqa
scripts = """
import json
import os
import ray
from ray import serve
@serve.deployment
class GetPID:
def __call__(self, *args):
return {{"pid": os.getpid()}}
serve.run(GetPID.options(num_replicas={num_replicas}).bind())
"""
check_script = """
import ray
import requests
@ray.remote
def get_pid():
return requests.get("http://127.0.0.1:8000/").json()["pid"]
pids = {{
requests.get("http://127.0.0.1:8000/").json()["pid"]
for _ in range(20)
}}
print(pids)
assert len(pids) == {num_replicas}
"""
check_ray_nodes_script = """
import ray
ray.init(address="auto")
print(ray.nodes())
"""
@pytest.mark.skipif(sys.platform != "linux", reason="Only works on linux.")
def test_ray_serve_basic(docker_cluster):
# This test covers the basic cases for serve ha
# - It starts the serve on worker nodes.
# - Check the deployment is OK
# - Stop headnode
# - Check the serve app is running healthy
# - Start a reconfig (2 replicas) and it'll hang
# - Start head node. The script will continue once GCS is back
# - Make sure two replicas are there
# TODO(iycheng): Update serve to better integrate with GCS HA:
# - Make sure no task can run in the raylet where GCS is deployed.
head, worker = docker_cluster
output = worker.exec_run(cmd=f"python -c '{scripts.format(num_replicas=1)}'")
assert output.exit_code == 0, output.output
assert b"Adding 1 replica to Deployment(" in output.output
output = worker.exec_run(cmd=f"python -c '{check_script.format(num_replicas=1)}'")
assert output.exit_code == 0, output.output
# Kill the head node.
head.kill()
# Make sure the Serve application can still handle traffic.
output = worker.exec_run(cmd=f"python -c '{check_script.format(num_replicas=1)}'")
assert output.exit_code == 0, output.output
# Scale up the application in a background thread so we can concurrently kill the
# head node.
def reconfig():
worker.exec_run(cmd=f"python -c '{scripts.format(num_replicas=2)}'")
t = threading.Thread(target=reconfig)
t.start()
sleep(1)
# Updating the application should continue once the head node is back online.
head.restart()
t.join()
# Ensure head node is up before calling check_script on the worker again.
def check_for_head_node_come_back_up():
_output = head.exec_run(cmd=f"python -c '{check_ray_nodes_script}'")
return (
_output.exit_code == 0
and bytes(HEAD_NODE_RESOURCE_NAME, "utf-8") in _output.output
)
wait_for_condition(check_for_head_node_come_back_up)
# Check that the application has been updated.
output = worker.exec_run(cmd=f"python -c '{check_script.format(num_replicas=2)}'")
assert output.exit_code == 0, output.output
# Make sure the serve controller still runs on the head node after restart.
check_controller_head_node_script = """
import ray
import requests
from ray.serve.schema import ServeInstanceDetails
from ray._common.constants import HEAD_NODE_RESOURCE_NAME
ray.init(address="auto")
head_node_id = ray.get_runtime_context().get_node_id()
serve_details = ServeInstanceDetails(
**requests.get("http://localhost:8265/api/serve/applications/").json())
assert serve_details.controller_info.node_id == head_node_id
"""
output = head.exec_run(cmd=f"python -c '{check_controller_head_node_script}'")
assert output.exit_code == 0, output.output
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))