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

303 lines
9.4 KiB
Python

import logging
import pickle
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from typing import Any, AsyncIterator, List, Optional, Tuple, Union
import grpc
from starlette.types import Receive, Scope, Send
from ray.serve._private.common import StreamingHTTPRequest, gRPCRequest
from ray.serve._private.constants import SERVE_LOGGER_NAME
from ray.serve._private.logging_utils import format_grpc_peer_address
from ray.serve._private.tracing_utils import (
extract_propagated_context,
is_tracing_enabled,
set_trace_context,
)
from ray.serve._private.utils import DEFAULT
from ray.serve.grpc_util import RayServegRPCContext
logger = logging.getLogger(SERVE_LOGGER_NAME)
class gRPCStreamingType(str, Enum):
"""Enum representing the gRPC streaming type."""
UNARY_UNARY = "unary_unary" # Single request, single response
UNARY_STREAM = (
"unary_stream" # Single request, streaming response (server streaming)
)
STREAM_UNARY = (
"stream_unary" # Streaming request, single response (client streaming)
)
STREAM_STREAM = (
"stream_stream" # Streaming request, streaming response (bidirectional)
)
class ProxyRequest(ABC):
"""Base ProxyRequest class to use in the common interface among proxies"""
@property
@abstractmethod
def request_type(self) -> str:
raise NotImplementedError
@property
@abstractmethod
def method(self) -> str:
raise NotImplementedError
@property
@abstractmethod
def route_path(self) -> str:
raise NotImplementedError
@property
@abstractmethod
def is_route_request(self) -> bool:
raise NotImplementedError
@property
@abstractmethod
def is_health_request(self) -> bool:
raise NotImplementedError
@property
def client(self) -> str:
return ""
@abstractmethod
def populate_tracing_context(self):
"""Implement this method to populate tracing context so the parent and
child spans will be connected into a single trace."""
raise NotImplementedError
class ASGIProxyRequest(ProxyRequest):
"""ProxyRequest implementation to wrap ASGI scope, receive, and send."""
def __init__(self, scope: Scope, receive: Receive, send: Send):
self.scope = scope
self.receive = receive
self.send = send
@property
def request_type(self) -> str:
return self.scope.get("type", "")
@property
def method(self) -> str:
# WebSocket messages don't have a 'method' field.
return self.scope.get("method", "WS").upper()
@property
def route_path(self) -> str:
return self.scope.get("path", "")[len(self.root_path) :]
@property
def is_route_request(self) -> bool:
return self.route_path == "/-/routes"
@property
def is_health_request(self) -> bool:
return self.route_path == "/-/healthz"
@property
def client(self) -> str:
return self.scope.get("client", "")
@property
def root_path(self) -> str:
return self.scope.get("root_path", "")
@property
def path(self) -> str:
return self.scope.get("path", "")
@property
def headers(self) -> List[Tuple[bytes, bytes]]:
return self.scope.get("headers", [])
def set_path(self, path: str):
self.scope["path"] = path
def set_root_path(self, root_path: str):
self.scope["root_path"] = root_path
def serialized_replica_arg(self, proxy_actor_name: str) -> bytes:
# NOTE(edoakes): it's important that the request is sent as raw bytes to
# skip the Ray cloudpickle serialization codepath for performance.
return pickle.dumps(
StreamingHTTPRequest(
asgi_scope=self.scope,
proxy_actor_name=proxy_actor_name,
)
)
def populate_tracing_context(self):
"""Populate tracing context for ASGI requests.
This method extracts the "traceparent" header from the request headers and sets
the tracing context from it.
"""
if not is_tracing_enabled():
return
for key, value in self.headers:
if key.decode() == "traceparent":
trace_context = extract_propagated_context(
{key.decode(): value.decode()}
)
set_trace_context(trace_context)
class gRPCProxyRequest(ProxyRequest):
"""ProxyRequest implementation to wrap gRPC request protobuf and metadata."""
def __init__(
self,
request_proto: Any,
context: grpc._cython.cygrpc._ServicerContext,
service_method: str,
stream: bool,
*,
streaming_type: gRPCStreamingType = None,
request_iterator: Optional[AsyncIterator[Any]] = None,
):
self._request_proto = request_proto
self._request_iterator = request_iterator
self.context = context
self.service_method = service_method
self.stream = stream
# Determine streaming type based on parameters
if streaming_type is not None:
self.streaming_type = streaming_type
elif request_iterator is not None:
# Has input stream
self.streaming_type = (
gRPCStreamingType.STREAM_STREAM
if stream
else gRPCStreamingType.STREAM_UNARY
)
else:
# No input stream
self.streaming_type = (
gRPCStreamingType.UNARY_STREAM
if stream
else gRPCStreamingType.UNARY_UNARY
)
self.app_name = ""
self.request_id = None
self.method_name = "__call__"
self.multiplexed_model_id = DEFAULT.VALUE
self.session_id = DEFAULT.VALUE
# ray_serve_grpc_context is a class implemented by us to be able to serialize
# the object and pass it into the deployment.
self.ray_serve_grpc_context = RayServegRPCContext(context)
self.setup_variables()
def setup_variables(self):
if not self.is_route_request and not self.is_health_request:
service_method_split = self.service_method.split("/")
self.method_name = service_method_split[-1]
for key, value in self.context.invocation_metadata():
if key == "application":
self.app_name = value
elif key == "request_id":
self.request_id = value
elif key == "multiplexed_model_id":
self.multiplexed_model_id = value
elif key == "session_id":
self.session_id = value
@property
def request_type(self) -> str:
return "grpc"
@property
def method(self) -> str:
return self.service_method
@property
def route_path(self) -> str:
return self.app_name
@property
def is_route_request(self) -> bool:
return self.service_method == "/ray.serve.RayServeAPIService/ListApplications"
@property
def is_health_request(self) -> bool:
return self.service_method == "/ray.serve.RayServeAPIService/Healthz"
@property
def client(self) -> str:
return format_grpc_peer_address(self.context.peer())
@property
def has_input_stream(self) -> bool:
"""Returns True if this request has a streaming input (client/bidi streaming)."""
return self.streaming_type in (
gRPCStreamingType.STREAM_UNARY,
gRPCStreamingType.STREAM_STREAM,
)
@property
def request_iterator(self) -> Optional[AsyncIterator[Any]]:
"""Returns the request iterator for client/bidi streaming, or None."""
return self._request_iterator
def send_request_id(self, request_id: str):
# Setting the trailing metadata on the ray_serve_grpc_context object, so it's
# not overriding the ones set from the user and will be sent back to the
# client altogether.
self.ray_serve_grpc_context.set_trailing_metadata([("request_id", request_id)])
def serialized_replica_arg(self) -> bytes:
# NOTE(edoakes): it's important that the request is sent as raw bytes to
# skip the Ray cloudpickle serialization codepath for performance.
return pickle.dumps(gRPCRequest(user_request_proto=self._request_proto))
def populate_tracing_context(self):
"""Populate tracing context for gRPC requests.
This method extracts the "traceparent" metadata from the request headers and
sets the tracing context from it.
"""
if not is_tracing_enabled():
return
for key, value in self.context.invocation_metadata():
if key == "traceparent":
trace_context = extract_propagated_context({key: value})
set_trace_context(trace_context)
@dataclass(frozen=True)
class ResponseStatus:
code: Union[str, grpc.StatusCode] # Must be convertible to a string.
is_error: bool = False
message: str = ""
# Yields protocol-specific messages followed by a final `ResponseStatus`.
ResponseGenerator = AsyncIterator[Union[Any, ResponseStatus]]
@dataclass(frozen=True)
class HandlerMetadata:
application_name: str = ""
deployment_name: str = ""
route: str = ""
@dataclass(frozen=True)
class ResponseHandlerInfo:
response_generator: ResponseGenerator
metadata: HandlerMetadata
should_record_access_log: bool
should_increment_ongoing_requests: bool