chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
+420
View File
@@ -0,0 +1,420 @@
import functools
import os
import sys
import threading
import time
from concurrent.futures.thread import ThreadPoolExecutor
from typing import Dict
import httpx
import pytest
import ray
from ray import serve
from ray._common.test_utils import SignalActor, wait_for_condition
from ray.serve._private.common import DeploymentStatus
from ray.serve._private.logging_utils import get_serve_logs_dir
from ray.serve._private.test_utils import (
SharedFlag,
check_deployment_status,
check_num_replicas_eq,
get_application_url,
)
from ray.serve._private.utils import get_component_file_name
from ray.serve.schema import ApplicationStatus
from ray.util.state import list_actors
def test_deploy_zero_cpus(serve_instance):
@serve.deployment(ray_actor_options={"num_cpus": 0})
class D:
def hello(self):
return "hello"
h = serve.run(D.bind())
assert h.hello.remote().result() == "hello"
def test_deployment_error_handling(serve_instance):
@serve.deployment
def f():
pass
with pytest.raises(RuntimeError):
# This is an invalid configuration since dynamic upload of working
# directories is not supported. The error this causes in the controller
# code should be caught and reported back to the `deploy` caller.
serve.run(
f.options(ray_actor_options={"runtime_env": {"working_dir": "."}}).bind()
)
def test_json_serialization_user_config(serve_instance):
"""See https://github.com/ray-project/ray/issues/25345.
See https://github.com/ray-project/ray/pull/26235 for additional context
about this test.
"""
@serve.deployment(name="simple-deployment")
class SimpleDeployment:
value: str
nested_value: str
def reconfigure(self, config: Dict) -> None:
self.value = config["value"]
self.nested_value = config["nested"]["value"]
def get_value(self) -> None:
return self.value
def get_nested_value(self) -> None:
return self.nested_value
app = SimpleDeployment.options(
user_config={
"value": "Success!",
"nested": {"value": "Success!"},
}
).bind()
handle = serve.run(app)
assert handle.get_value.remote().result() == "Success!"
assert handle.get_nested_value.remote().result() == "Success!"
handle = serve.run(
SimpleDeployment.options(
user_config={
"value": "Failure!",
"another-value": "Failure!",
"nested": {"value": "Success!"},
}
).bind()
)
assert handle.get_value.remote().result() == "Failure!"
assert handle.get_nested_value.remote().result() == "Success!"
def test_http_proxy_request_cancellation(serve_instance):
# https://github.com/ray-project/ray/issues/21425
s = SignalActor.remote()
@serve.deployment(max_ongoing_requests=1)
class A:
def __init__(self) -> None:
self.counter = 0
async def __call__(self):
self.counter += 1
ret_val = self.counter
await s.wait.remote()
return ret_val
serve.run(A.bind())
url = get_application_url("HTTP")
# Windows usually resolves "localhost" to the IPv6 loopback ::1 first, but the
# Serve proxy is listening only on IPv4. The initial TCP connect then hangs,
# breaking the shorttimeout logic in this test. Using the literal IPv4 address
# 127.0.0.1 skips the IPv6 attempt and makes the test deterministic on Windows.
if sys.platform == "win32":
url = url.replace("localhost", "127.0.0.1")
with ThreadPoolExecutor() as pool:
# Send the first request, it should block for the result
first_blocking_fut = pool.submit(functools.partial(httpx.get, url, timeout=100))
wait_for_condition(lambda: ray.get(s.cur_num_waiters.remote()) == 1)
assert not first_blocking_fut.done()
# Send more requests, these should be queued in handle.
# But because first request is hanging and these have low timeout.
# They should all disconnect from http connection.
# These requests should never reach the replica.
rest_blocking_futs = [
pool.submit(functools.partial(httpx.get, url, timeout=0.5))
for _ in range(3)
]
wait_for_condition(lambda: all(f.done() for f in rest_blocking_futs))
# Now unblock the first request.
ray.get(s.send.remote())
assert first_blocking_fut.result().text == "1"
# Sending another request to verify that only one request has been
# processed so far.
assert httpx.get(url).text == "2"
def test_nonserializable_deployment(serve_instance):
lock = threading.Lock()
class D:
def hello(self, _):
return lock
# Check that the `inspect_serializability` trace was printed
with pytest.raises(
TypeError,
match=r"Could not serialize the deployment[\s\S]*was found to be non-serializable.*", # noqa
):
serve.deployment(D)
@serve.deployment
class E:
def __init__(self, arg):
self.arg = arg
with pytest.raises(TypeError, match="pickle"):
serve.run(E.bind(lock))
with pytest.raises(TypeError, match="pickle"):
serve.run(E.bind(arg=lock))
def test_deploy_application_unhealthy(serve_instance):
"""Test deploying an application that becomes unhealthy."""
event = SharedFlag.remote()
@serve.deployment(health_check_period_s=1, health_check_timeout_s=3)
class Model:
def __call__(self):
return "hello world"
def check_health(self):
if ray.get(event.is_set.remote()):
raise RuntimeError("Intentionally failing.")
handle = serve.run(Model.bind(), name="app")
assert handle.remote().result() == "hello world"
assert serve.status().applications["app"].status == ApplicationStatus.RUNNING
# When a deployment becomes unhealthy, application should transition -> UNHEALTHY
event.set.remote()
wait_for_condition(
lambda: serve.status().applications["app"].status == ApplicationStatus.UNHEALTHY
)
# Check that application stays unhealthy
for _ in range(10):
assert serve.status().applications["app"].status == ApplicationStatus.UNHEALTHY
time.sleep(0.1)
# At least 10 control loop iterations should have passed. Check that
# the logs from application state manager notifying about unhealthy
# deployments doesn't spam, they should get printed only once.
controller_pid = [
actor["pid"]
for actor in list_actors()
if actor["name"] == "SERVE_CONTROLLER_ACTOR"
][0]
controller_log_file_name = get_component_file_name(
"controller", controller_pid, component_type=None, suffix=".log"
)
controller_log_path = os.path.join(get_serve_logs_dir(), controller_log_file_name)
with open(controller_log_path, "r") as f:
s = f.read()
assert s.count("The deployments ['Model'] are UNHEALTHY.") <= 1
@pytest.mark.skipif(
sys.platform == "win32", reason="Runtime env support experimental on windows"
)
def test_deploy_bad_pip_package_deployment(serve_instance):
"""Test deploying with a bad runtime env at deployment level."""
@serve.deployment(ray_actor_options={"runtime_env": {"pip": ["does_not_exist"]}})
class Model:
def __call__(self):
return "hello world"
serve._run(Model.bind(), _blocking=False)
def check_fail():
app_status = serve.status().applications["default"]
assert app_status.status == ApplicationStatus.DEPLOY_FAILED
deployment_message = app_status.deployments["Model"].message
assert "No matching distribution found for does_not_exist" in deployment_message
return True
wait_for_condition(check_fail, timeout=60)
def test_deploy_same_deployment_name_different_app(serve_instance):
@serve.deployment
class Model:
def __init__(self, name):
self.name = name
def __call__(self):
return f"hello {self.name}"
serve.run(Model.bind("alice"), name="app1", route_prefix="/app1")
serve.run(Model.bind("bob"), name="app2", route_prefix="/app2")
url = get_application_url("HTTP", app_name="app1")
assert httpx.get(f"{url}").text == "hello alice"
url_without_route_prefix = get_application_url(
"HTTP", app_name="app1", exclude_route_prefix=True
)
routes_url = f"{url_without_route_prefix}/-/routes"
routes = httpx.get(routes_url).json()
assert routes["/app1"] == "app1"
url = get_application_url("HTTP", app_name="app2")
assert httpx.get(f"{url}").text == "hello bob"
url_without_route_prefix = get_application_url(
"HTTP", app_name="app2", exclude_route_prefix=True
)
routes_url = f"{url_without_route_prefix}/-/routes"
routes = httpx.get(routes_url).json()
assert routes["/app2"] == "app2"
app1_status = serve.status().applications["app1"]
app2_status = serve.status().applications["app2"]
assert app1_status.status == "RUNNING"
assert app1_status.deployments["Model"].status == "HEALTHY"
assert app2_status.status == "RUNNING"
assert app2_status.deployments["Model"].status == "HEALTHY"
@pytest.mark.parametrize("use_options", [True, False])
def test_num_replicas_auto_api(serve_instance, use_options):
"""Test setting only `num_replicas="auto"`."""
signal = SignalActor.remote()
class A:
async def __call__(self):
await signal.wait.remote()
if use_options:
A = serve.deployment(A).options(num_replicas="auto")
else:
A = serve.deployment(num_replicas="auto")(A)
serve.run(A.bind(), name="default")
wait_for_condition(
check_deployment_status, name="A", expected_status=DeploymentStatus.HEALTHY
)
check_num_replicas_eq("A", 1)
app_details = serve_instance.get_serve_details()["applications"]["default"]
deployment_config = app_details["deployments"]["A"]["deployment_config"]
assert "num_replicas" not in deployment_config
assert deployment_config["max_ongoing_requests"] == 5
assert deployment_config["autoscaling_config"] == {
# Set by `num_replicas="auto"`
"target_ongoing_requests": 2.0,
"min_replicas": 1,
"max_replicas": 100,
# Untouched defaults
"metrics_interval_s": 10.0,
"upscale_delay_s": 30.0,
"look_back_period_s": 30.0,
"downscale_delay_s": 600.0,
"downscale_to_zero_delay_s": None,
"upscale_smoothing_factor": None,
"downscale_smoothing_factor": None,
"upscaling_factor": None,
"downscaling_factor": None,
"smoothing_factor": 1.0,
"initial_replicas": None,
"aggregation_function": "mean",
"policy": {
"policy_function": "ray.serve.autoscaling_policy:default_autoscaling_policy",
"policy_kwargs": {},
},
}
@pytest.mark.parametrize("use_options", [True, False])
def test_num_replicas_auto_basic(serve_instance, use_options):
"""Test `num_replicas="auto"` and the defaults are used by autoscaling."""
signal = SignalActor.remote()
class A:
async def __call__(self):
await signal.wait.remote()
if use_options:
A = serve.deployment(A).options(
num_replicas="auto",
autoscaling_config={
"metrics_interval_s": 1,
"upscale_delay_s": 1,
"look_back_period_s": 2,
# Shrink from the 600s default so the transient extra replica
# (metric reading fractionally over target) downscales back
# within the test's wait window.
"downscale_delay_s": 4,
},
graceful_shutdown_timeout_s=1,
)
else:
A = serve.deployment(
num_replicas="auto",
autoscaling_config={
"metrics_interval_s": 1,
"upscale_delay_s": 1,
"look_back_period_s": 2,
# Shrink from the 600s default so the transient extra replica
# (metric reading fractionally over target) downscales back
# within the test's wait window.
"downscale_delay_s": 4,
},
graceful_shutdown_timeout_s=1,
)(A)
h = serve.run(A.bind(), name="default")
wait_for_condition(
check_deployment_status, name="A", expected_status=DeploymentStatus.HEALTHY
)
check_num_replicas_eq("A", 1)
app_details = serve_instance.get_serve_details()["applications"]["default"]
deployment_config = app_details["deployments"]["A"]["deployment_config"]
assert "num_replicas" not in deployment_config
assert deployment_config["max_ongoing_requests"] == 5
assert deployment_config["autoscaling_config"] == {
# Set by `num_replicas="auto"`
"target_ongoing_requests": 2.0,
"min_replicas": 1,
"max_replicas": 100,
# Overrided by `autoscaling_config`
"metrics_interval_s": 1.0,
"upscale_delay_s": 1.0,
"downscale_delay_s": 4.0,
# Untouched defaults
"look_back_period_s": 2.0,
"downscale_to_zero_delay_s": None,
"upscale_smoothing_factor": None,
"downscale_smoothing_factor": None,
"upscaling_factor": None,
"downscaling_factor": None,
"smoothing_factor": 1.0,
"initial_replicas": None,
"aggregation_function": "mean",
"policy": {
"policy_function": "ray.serve.autoscaling_policy:default_autoscaling_policy",
"policy_kwargs": {},
},
}
for i in range(3):
[h.remote() for _ in range(2)]
def check_num_waiters(target: int):
assert ray.get(signal.cur_num_waiters.remote()) == target
return True
wait_for_condition(check_num_waiters, target=2 * (i + 1), timeout=30)
print(time.time(), f"Number of waiters on signal reached {2*(i+1)}.")
wait_for_condition(check_num_replicas_eq, name="A", target=i + 1, timeout=30)
print(time.time(), f"Confirmed number of replicas are at {i+1}.")
if __name__ == "__main__":
sys.exit(pytest.main(["-v", "-s", __file__]))