项目文件夹

文件
2026-07-13 13:17:40 +08:00

1076 行
37 KiB
Python

import random
import sys
from typing import DefaultDict, Dict, List
import grpc
import httpx
import pytest
from fastapi import FastAPI
import ray
from ray import serve
from ray._common.test_utils import (
PrometheusTimeseries,
SignalActor,
fetch_prometheus_metric_timeseries,
wait_for_condition,
)
from ray.serve._private.constants import DEFAULT_LATENCY_BUCKET_MS
from ray.serve._private.test_utils import (
PROMETHEUS_METRICS_TIMEOUT_S,
get_application_url,
ping_fruit_stand,
ping_grpc_call_method,
)
from ray.serve.handle import DeploymentHandle
from ray.serve.metrics import Counter, Gauge, Histogram
from ray.serve.tests.test_config_files.grpc_deployment import g, g2
from ray.serve.tests.test_metrics import (
check_metric_float_eq,
check_sum_metric_eq,
get_metric_dictionaries,
)
@serve.deployment
class WaitForSignal:
async def __call__(self):
signal = ray.get_actor("signal123")
await signal.wait.remote()
@serve.deployment
class Router:
def __init__(self, handles):
self.handles = handles
async def __call__(self, index: int):
return await self.handles[index - 1].remote()
@ray.remote
def call(deployment_name, app_name, *args):
handle = DeploymentHandle(deployment_name, app_name)
handle.remote(*args)
@ray.remote
class CallActor:
def __init__(self, deployment_name: str, app_name: str):
self.handle = DeploymentHandle(deployment_name, app_name)
async def call(self, *args):
await self.handle.remote(*args)
class TestRequestContextMetrics:
def _generate_metrics_summary(self, metrics: List[Dict]):
"""Generate "route" and "application" information from metrics.
Args:
metrics: List of metric dictionaries, each generated by the
get_metric_dictionaries function.
Returns:
Tuple[dict, dict]:
- The first dictionary maps deployment names to a set of routes.
- The second dictionary maps deployment names to application names.
"""
metrics_summary_route = DefaultDict(set)
metrics_summary_app = DefaultDict(str)
for request_metrics in metrics:
metrics_summary_route[request_metrics["deployment"]].add(
request_metrics["route"]
)
metrics_summary_app[request_metrics["deployment"]] = request_metrics[
"application"
]
return metrics_summary_route, metrics_summary_app
def verify_metrics(self, metric, expected_output):
for key in expected_output:
assert metric[key] == expected_output[key]
def test_request_context_pass_for_http_proxy(self, metrics_start_shutdown):
"""Test HTTP proxy passing request context"""
@serve.deployment(graceful_shutdown_timeout_s=0.001)
def f():
return "hello"
@serve.deployment(graceful_shutdown_timeout_s=0.001)
def g():
return "world"
@serve.deployment(graceful_shutdown_timeout_s=0.001)
def h():
return 1 / 0
serve.run(f.bind(), name="app1", route_prefix="/app1")
serve.run(g.bind(), name="app2", route_prefix="/app2")
serve.run(h.bind(), name="app3", route_prefix="/app3")
resp = httpx.get("http://127.0.0.1:8000/app1")
assert resp.status_code == 200
assert resp.text == "hello"
resp = httpx.get("http://127.0.0.1:8000/app2")
assert resp.status_code == 200
assert resp.text == "world"
resp = httpx.get("http://127.0.0.1:8000/app3")
assert resp.status_code == 500
timeseries = PrometheusTimeseries()
wait_for_condition(
lambda: len(
get_metric_dictionaries(
"ray_serve_deployment_processing_latency_ms_sum",
timeseries=timeseries,
wait=False,
)
)
== 3,
timeout=40,
)
def wait_for_route_and_name(
metric_name: str,
deployment_name: str,
app_name: str,
route: str,
timeout: float = 5,
):
"""Waits for app name and route to appear in deployment's metric."""
def check():
# Check replica qps & latency
(
qps_metrics_route,
qps_metrics_app_name,
) = self._generate_metrics_summary(
get_metric_dictionaries(metric_name, timeseries=timeseries),
)
assert qps_metrics_app_name[deployment_name] == app_name
assert qps_metrics_route[deployment_name] == {route}
return True
wait_for_condition(check, timeout=timeout)
# Check replica qps & latency
wait_for_route_and_name(
"ray_serve_deployment_request_counter_total", "f", "app1", "/app1"
)
wait_for_route_and_name(
"ray_serve_deployment_request_counter_total", "g", "app2", "/app2"
)
wait_for_route_and_name(
"ray_serve_deployment_error_counter_total", "h", "app3", "/app3"
)
# Check http proxy qps & latency
for metric_name in [
"ray_serve_num_http_requests_total",
"ray_serve_http_request_latency_ms_sum",
]:
metrics = [
sample.labels
for sample in fetch_prometheus_metric_timeseries(
["localhost:9999"],
timeseries,
timeout=PROMETHEUS_METRICS_TIMEOUT_S,
)[metric_name]
]
assert {metric["route"] for metric in metrics} == {
"/app1",
"/app2",
"/app3",
}
for metric_name in [
"ray_serve_handle_request_counter_total",
"ray_serve_num_router_requests_total",
"ray_serve_deployment_processing_latency_ms_sum",
]:
metrics_route, metrics_app_name = self._generate_metrics_summary(
[
sample.labels
for sample in fetch_prometheus_metric_timeseries(
["localhost:9999"],
timeseries,
timeout=PROMETHEUS_METRICS_TIMEOUT_S,
)[metric_name]
]
)
msg = f"Incorrect metrics for {metric_name}"
assert metrics_route["f"] == {"/app1"}, msg
assert metrics_route["g"] == {"/app2"}, msg
assert metrics_route["h"] == {"/app3"}, msg
assert metrics_app_name["f"] == "app1", msg
assert metrics_app_name["g"] == "app2", msg
assert metrics_app_name["h"] == "app3", msg
def test_request_context_pass_for_grpc_proxy(self, metrics_start_shutdown):
"""Test gRPC proxy passing request context"""
@serve.deployment(graceful_shutdown_timeout_s=0.001)
class H:
def __call__(self, *args, **kwargs):
return 1 / 0
h = H.bind()
app_name1 = "app1"
depl_name1 = "grpc-deployment"
app_name2 = "app2"
depl_name2 = "grpc-deployment-model-composition"
app_name3 = "app3"
depl_name3 = "H"
serve.run(g, name=app_name1, route_prefix="/app1")
serve.run(g2, name=app_name2, route_prefix="/app2")
serve.run(h, name=app_name3, route_prefix="/app3")
channel = grpc.insecure_channel("localhost:9000")
ping_grpc_call_method(channel, app_name1)
ping_fruit_stand(channel, app_name2)
with pytest.raises(grpc.RpcError):
ping_grpc_call_method(channel, app_name3)
timeseries = PrometheusTimeseries()
# app1 has 1 deployment, app2 has 3 deployments, and app3 has 1 deployment.
wait_for_condition(
lambda: len(
get_metric_dictionaries(
"ray_serve_deployment_processing_latency_ms_sum",
timeseries=timeseries,
wait=False,
)
)
== 5,
timeout=40,
)
def wait_for_route_and_name(
_metric_name: str,
deployment_name: str,
app_name: str,
route: str,
timeout: float = 5,
):
"""Waits for app name and route to appear in deployment's metric."""
def check():
# Check replica qps & latency
(
qps_metrics_route,
qps_metrics_app_name,
) = self._generate_metrics_summary(
get_metric_dictionaries(_metric_name, timeseries=timeseries),
)
assert qps_metrics_app_name[deployment_name] == app_name
assert qps_metrics_route[deployment_name] == {route}
return True
wait_for_condition(check, timeout=timeout)
# Check replica qps & latency
wait_for_route_and_name(
"ray_serve_deployment_request_counter_total",
depl_name1,
app_name1,
app_name1,
)
wait_for_route_and_name(
"ray_serve_deployment_request_counter_total",
depl_name2,
app_name2,
app_name2,
)
wait_for_route_and_name(
"ray_serve_deployment_error_counter_total", depl_name3, app_name3, app_name3
)
# Check grpc proxy qps & latency
for metric_name in [
"ray_serve_num_grpc_requests_total",
"ray_serve_grpc_request_latency_ms_sum",
]:
metrics = [
sample.labels
for sample in fetch_prometheus_metric_timeseries(
["localhost:9999"],
timeseries,
timeout=PROMETHEUS_METRICS_TIMEOUT_S,
)[metric_name]
]
assert {metric["route"] for metric in metrics} == {
"app1",
"app2",
"app3",
}
for metric_name in [
"ray_serve_handle_request_counter_total",
"ray_serve_num_router_requests_total",
"ray_serve_deployment_processing_latency_ms_sum",
]:
metrics_route, metrics_app_name = self._generate_metrics_summary(
get_metric_dictionaries(metric_name, timeseries=timeseries),
)
msg = f"Incorrect metrics for {metric_name}"
assert metrics_route[depl_name1] == {"app1"}, msg
assert metrics_route[depl_name2] == {"app2"}, msg
assert metrics_route[depl_name3] == {"app3"}, msg
assert metrics_app_name[depl_name1] == "app1", msg
assert metrics_app_name[depl_name2] == "app2", msg
assert metrics_app_name[depl_name3] == "app3", msg
def test_request_context_pass_for_handle_passing(self, metrics_start_shutdown):
"""Test handle passing contexts between replicas"""
@serve.deployment
def g1():
return "ok1"
@serve.deployment
def g2():
return "ok2"
app = FastAPI()
@serve.deployment
@serve.ingress(app)
class G:
def __init__(self, handle1: DeploymentHandle, handle2: DeploymentHandle):
self.handle1 = handle1
self.handle2 = handle2
@app.get("/api")
async def app1(self):
return await self.handle1.remote()
@app.get("/api2")
async def app2(self):
return await self.handle2.remote()
serve.run(G.bind(g1.bind(), g2.bind()), name="app")
app_url = get_application_url("HTTP", "app")
resp = httpx.get(f"{app_url}/api")
assert resp.text == '"ok1"'
resp = httpx.get(f"{app_url}/api2")
assert resp.text == '"ok2"'
# G deployment metrics:
# {xxx, route:/api}, {xxx, route:/api2}
# g1 deployment metrics:
# {xxx, route:/api}
# g2 deployment metrics:
# {xxx, route:/api2}
timeseries = PrometheusTimeseries()
wait_for_condition(
lambda: len(
get_metric_dictionaries(
"ray_serve_deployment_request_counter_total",
timeseries=timeseries,
wait=False,
)
)
== 4,
timeout=40,
)
(
requests_metrics_route,
requests_metrics_app_name,
) = self._generate_metrics_summary(
get_metric_dictionaries(
"ray_serve_deployment_request_counter_total", timeseries=timeseries
),
)
assert requests_metrics_route["G"] == {"/api", "/api2"}
assert requests_metrics_route["g1"] == {"/api"}
assert requests_metrics_route["g2"] == {"/api2"}
assert requests_metrics_app_name["G"] == "app"
assert requests_metrics_app_name["g1"] == "app"
assert requests_metrics_app_name["g2"] == "app"
@pytest.mark.parametrize("route_prefix", ["", "/prefix"])
def test_fastapi_route_metrics(self, metrics_start_shutdown, route_prefix: str):
app = FastAPI()
@serve.deployment
@serve.ingress(app)
class A:
@app.get("/api")
def route1(self):
return "ok1"
@app.get("/api2/{user_id}")
def route2(self):
return "ok2"
if route_prefix:
serve.run(A.bind(), route_prefix=route_prefix)
else:
serve.run(A.bind())
base_url = get_application_url("HTTP")
resp = httpx.get(f"{base_url}/api")
assert resp.text == '"ok1"'
resp = httpx.get(f"{base_url}/api2/abc123")
assert resp.text == '"ok2"'
timeseries = PrometheusTimeseries()
wait_for_condition(
lambda: len(
get_metric_dictionaries(
"ray_serve_deployment_request_counter_total",
timeseries=timeseries,
wait=False,
)
)
== 2,
timeout=40,
)
(requests_metrics_route, _,) = self._generate_metrics_summary(
get_metric_dictionaries(
"ray_serve_deployment_request_counter_total", timeseries=timeseries
)
)
assert requests_metrics_route["A"] == {
route_prefix + "/api",
route_prefix + "/api2/{user_id}",
}
def test_customer_metrics_with_context(self, metrics_start_shutdown):
@serve.deployment
class Model:
def __init__(self):
self.counter = Counter(
"my_counter",
description="my counter metrics",
tag_keys=(
"my_static_tag",
"my_runtime_tag",
"route",
),
)
self.counter.set_default_tags({"my_static_tag": "static_value"})
self.histogram = Histogram(
"my_histogram",
description=("my histogram "),
boundaries=DEFAULT_LATENCY_BUCKET_MS,
tag_keys=(
"my_static_tag",
"my_runtime_tag",
"route",
),
)
self.histogram.set_default_tags({"my_static_tag": "static_value"})
self.gauge = Gauge(
"my_gauge",
description=("my_gauge"),
tag_keys=(
"my_static_tag",
"my_runtime_tag",
"route",
),
)
self.gauge.set_default_tags({"my_static_tag": "static_value"})
def __call__(self):
self.counter.inc(tags={"my_runtime_tag": "100"})
self.histogram.observe(200, tags={"my_runtime_tag": "200"})
self.gauge.set(300, tags={"my_runtime_tag": "300"})
return [
# NOTE(zcin): this is to match the current implementation in
# Serve's _add_serve_metric_default_tags().
ray.serve.context._INTERNAL_REPLICA_CONTEXT.deployment,
ray.serve.context._INTERNAL_REPLICA_CONTEXT.replica_id.unique_id,
]
timeseries = PrometheusTimeseries()
serve.run(Model.bind(), name="app", route_prefix="/app")
http_url = get_application_url("HTTP", "app")
resp = httpx.get(http_url)
deployment_name, replica_id = resp.json()
wait_for_condition(
lambda: len(
get_metric_dictionaries(
"ray_my_gauge", timeseries=timeseries, wait=False
),
)
== 1,
timeout=40,
)
counter_metrics = get_metric_dictionaries(
"ray_my_counter_total", timeseries=timeseries
)
assert len(counter_metrics) == 1
expected_metrics = {
"my_static_tag": "static_value",
"my_runtime_tag": "100",
"replica": replica_id,
"deployment": deployment_name,
"application": "app",
"route": "/app",
}
self.verify_metrics(counter_metrics[0], expected_metrics)
expected_metrics = {
"my_static_tag": "static_value",
"my_runtime_tag": "300",
"replica": replica_id,
"deployment": deployment_name,
"application": "app",
"route": "/app",
}
gauge_metrics = get_metric_dictionaries("ray_my_gauge", timeseries=timeseries)
assert len(gauge_metrics) == 1
self.verify_metrics(gauge_metrics[0], expected_metrics)
expected_metrics = {
"my_static_tag": "static_value",
"my_runtime_tag": "200",
"replica": replica_id,
"deployment": deployment_name,
"application": "app",
"route": "/app",
}
histogram_metrics = get_metric_dictionaries(
"ray_my_histogram_sum", timeseries=timeseries
)
assert len(histogram_metrics) == 1
self.verify_metrics(histogram_metrics[0], expected_metrics)
@pytest.mark.parametrize("use_actor", [False, True])
def test_serve_metrics_outside_serve(self, use_actor, metrics_start_shutdown):
"""Make sure ray.serve.metrics work in ray actor"""
if use_actor:
@ray.remote
class MyActor:
def __init__(self):
self.counter = Counter(
"my_counter",
description="my counter metrics",
tag_keys=(
"my_static_tag",
"my_runtime_tag",
),
)
self.counter.set_default_tags({"my_static_tag": "static_value"})
self.histogram = Histogram(
"my_histogram",
description=("my histogram "),
boundaries=DEFAULT_LATENCY_BUCKET_MS,
tag_keys=(
"my_static_tag",
"my_runtime_tag",
),
)
self.histogram.set_default_tags({"my_static_tag": "static_value"})
self.gauge = Gauge(
"my_gauge",
description=("my_gauge"),
tag_keys=(
"my_static_tag",
"my_runtime_tag",
),
)
self.gauge.set_default_tags({"my_static_tag": "static_value"})
def test(self):
self.counter.inc(tags={"my_runtime_tag": "100"})
self.histogram.observe(200, tags={"my_runtime_tag": "200"})
self.gauge.set(300, tags={"my_runtime_tag": "300"})
return "hello"
else:
counter = Counter(
"my_counter",
description="my counter metrics",
tag_keys=(
"my_static_tag",
"my_runtime_tag",
),
)
histogram = Histogram(
"my_histogram",
description=("my histogram "),
boundaries=DEFAULT_LATENCY_BUCKET_MS,
tag_keys=(
"my_static_tag",
"my_runtime_tag",
),
)
gauge = Gauge(
"my_gauge",
description=("my_gauge"),
tag_keys=(
"my_static_tag",
"my_runtime_tag",
),
)
@ray.remote
def fn():
counter.set_default_tags({"my_static_tag": "static_value"})
histogram.set_default_tags({"my_static_tag": "static_value"})
gauge.set_default_tags({"my_static_tag": "static_value"})
counter.inc(tags={"my_runtime_tag": "100"})
histogram.observe(200, tags={"my_runtime_tag": "200"})
gauge.set(300, tags={"my_runtime_tag": "300"})
return "hello"
@serve.deployment
class Model:
def __init__(self):
if use_actor:
self.my_actor = MyActor.remote()
async def __call__(self):
if use_actor:
return await self.my_actor.test.remote()
else:
return await fn.remote()
serve.run(Model.bind(), name="app", route_prefix="/app")
http_url = get_application_url("HTTP", "app")
resp = httpx.get(http_url)
assert resp.text == "hello"
timeseries = PrometheusTimeseries()
wait_for_condition(
lambda: len(
get_metric_dictionaries(
"ray_my_gauge", timeseries=timeseries, wait=False
),
)
== 1,
timeout=40,
)
counter_metrics = get_metric_dictionaries(
"ray_my_counter_total", timeseries=timeseries
)
assert len(counter_metrics) == 1
expected_metrics = {
"my_static_tag": "static_value",
"my_runtime_tag": "100",
}
self.verify_metrics(counter_metrics[0], expected_metrics)
gauge_metrics = get_metric_dictionaries("ray_my_gauge", timeseries=timeseries)
assert len(gauge_metrics) == 1
expected_metrics = {
"my_static_tag": "static_value",
"my_runtime_tag": "300",
}
self.verify_metrics(gauge_metrics[0], expected_metrics)
histogram_metrics = get_metric_dictionaries(
"ray_my_histogram_sum", timeseries=timeseries
)
assert len(histogram_metrics) == 1
expected_metrics = {
"my_static_tag": "static_value",
"my_runtime_tag": "200",
}
self.verify_metrics(histogram_metrics[0], expected_metrics)
class TestHandleMetrics:
def test_queued_queries_basic(self, metrics_start_shutdown):
signal = SignalActor.options(name="signal123").remote()
timeseries = PrometheusTimeseries()
serve.run(WaitForSignal.options(max_ongoing_requests=1).bind(), name="app1")
# First call should get assigned to a replica
# call.remote("WaitForSignal", "app1")
caller = CallActor.remote("WaitForSignal", "app1")
caller.call.remote()
for i in range(5):
# call.remote("WaitForSignal", "app1")
# c.call.remote()
caller.call.remote()
wait_for_condition(
check_sum_metric_eq,
metric_name="ray_serve_deployment_queued_queries",
tags={"application": "app1"},
expected=i + 1,
timeseries=timeseries,
)
# Release signal
ray.get(signal.send.remote())
wait_for_condition(
check_sum_metric_eq,
metric_name="ray_serve_deployment_queued_queries",
tags={"application": "app1", "deployment": "WaitForSignal"},
expected=0,
timeseries=timeseries,
)
def test_queued_queries_multiple_handles(self, metrics_start_shutdown):
signal = SignalActor.options(name="signal123").remote()
serve.run(WaitForSignal.options(max_ongoing_requests=1).bind(), name="app1")
# Send first request
call.remote("WaitForSignal", "app1")
wait_for_condition(
check_sum_metric_eq,
metric_name="ray_serve_deployment_queued_queries",
tags={"application": "app1", "deployment": "WaitForSignal"},
expected=0,
)
# Send second request (which should stay queued)
call.remote("WaitForSignal", "app1")
wait_for_condition(
check_sum_metric_eq,
metric_name="ray_serve_deployment_queued_queries",
tags={"application": "app1", "deployment": "WaitForSignal"},
expected=1,
)
# Send third request (which should stay queued)
call.remote("WaitForSignal", "app1")
wait_for_condition(
check_sum_metric_eq,
metric_name="ray_serve_deployment_queued_queries",
tags={"application": "app1", "deployment": "WaitForSignal"},
expected=2,
)
# Release signal
ray.get(signal.send.remote())
wait_for_condition(
check_sum_metric_eq,
metric_name="ray_serve_deployment_queued_queries",
tags={"application": "app1", "deployment": "WaitForSignal"},
expected=0,
)
def test_queued_queries_disconnected(self, metrics_start_shutdown):
"""Check that disconnected queued queries are tracked correctly."""
signal = SignalActor.remote()
@serve.deployment(
max_ongoing_requests=1,
)
async def hang_on_first_request():
await signal.wait.remote()
serve.run(hang_on_first_request.bind())
print("Deployed hang_on_first_request deployment.")
timeseries = PrometheusTimeseries()
wait_for_condition(
check_metric_float_eq,
timeout=15,
metric="ray_serve_num_scheduling_tasks",
# Router is eagerly created on HTTP proxy, so there are metrics emitted
# from proxy router
expected=0,
# TODO(zcin): this tag shouldn't be necessary, there shouldn't be a mix of
# metrics from new and old sessions.
expected_tags={
"SessionName": ray._private.worker.global_worker.node.session_name
},
timeseries=timeseries,
)
print("ray_serve_num_scheduling_tasks updated successfully.")
wait_for_condition(
check_metric_float_eq,
timeout=15,
metric="ray_serve_num_scheduling_tasks_in_backoff",
# Router is eagerly created on HTTP proxy, so there are metrics emitted
# from proxy router
expected=0,
# TODO(zcin): this tag shouldn't be necessary, there shouldn't be a mix of
# metrics from new and old sessions.
expected_tags={
"SessionName": ray._private.worker.global_worker.node.session_name
},
timeseries=timeseries,
)
print("serve_num_scheduling_tasks_in_backoff updated successfully.")
@ray.remote(num_cpus=0)
def do_request():
r = httpx.get("http://localhost:8000/", timeout=10)
r.raise_for_status()
return r
# Make a request to block the deployment from accepting other requests.
request_refs = [do_request.remote()]
wait_for_condition(
lambda: ray.get(signal.cur_num_waiters.remote()) == 1, timeout=10
)
print("First request is executing.")
wait_for_condition(
check_sum_metric_eq,
timeout=15,
metric_name="ray_serve_num_ongoing_http_requests",
expected=1,
timeseries=timeseries,
)
print("ray_serve_num_ongoing_http_requests updated successfully.")
num_queued_requests = 3
request_refs.extend([do_request.remote() for _ in range(num_queued_requests)])
print(f"{num_queued_requests} more requests now queued.")
# First request should be processing. All others should be queued.
wait_for_condition(
check_sum_metric_eq,
timeout=15,
metric_name="ray_serve_deployment_queued_queries",
expected=num_queued_requests,
timeseries=timeseries,
)
print("ray_serve_deployment_queued_queries updated successfully.")
wait_for_condition(
check_sum_metric_eq,
timeout=15,
metric_name="ray_serve_num_ongoing_http_requests",
expected=num_queued_requests + 1,
timeseries=timeseries,
)
print("ray_serve_num_ongoing_http_requests updated successfully.")
# There should be 2 scheduling tasks (which is the max, since
# 2 = 2 * 1 replica) that are attempting to schedule the hanging requests.
wait_for_condition(
check_sum_metric_eq,
timeout=15,
metric_name="ray_serve_num_scheduling_tasks",
expected=2,
timeseries=timeseries,
)
print("ray_serve_num_scheduling_tasks updated successfully.")
wait_for_condition(
check_sum_metric_eq,
timeout=15,
metric_name="ray_serve_num_scheduling_tasks_in_backoff",
expected=2,
timeseries=timeseries,
)
print("serve_num_scheduling_tasks_in_backoff updated successfully.")
# Disconnect all requests by cancelling the Ray tasks.
[ray.cancel(ref, force=True) for ref in request_refs]
timeseries.flush()
print("Cancelled all HTTP requests.")
wait_for_condition(
check_sum_metric_eq,
timeout=15,
metric_name="ray_serve_deployment_queued_queries",
expected=0,
timeseries=timeseries,
)
print("ray_serve_deployment_queued_queries updated successfully.")
# Task should get cancelled.
wait_for_condition(
check_sum_metric_eq,
timeout=15,
metric_name="ray_serve_num_ongoing_http_requests",
expected=0,
timeseries=timeseries,
)
print("ray_serve_num_ongoing_http_requests updated successfully.")
wait_for_condition(
check_sum_metric_eq,
timeout=15,
metric_name="ray_serve_num_scheduling_tasks",
expected=0,
timeseries=timeseries,
)
print("ray_serve_num_scheduling_tasks updated successfully.")
wait_for_condition(
check_sum_metric_eq,
timeout=15,
metric_name="ray_serve_num_scheduling_tasks_in_backoff",
expected=0,
timeseries=timeseries,
)
print("serve_num_scheduling_tasks_in_backoff updated successfully.")
# Unblock hanging request.
ray.get(signal.send.remote())
def test_running_requests_gauge(self, metrics_start_shutdown):
signal = SignalActor.options(name="signal123").remote()
serve.run(
Router.options(num_replicas=2, ray_actor_options={"num_cpus": 0}).bind(
[
WaitForSignal.options(
name="d1",
ray_actor_options={"num_cpus": 0},
max_ongoing_requests=2,
num_replicas=3,
).bind(),
WaitForSignal.options(
name="d2",
ray_actor_options={"num_cpus": 0},
max_ongoing_requests=2,
num_replicas=3,
).bind(),
],
),
name="app1",
)
requests_sent = {1: 0, 2: 0}
timeseries = PrometheusTimeseries()
for i in range(5):
index = random.choice([1, 2])
print(f"Sending request to d{index}")
call.remote("Router", "app1", index)
requests_sent[index] += 1
wait_for_condition(
check_sum_metric_eq,
metric_name="ray_serve_num_ongoing_requests_at_replicas",
tags={"application": "app1", "deployment": "d1"},
expected=requests_sent[1],
timeseries=timeseries,
)
wait_for_condition(
check_sum_metric_eq,
metric_name="ray_serve_num_ongoing_requests_at_replicas",
tags={"application": "app1", "deployment": "d2"},
expected=requests_sent[2],
timeseries=timeseries,
)
wait_for_condition(
check_sum_metric_eq,
metric_name="ray_serve_num_ongoing_requests_at_replicas",
tags={"application": "app1", "deployment": "Router"},
expected=i + 1,
timeseries=timeseries,
)
# Release signal, the number of running requests should drop to 0
ray.get(signal.send.remote())
wait_for_condition(
check_sum_metric_eq,
metric_name="ray_serve_num_ongoing_requests_at_replicas",
tags={"application": "app1"},
expected=0,
timeseries=timeseries,
)
class TestProxyStateMetrics:
def test_proxy_status_metric(self, metrics_start_shutdown):
"""Test that proxy status metric is reported correctly."""
@serve.deployment
def f():
return "hello"
serve.run(f.bind(), name="app")
timeseries = PrometheusTimeseries()
# Wait for the proxy to become healthy and metric to be reported
def check_proxy_status():
metrics = get_metric_dictionaries(
"ray_serve_proxy_status", timeseries=timeseries, wait=False
)
if not metrics:
return False
# Check that at least one proxy has the metric with expected tags
for metric in metrics:
if "node_id" in metric and "node_ip_address" in metric:
return True
return False
wait_for_condition(check_proxy_status, timeout=30)
# Verify the metric has the expected tags
metrics = get_metric_dictionaries(
"ray_serve_proxy_status", timeseries=timeseries
)
assert len(metrics) >= 1
for metric in metrics:
assert "node_id" in metric
assert "node_ip_address" in metric
wait_for_condition(
check_metric_float_eq,
metric="ray_serve_proxy_status",
expected=2,
timeseries=timeseries,
expected_tags={},
)
def test_proxy_shutdown_duration_metric(self, metrics_start_shutdown):
"""Test that proxy shutdown duration metric is recorded when proxy shuts down."""
@serve.deployment
def f():
return "hello"
serve.run(f.bind(), name="app")
timeseries = PrometheusTimeseries()
# Wait for the proxy to become healthy first (status=2 means HEALTHY)
wait_for_condition(
check_metric_float_eq,
metric="ray_serve_proxy_status",
expected=2,
timeseries=timeseries,
expected_tags={},
timeout=30,
)
# Shutdown serve, which will trigger proxy shutdown
serve.shutdown()
# Wait for the shutdown duration metric to be recorded
# The histogram metric will have _sum and _count suffixes
def check_shutdown_duration_metric_exists():
metrics = get_metric_dictionaries(
"ray_serve_proxy_shutdown_duration_ms_sum",
timeseries=timeseries,
wait=False,
)
if not metrics:
return False
# Check that the metric has expected tags
for metric in metrics:
if "node_id" in metric and "node_ip_address" in metric:
return True
return False
wait_for_condition(check_shutdown_duration_metric_exists, timeout=30)
# Verify the metric has the expected tags
metrics = get_metric_dictionaries(
"ray_serve_proxy_shutdown_duration_ms_sum",
timeseries=timeseries,
wait=False,
)
assert len(metrics) == 1
for metric in metrics:
assert "node_id" in metric
assert "node_ip_address" in metric
# Also verify _count metric exists
count_metrics = get_metric_dictionaries(
"ray_serve_proxy_shutdown_duration_ms_count",
timeseries=timeseries,
wait=False,
)
assert len(count_metrics) == 1
if __name__ == "__main__":
sys.exit(pytest.main(["-v", "-s", __file__]))