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

463 lines
16 KiB
Python

import subprocess
import sys
import time
import httpx
import pytest
import yaml
from fastapi import FastAPI
import ray
from ray import serve
from ray._common.test_utils import wait_for_condition
from ray._common.usage.usage_lib import get_extra_usage_tags_to_report
from ray.serve._private.constants import (
RAY_SERVE_USE_GRPC_BY_DEFAULT,
SERVE_MULTIPLEXED_MODEL_ID,
)
from ray.serve._private.test_utils import (
check_apps_running,
check_telemetry,
)
from ray.serve._private.usage import ServeUsageTag
from ray.serve.context import _get_global_client
from ray.serve.schema import ApplicationStatus, ServeDeploySchema
def test_fastapi_detected(manage_ray_with_telemetry):
"""
Check that FastAPI is detected by telemetry.
"""
storage_handle = manage_ray_with_telemetry
# Check that telemetry related to FastAPI app is not set
def check_report_before_fastapi():
report = ray.get(storage_handle.get_report.remote())
assert ServeUsageTag.FASTAPI_USED.get_value_from_report(report) is None
assert ServeUsageTag.API_VERSION.get_value_from_report(report) == "v2"
assert int(ServeUsageTag.NUM_APPS.get_value_from_report(report)) == 1
assert int(ServeUsageTag.NUM_DEPLOYMENTS.get_value_from_report(report)) == 1
assert int(ServeUsageTag.NUM_GPU_DEPLOYMENTS.get_value_from_report(report)) == 0
return True
wait_for_condition(check_report_before_fastapi)
app = FastAPI()
@serve.deployment
@serve.ingress(app)
class FastAPIDeployment:
@app.get("/route1")
async def route1(self):
return "Welcome to route 1!"
@app.get("/route2")
async def app2(self):
return "Welcome to route 2!"
fastapi_app = FastAPIDeployment.bind()
serve.run(fastapi_app, name="fastapi_app", route_prefix="/fastapi")
wait_for_condition(
lambda: serve.status().applications["fastapi_app"].status
== ApplicationStatus.RUNNING,
timeout=15,
)
current_num_reports = ray.get(storage_handle.get_reports_received.remote())
wait_for_condition(
lambda: ray.get(storage_handle.get_reports_received.remote())
> current_num_reports,
timeout=5,
)
report = ray.get(storage_handle.get_report.remote())
# Check all telemetry relevant to the Serve apps on this cluster.
assert int(ServeUsageTag.FASTAPI_USED.get_value_from_report(report)) == 1
assert ServeUsageTag.API_VERSION.get_value_from_report(report) == "v2"
assert int(ServeUsageTag.NUM_APPS.get_value_from_report(report)) == 2
assert int(ServeUsageTag.NUM_DEPLOYMENTS.get_value_from_report(report)) == 2
assert int(ServeUsageTag.NUM_GPU_DEPLOYMENTS.get_value_from_report(report)) == 0
# Check that Serve telemetry not relevant to the running apps is omitted.
assert ServeUsageTag.DAG_DRIVER_USED.get_value_from_report(report) is None
assert ServeUsageTag.HTTP_ADAPTER_USED.get_value_from_report(report) is None
assert ServeUsageTag.GRPC_INGRESS_USED.get_value_from_report(report) is None
assert ServeUsageTag.REST_API_VERSION.get_value_from_report(report) is None
@serve.deployment
class Stub:
pass
stub_app = Stub.bind()
@pytest.mark.skipif(sys.platform == "win32", reason="File path incorrect on Windows.")
def test_rest_api(manage_ray_with_telemetry, tmp_dir):
"""
Check that telemetry works with REST API.
"""
storage = manage_ray_with_telemetry
# Check that REST API telemetry is not set
check_telemetry(ServeUsageTag.REST_API_VERSION, expected=None)
config = {
"applications": [
{
"name": "stub_app",
"import_path": "ray.serve.tests.test_telemetry_1.stub_app",
"route_prefix": "/stub",
},
]
}
config_file_path = f"{tmp_dir}/config.yaml"
with open(config_file_path, "w+") as f:
yaml.safe_dump(config, f)
subprocess.check_output(["serve", "deploy", config_file_path])
client = _get_global_client()
# Make sure the applications are RUNNING.
wait_for_condition(check_apps_running, apps=["stub_app"], timeout=15)
current_num_reports = ray.get(storage.get_reports_received.remote())
wait_for_condition(
lambda: ray.get(storage.get_reports_received.remote()) > current_num_reports,
timeout=5,
)
report = ray.get(storage.get_report.remote())
# Check all telemetry relevant to the Serve apps on this cluster
assert ServeUsageTag.REST_API_VERSION.get_value_from_report(report) == "v2"
assert ServeUsageTag.API_VERSION.get_value_from_report(report) == "v2"
assert int(ServeUsageTag.NUM_GPU_DEPLOYMENTS.get_value_from_report(report)) == 0
# Assert num of deployments from controller.
assert len(client.get_all_deployment_statuses()) == 2
assert int(ServeUsageTag.NUM_APPS.get_value_from_report(report)) == 2
assert int(ServeUsageTag.NUM_DEPLOYMENTS.get_value_from_report(report)) == 2
# Check that Serve telemetry not relevant to the running apps is omitted
assert ServeUsageTag.FASTAPI_USED.get_value_from_report(report) is None
assert ServeUsageTag.GRPC_INGRESS_USED.get_value_from_report(report) is None
assert ServeUsageTag.HTTP_ADAPTER_USED.get_value_from_report(report) is None
assert ServeUsageTag.DAG_DRIVER_USED.get_value_from_report(report) is None
assert ServeUsageTag.AUTO_NUM_REPLICAS_USED.get_value_from_report(report) is None
# Check that app deletions are tracked.
new_config = {"applications": []}
with open(config_file_path, "w+") as f:
yaml.safe_dump(new_config, f)
subprocess.check_output(["serve", "deploy", config_file_path])
wait_for_condition(
lambda: int(
ServeUsageTag.NUM_APPS.get_value_from_report(
ray.get(storage.get_report.remote())
)
)
== 1,
timeout=15,
)
report = ray.get(storage.get_report.remote())
assert int(ServeUsageTag.NUM_APPS.get_value_from_report(report)) == 1
assert int(ServeUsageTag.NUM_DEPLOYMENTS.get_value_from_report(report)) == 1
@serve.deployment(ray_actor_options={"num_cpus": 0})
class Tester:
def __call__(self, *args):
pass
def reconfigure(self, *args):
pass
tester = Tester.bind()
@pytest.mark.parametrize(
"lightweight_option,value,new_value",
[
("num_replicas", 1, 2),
("user_config", {}, {"some_setting": 10}),
("autoscaling_config", {"max_replicas": 3}, {"max_replicas": 5}),
],
)
def test_lightweight_config_options(
manage_ray_with_telemetry, lightweight_option, value, new_value
):
"""Check that lightweight config options are detected by telemetry."""
storage = manage_ray_with_telemetry
lightweight_tagkeys = {
"num_replicas": ServeUsageTag.NUM_REPLICAS_LIGHTWEIGHT_UPDATED,
"user_config": ServeUsageTag.USER_CONFIG_LIGHTWEIGHT_UPDATED,
"autoscaling_config": ServeUsageTag.AUTOSCALING_CONFIG_LIGHTWEIGHT_UPDATED,
}
# Check that REST API telemetry is not set
check_telemetry(ServeUsageTag.REST_API_VERSION, expected=None)
config = {
"applications": [
{
"name": "test_app",
"import_path": "ray.serve.tests.test_telemetry_1.tester",
"deployments": [{"name": "Tester"}],
},
]
}
config["applications"][0]["deployments"][0][lightweight_option] = value
# Deploy first config
client = _get_global_client()
client.deploy_apps(ServeDeploySchema(**config))
wait_for_condition(check_apps_running, apps=["test_app"], timeout=15)
current_num_reports = ray.get(storage.get_reports_received.remote())
wait_for_condition(
lambda: ray.get(storage.get_reports_received.remote()) > current_num_reports,
timeout=10,
)
report = ray.get(storage.get_report.remote())
# Check
assert int(ServeUsageTag.NUM_APPS.get_value_from_report(report)) == 2
assert ServeUsageTag.API_VERSION.get_value_from_report(report) == "v2"
for tagkey in lightweight_tagkeys.values():
assert tagkey.get_value_from_report(report) is None
# Change config and deploy again
config["applications"][0]["deployments"][0][lightweight_option] = new_value
client.deploy_apps(ServeDeploySchema(**config))
wait_for_condition(check_apps_running, apps=["test_app"], timeout=15)
# Check again
wait_for_condition(
check_telemetry,
tag=lightweight_tagkeys[lightweight_option],
expected="True",
timeout=5,
)
report = ray.get(storage.get_report.remote())
assert int(ServeUsageTag.NUM_APPS.get_value_from_report(report)) == 2
assert ServeUsageTag.API_VERSION.get_value_from_report(report) == "v2"
for tagkey in lightweight_tagkeys.values():
if tagkey != lightweight_tagkeys[lightweight_option]:
assert tagkey.get_value_from_report(report) is None
@pytest.mark.parametrize("call_in_deployment", [False, True])
def test_handle_apis_detected(manage_ray_with_telemetry, call_in_deployment):
"""Check that the various handles are detected correctly by telemetry."""
check_telemetry(ServeUsageTag.DEPLOYMENT_HANDLE_API_USED, expected=None)
@serve.deployment
class Downstream:
def __call__(self):
return "hi"
@serve.deployment
class Caller:
def __init__(self, h):
self._h = h
async def __call__(self, call_downstream=True):
if call_downstream:
await self._h.remote()
return "ok"
handle = serve.run(Caller.bind(Downstream.bind()))
if call_in_deployment:
result = httpx.get("http://localhost:8000/").text
else:
result = handle.remote(call_downstream=False).result()
assert result == "ok"
wait_for_condition(
check_telemetry, tag=ServeUsageTag.DEPLOYMENT_HANDLE_API_USED, expected="1"
)
@pytest.mark.skipif(
RAY_SERVE_USE_GRPC_BY_DEFAULT,
reason="cannot convert to object ref when using gRPC",
)
@pytest.mark.parametrize("mode", ["http", "outside_deployment", "inside_deployment"])
def test_deployment_handle_to_obj_ref_detected(manage_ray_with_telemetry, mode):
"""Check that the handle to_object_ref API is detected correctly by telemetry."""
check_telemetry(
ServeUsageTag.DEPLOYMENT_HANDLE_TO_OBJECT_REF_API_USED, expected=None
)
@serve.deployment
class Downstream:
def __call__(self):
return "hi"
@serve.deployment
class Caller:
def __init__(self, h):
self._h = h
async def get(self, call_downstream=False):
if call_downstream:
await self._h.remote()._to_object_ref()
return "ok"
async def __call__(self):
return await self.get()
handle = serve.run(Caller.bind(Downstream.bind()))
if mode == "http":
result = httpx.get("http://localhost:8000/").text
elif mode == "outside_deployment":
result = ray.get(handle.get.remote()._to_object_ref_sync())
else:
result = handle.get.remote(call_downstream=True).result()
assert result == "ok"
if mode == "http":
for _ in range(20):
check_telemetry(
ServeUsageTag.DEPLOYMENT_HANDLE_TO_OBJECT_REF_API_USED, expected=None
)
time.sleep(0.1)
else:
wait_for_condition(
check_telemetry,
tag=ServeUsageTag.DEPLOYMENT_HANDLE_TO_OBJECT_REF_API_USED,
expected="1",
)
def test_multiplexed_detect(manage_ray_with_telemetry):
"""Check that multiplexed api is detected by telemetry."""
@serve.deployment
class Model:
@serve.multiplexed(max_num_models_per_replica=1)
async def get_model(self, tag):
return tag
async def __call__(self, request):
tag = serve.get_multiplexed_model_id()
await self.get_model(tag)
return tag
serve.run(Model.bind(), name="app", route_prefix="/app")
wait_for_condition(check_apps_running, apps=["app"])
check_telemetry(ServeUsageTag.MULTIPLEXED_API_USED, expected=None)
headers = {SERVE_MULTIPLEXED_MODEL_ID: "1"}
resp = httpx.get("http://localhost:8000/app", headers=headers)
assert resp.status_code == 200
wait_for_condition(
check_telemetry, tag=ServeUsageTag.MULTIPLEXED_API_USED, expected="1"
)
class TestProxyTelemetry:
def test_both_proxies_detected(manage_ray, ray_shutdown):
"""Test that both HTTP and gRPC proxies are detected by telemetry.
When both HTTP and gRPC proxies are used, both telemetry should be detected.
"""
result = get_extra_usage_tags_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
report = {"extra_usage_tags": result}
# Ensure neither the HTTP nor gRPC proxy telemetry exist.
assert ServeUsageTag.HTTP_PROXY_USED.get_value_from_report(report) is None
assert ServeUsageTag.GRPC_PROXY_USED.get_value_from_report(report) is None
grpc_servicer_functions = [
"ray.serve.generated.serve_pb2_grpc."
"add_UserDefinedServiceServicer_to_server",
]
serve.start(grpc_options={"grpc_servicer_functions": grpc_servicer_functions})
result = get_extra_usage_tags_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
report = {"extra_usage_tags": result}
# Ensure both HTTP and gRPC proxy telemetry exist.
assert int(ServeUsageTag.HTTP_PROXY_USED.get_value_from_report(report)) == 1
assert int(ServeUsageTag.GRPC_PROXY_USED.get_value_from_report(report)) == 1
def test_only_http_proxy_detected(manage_ray, ray_shutdown):
"""Test that only HTTP proxy is detected by telemetry.
When only HTTP proxy is used, only the http proxy telemetry should be detected.
"""
result = get_extra_usage_tags_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
report = {"extra_usage_tags": result}
# Ensure the telemetry does not yet exist.
assert ServeUsageTag.HTTP_PROXY_USED.get_value_from_report(report) is None
assert ServeUsageTag.GRPC_PROXY_USED.get_value_from_report(report) is None
serve.start()
result = get_extra_usage_tags_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
report = {"extra_usage_tags": result}
# Ensure only the HTTP proxy telemetry exist.
assert int(ServeUsageTag.HTTP_PROXY_USED.get_value_from_report(report)) == 1
assert ServeUsageTag.GRPC_PROXY_USED.get_value_from_report(report) is None
def test_no_proxy_detected(manage_ray, ray_shutdown):
"""Test that no proxy is detected by telemetry.
When neither HTTP nor gRPC proxy is used, no proxy telemetry should be detected.
"""
result = get_extra_usage_tags_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
report = {"extra_usage_tags": result}
# Ensure neither the HTTP nor gRPC proxy telemetry exist.
assert ServeUsageTag.HTTP_PROXY_USED.get_value_from_report(report) is None
assert ServeUsageTag.GRPC_PROXY_USED.get_value_from_report(report) is None
serve.start(http_options={"location": "NoServer"})
result = get_extra_usage_tags_to_report(
ray.experimental.internal_kv.internal_kv_get_gcs_client()
)
report = {"extra_usage_tags": result}
# Ensure neither the HTTP nor gRPC proxy telemetry exist.
assert ServeUsageTag.HTTP_PROXY_USED.get_value_from_report(report) is None
assert ServeUsageTag.GRPC_PROXY_USED.get_value_from_report(report) is None
if __name__ == "__main__":
sys.exit(pytest.main(["-v", "-s", __file__]))