chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,475 @@
|
||||
"""
|
||||
This file stores global state for a Serve application. Deployment replicas
|
||||
can use this state to access metadata or the Serve controller.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextvars
|
||||
import logging
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Dict, List, Optional
|
||||
|
||||
import ray
|
||||
from ray.serve._private.client import ServeControllerClient
|
||||
from ray.serve._private.common import DeploymentID, ReplicaID
|
||||
from ray.serve._private.config import DeploymentConfig
|
||||
from ray.serve._private.constants import (
|
||||
RAY_SERVE_INTERNAL_DEPLOYMENT_ACTOR_NAME_ENV_VAR,
|
||||
RAY_SERVE_INTERNAL_DEPLOYMENT_APP_NAME_ENV_VAR,
|
||||
RAY_SERVE_INTERNAL_DEPLOYMENT_CODE_VERSION_ENV_VAR,
|
||||
RAY_SERVE_INTERNAL_DEPLOYMENT_NAME_ENV_VAR,
|
||||
SERVE_CONTROLLER_NAME,
|
||||
SERVE_LOGGER_NAME,
|
||||
SERVE_NAMESPACE,
|
||||
)
|
||||
from ray.serve._private.replica_result import ReplicaResult
|
||||
from ray.serve._private.utils import get_deployment_actor_name
|
||||
from ray.serve.exceptions import RayServeException
|
||||
from ray.serve.gang import GangContext
|
||||
from ray.serve.grpc_util import RayServegRPCContext
|
||||
from ray.serve.schema import ReplicaRank
|
||||
from ray.util.annotations import DeveloperAPI
|
||||
|
||||
logger = logging.getLogger(SERVE_LOGGER_NAME)
|
||||
|
||||
_INTERNAL_REPLICA_CONTEXT: "ReplicaContext" = None
|
||||
_INTERNAL_DEPLOYMENT_ACTOR_CONTEXT: "DeploymentActorContext" = None
|
||||
_global_client: ServeControllerClient = None
|
||||
# Ray job id (driver session) that created the cached client above. ray.shutdown()
|
||||
# leaves this module global intact, so after a reconnect (new job id) the cached
|
||||
# handle is unusable and must not be reused. See #64647.
|
||||
_global_client_job_id: Optional[str] = None
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
@dataclass
|
||||
class ReplicaContext:
|
||||
"""Stores runtime context info for replicas.
|
||||
|
||||
Fields:
|
||||
- app_name: name of the application the replica is a part of.
|
||||
- deployment: name of the deployment the replica is a part of.
|
||||
- replica_tag: unique ID for the replica.
|
||||
- servable_object: instance of the user class/function this replica is running.
|
||||
- rank: the rank of the replica.
|
||||
- world_size: the number of replicas in the deployment.
|
||||
- gang_context: context information for the gang the replica is part of.
|
||||
- code_version: code version of the deployment (for get_deployment_actor).
|
||||
"""
|
||||
|
||||
replica_id: ReplicaID
|
||||
servable_object: Callable
|
||||
_deployment_config: DeploymentConfig
|
||||
rank: ReplicaRank
|
||||
world_size: int
|
||||
_handle_registration_callback: Optional[Callable[[DeploymentID], None]] = None
|
||||
gang_context: Optional[GangContext] = None
|
||||
code_version: Optional[str] = None
|
||||
|
||||
@property
|
||||
def app_name(self) -> str:
|
||||
return self.replica_id.deployment_id.app_name
|
||||
|
||||
@property
|
||||
def deployment(self) -> str:
|
||||
return self.replica_id.deployment_id.name
|
||||
|
||||
@property
|
||||
def replica_tag(self) -> str:
|
||||
return self.replica_id.unique_id
|
||||
|
||||
|
||||
@DeveloperAPI
|
||||
@dataclass
|
||||
class DeploymentActorContext:
|
||||
"""Stores runtime context info for deployment-scoped actors."""
|
||||
|
||||
deployment_id: DeploymentID
|
||||
actor_name: str
|
||||
code_version: Optional[str] = None
|
||||
|
||||
@property
|
||||
def app_name(self) -> str:
|
||||
return self.deployment_id.app_name
|
||||
|
||||
@property
|
||||
def deployment(self) -> str:
|
||||
return self.deployment_id.name
|
||||
|
||||
|
||||
def _get_global_client(
|
||||
raise_if_no_controller_running: bool = True,
|
||||
) -> Optional[ServeControllerClient]:
|
||||
"""Gets the global client, which stores the controller's handle.
|
||||
|
||||
Args:
|
||||
raise_if_no_controller_running: Whether to raise an exception if
|
||||
there is no currently running Serve controller.
|
||||
|
||||
Returns:
|
||||
ServeControllerClient to the running Serve controller. If there
|
||||
is no running controller and raise_if_no_controller_running is
|
||||
set to False, returns None.
|
||||
|
||||
Raises:
|
||||
RayServeException: If there is no running Serve controller actor
|
||||
and raise_if_no_controller_running is set to True.
|
||||
"""
|
||||
|
||||
if _cached_client_from_current_session():
|
||||
return _global_client
|
||||
|
||||
# No usable cache (never set, or left over from a previous driver session).
|
||||
# Drop any stale handle and rediscover the controller by name.
|
||||
_disconnect()
|
||||
return _connect(raise_if_no_controller_running)
|
||||
|
||||
|
||||
def _check_cached_client_alive() -> tuple:
|
||||
"""Health-check the cached controller client.
|
||||
|
||||
Returns:
|
||||
(client, had_cached) tuple.
|
||||
- ``(client, True)``: cached client from the current session is alive.
|
||||
- ``(None, True)``: cached client from the current session existed but
|
||||
is unreachable; the cache has been cleared. Callers should **not**
|
||||
attempt to reconnect via ``_connect()`` because GCS is likely dead and
|
||||
``ray.get_actor()`` would hang until the 60-second C++ GCS
|
||||
reconnection timeout kills the process.
|
||||
- ``(None, False)``: no usable cached client (never set, or left over
|
||||
from a previous driver session). Callers may safely call
|
||||
``_get_global_client()`` to discover a running controller.
|
||||
"""
|
||||
|
||||
if not _cached_client_from_current_session():
|
||||
# Either nothing cached, or a handle left behind by a previous driver
|
||||
# session. In the latter case GCS is alive (we are connected now), so it
|
||||
# is safe for callers to reconnect. Drop any stale handle.
|
||||
_disconnect()
|
||||
return None, False
|
||||
|
||||
try:
|
||||
ray.get(_global_client._controller.check_alive.remote(), timeout=5)
|
||||
return _global_client, True
|
||||
except Exception as e:
|
||||
logger.info(f"The cached controller has died or is unreachable: {e}.")
|
||||
_disconnect()
|
||||
return None, True
|
||||
|
||||
|
||||
def _set_global_client(client):
|
||||
global _global_client, _global_client_job_id
|
||||
_global_client = client
|
||||
_global_client_job_id = (
|
||||
ray.get_runtime_context().get_job_id() if client is not None else None
|
||||
)
|
||||
|
||||
|
||||
def _disconnect():
|
||||
"""Forget the cached controller client for this driver session.
|
||||
|
||||
Mirrors ``_connect()``. This does **not** shut Serve down on the cluster; it
|
||||
only drops the local cached handle so the next call rediscovers the
|
||||
controller by name.
|
||||
"""
|
||||
_set_global_client(None)
|
||||
|
||||
|
||||
def _cached_client_from_current_session() -> bool:
|
||||
"""Whether a usable cached client exists for the current Ray session.
|
||||
|
||||
``_global_client`` is a module global that survives ``ray.shutdown()``, so a
|
||||
handle cached by a previous driver session cannot be used after the driver
|
||||
reconnects (which yields a new job id). Comparing job ids detects that case.
|
||||
"""
|
||||
if _global_client is None or not ray.is_initialized():
|
||||
return False
|
||||
return _global_client_job_id == ray.get_runtime_context().get_job_id()
|
||||
|
||||
|
||||
def _get_internal_replica_context():
|
||||
return _INTERNAL_REPLICA_CONTEXT
|
||||
|
||||
|
||||
def _get_internal_deployment_actor_context():
|
||||
global _INTERNAL_DEPLOYMENT_ACTOR_CONTEXT
|
||||
|
||||
if _INTERNAL_DEPLOYMENT_ACTOR_CONTEXT is not None:
|
||||
return _INTERNAL_DEPLOYMENT_ACTOR_CONTEXT
|
||||
|
||||
app_name = os.environ.get(RAY_SERVE_INTERNAL_DEPLOYMENT_APP_NAME_ENV_VAR)
|
||||
deployment_name = os.environ.get(RAY_SERVE_INTERNAL_DEPLOYMENT_NAME_ENV_VAR)
|
||||
actor_name = os.environ.get(RAY_SERVE_INTERNAL_DEPLOYMENT_ACTOR_NAME_ENV_VAR)
|
||||
|
||||
if app_name is None or deployment_name is None or actor_name is None:
|
||||
return None
|
||||
|
||||
_INTERNAL_DEPLOYMENT_ACTOR_CONTEXT = DeploymentActorContext(
|
||||
deployment_id=DeploymentID(name=deployment_name, app_name=app_name),
|
||||
actor_name=actor_name,
|
||||
code_version=os.environ.get(RAY_SERVE_INTERNAL_DEPLOYMENT_CODE_VERSION_ENV_VAR),
|
||||
)
|
||||
return _INTERNAL_DEPLOYMENT_ACTOR_CONTEXT
|
||||
|
||||
|
||||
def _get_deployment_actor(actor_name: str):
|
||||
"""Get a handle to a deployment-scoped actor by name.
|
||||
|
||||
Thin wrapper around ``ray.get_actor`` with the Serve deployment-actor naming
|
||||
convention. See ``serve.get_deployment_actor`` docstring for behavior,
|
||||
``ValueError``/``RayActorError`` expectations, and refresh patterns.
|
||||
"""
|
||||
internal_context = _get_internal_replica_context()
|
||||
if internal_context is None:
|
||||
raise RayServeException(
|
||||
"`serve.get_deployment_actor()` may only be called from within "
|
||||
"a Ray Serve deployment replica."
|
||||
)
|
||||
deployment_id = internal_context.replica_id.deployment_id
|
||||
return ray.get_actor(
|
||||
get_deployment_actor_name(
|
||||
deployment_id,
|
||||
actor_name,
|
||||
code_version=internal_context.code_version,
|
||||
),
|
||||
namespace=SERVE_NAMESPACE,
|
||||
)
|
||||
|
||||
|
||||
def _set_internal_replica_context(
|
||||
*,
|
||||
replica_id: ReplicaID,
|
||||
servable_object: Callable,
|
||||
_deployment_config: DeploymentConfig,
|
||||
rank: ReplicaRank,
|
||||
world_size: int,
|
||||
handle_registration_callback: Optional[Callable[[str, str], None]] = None,
|
||||
gang_context: Optional[GangContext] = None,
|
||||
code_version: Optional[str] = None,
|
||||
):
|
||||
global _INTERNAL_REPLICA_CONTEXT
|
||||
_INTERNAL_REPLICA_CONTEXT = ReplicaContext(
|
||||
replica_id=replica_id,
|
||||
servable_object=servable_object,
|
||||
_deployment_config=_deployment_config,
|
||||
rank=rank,
|
||||
world_size=world_size,
|
||||
_handle_registration_callback=handle_registration_callback,
|
||||
gang_context=gang_context,
|
||||
code_version=code_version,
|
||||
)
|
||||
|
||||
|
||||
def _connect(raise_if_no_controller_running: bool = True) -> ServeControllerClient:
|
||||
"""Connect to an existing Serve application on this Ray cluster.
|
||||
|
||||
If called from within a replica, this will connect to the same Serve
|
||||
app that the replica is running in.
|
||||
|
||||
Args:
|
||||
raise_if_no_controller_running: If ``True``, raise when no Serve
|
||||
controller actor is found. If ``False``, return ``None`` instead.
|
||||
|
||||
Returns:
|
||||
ServeControllerClient that encapsulates a Ray actor handle to the
|
||||
existing Serve application's Serve Controller. None if there is
|
||||
no running Serve controller actor and raise_if_no_controller_running
|
||||
is set to False.
|
||||
|
||||
Raises:
|
||||
RayServeException: If there is no running Serve controller actor
|
||||
and raise_if_no_controller_running is set to True.
|
||||
"""
|
||||
|
||||
# Initialize ray if needed.
|
||||
ray._private.worker.global_worker._filter_logs_by_job = False
|
||||
if not ray.is_initialized():
|
||||
ray.init(namespace=SERVE_NAMESPACE)
|
||||
|
||||
# Try to get serve controller if it exists
|
||||
try:
|
||||
controller = ray.get_actor(SERVE_CONTROLLER_NAME, namespace=SERVE_NAMESPACE)
|
||||
except ValueError:
|
||||
if raise_if_no_controller_running:
|
||||
raise RayServeException(
|
||||
"There is no Serve instance running on this Ray cluster."
|
||||
)
|
||||
return
|
||||
|
||||
client = ServeControllerClient(
|
||||
controller,
|
||||
)
|
||||
_set_global_client(client)
|
||||
return client
|
||||
|
||||
|
||||
# Serve request context var which is used for storing the internal
|
||||
# request context information.
|
||||
# route_prefix: http url route path, e.g. http://127.0.0.1:/app
|
||||
# the route is "/app". When you send requests by handle,
|
||||
# the route is empty.
|
||||
# request_id: the request id is generated from http proxy, the value
|
||||
# shouldn't be changed when the variable is set.
|
||||
# This can be from the client and is used for logging.
|
||||
# _internal_request_id: the request id is generated from the proxy. Used to track the
|
||||
# request objects in the system.
|
||||
# note:
|
||||
# The request context is readonly to avoid potential
|
||||
# async task conflicts when using it concurrently.
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _RequestContext:
|
||||
route: str = ""
|
||||
request_id: str = ""
|
||||
_internal_request_id: str = ""
|
||||
app_name: str = ""
|
||||
multiplexed_model_id: str = ""
|
||||
session_id: str = ""
|
||||
grpc_context: Optional[RayServegRPCContext] = None
|
||||
is_http_request: bool = False
|
||||
cancel_on_parent_request_cancel: bool = False
|
||||
# The client address in "host:port" format, if available.
|
||||
_client: str = ""
|
||||
# Ray tracing context for this request (if tracing is enabled)
|
||||
# This is extracted from _ray_trace_ctx kwarg at the replica entry point
|
||||
# Advanced users can access this to propagate tracing to external systems
|
||||
_ray_trace_ctx: Optional[dict] = None
|
||||
|
||||
|
||||
_serve_request_context = contextvars.ContextVar(
|
||||
"Serve internal request context variable", default=None
|
||||
)
|
||||
|
||||
_serve_batch_request_context = contextvars.ContextVar(
|
||||
"Serve internal batching request context variable", default=None
|
||||
)
|
||||
|
||||
|
||||
def _get_serve_request_context():
|
||||
"""Get the current request context.
|
||||
|
||||
Returns:
|
||||
The current request context
|
||||
"""
|
||||
|
||||
if _serve_request_context.get() is None:
|
||||
_serve_request_context.set(_RequestContext())
|
||||
return _serve_request_context.get()
|
||||
|
||||
|
||||
def _get_serve_batch_request_context():
|
||||
"""Get the list of request contexts for the current batch."""
|
||||
if _serve_batch_request_context.get() is None:
|
||||
_serve_batch_request_context.set([])
|
||||
return _serve_batch_request_context.get()
|
||||
|
||||
|
||||
def _set_request_context(
|
||||
route: str = "",
|
||||
request_id: str = "",
|
||||
_internal_request_id: str = "",
|
||||
app_name: str = "",
|
||||
multiplexed_model_id: str = "",
|
||||
):
|
||||
"""Set the request context. If the value is not set,
|
||||
the current context value will be used."""
|
||||
|
||||
current_request_context = _get_serve_request_context()
|
||||
|
||||
_serve_request_context.set(
|
||||
_RequestContext(
|
||||
route=route or current_request_context.route,
|
||||
request_id=request_id or current_request_context.request_id,
|
||||
_internal_request_id=_internal_request_id
|
||||
or current_request_context._internal_request_id,
|
||||
app_name=app_name or current_request_context.app_name,
|
||||
multiplexed_model_id=multiplexed_model_id
|
||||
or current_request_context.multiplexed_model_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _unset_request_context():
|
||||
"""Unset the request context."""
|
||||
_serve_request_context.set(_RequestContext())
|
||||
|
||||
|
||||
def _set_batch_request_context(request_contexts: List[_RequestContext]):
|
||||
"""Add the request context to the batch request context."""
|
||||
_serve_batch_request_context.set(request_contexts)
|
||||
|
||||
|
||||
# `_requests_pending_assignment` is a map from request ID to a
|
||||
# dictionary of asyncio tasks.
|
||||
# The request ID points to an ongoing request that is executing on the
|
||||
# current replica, and the asyncio tasks are ongoing tasks started on
|
||||
# the router to assign child requests to downstream replicas.
|
||||
|
||||
# A dictionary is used over a set to track the asyncio tasks for more
|
||||
# efficient addition and deletion time complexity. A uniquely generated
|
||||
# `response_id` is used to identify each task.
|
||||
|
||||
_requests_pending_assignment: Dict[str, Dict[str, asyncio.Task]] = defaultdict(dict)
|
||||
|
||||
|
||||
# Note that the functions below that manipulate
|
||||
# `_requests_pending_assignment` are NOT thread-safe. They are only
|
||||
# expected to be called from the same thread/asyncio event-loop.
|
||||
|
||||
|
||||
def _get_requests_pending_assignment(parent_request_id: str) -> Dict[str, asyncio.Task]:
|
||||
if parent_request_id in _requests_pending_assignment:
|
||||
return _requests_pending_assignment[parent_request_id]
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _add_request_pending_assignment(parent_request_id: str, response_id: str, task):
|
||||
# NOTE: `parent_request_id` is the `internal_request_id` corresponding
|
||||
# to an ongoing Serve request, so it is always non-empty.
|
||||
_requests_pending_assignment[parent_request_id][response_id] = task
|
||||
|
||||
|
||||
def _remove_request_pending_assignment(parent_request_id: str, response_id: str):
|
||||
if response_id in _requests_pending_assignment[parent_request_id]:
|
||||
del _requests_pending_assignment[parent_request_id][response_id]
|
||||
|
||||
if len(_requests_pending_assignment[parent_request_id]) == 0:
|
||||
del _requests_pending_assignment[parent_request_id]
|
||||
|
||||
|
||||
# `_in_flight_requests` is a map from request ID to a dictionary of replica results.
|
||||
# The request ID points to an ongoing Serve request, and the replica results are
|
||||
# in-flight child requests that have been assigned to a downstream replica.
|
||||
|
||||
# A dictionary is used over a set to track the replica results for more
|
||||
# efficient addition and deletion time complexity. A uniquely generated
|
||||
# `response_id` is used to identify each replica result.
|
||||
|
||||
_in_flight_requests: Dict[str, Dict[str, ReplicaResult]] = defaultdict(dict)
|
||||
|
||||
# Note that the functions below that manipulate `_in_flight_requests`
|
||||
# are NOT thread-safe. They are only expected to be called from the
|
||||
# same thread/asyncio event-loop.
|
||||
|
||||
|
||||
def _get_in_flight_requests(parent_request_id):
|
||||
if parent_request_id in _in_flight_requests:
|
||||
return _in_flight_requests[parent_request_id]
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _add_in_flight_request(parent_request_id, response_id, replica_result):
|
||||
_in_flight_requests[parent_request_id][response_id] = replica_result
|
||||
|
||||
|
||||
def _remove_in_flight_request(parent_request_id, response_id):
|
||||
if response_id in _in_flight_requests[parent_request_id]:
|
||||
del _in_flight_requests[parent_request_id][response_id]
|
||||
|
||||
if len(_in_flight_requests[parent_request_id]) == 0:
|
||||
del _in_flight_requests[parent_request_id]
|
||||
Reference in New Issue
Block a user