import hashlib import json import logging import time from typing import Any, Dict, Optional, Union import ray import ray.util.serialization_addons from ray.serve._private.common import DeploymentID from ray.serve._private.config import DeploymentConfig, ReplicaConfig from ray.serve._private.constants import ( RAY_SERVE_DIRECT_INGRESS_MIN_DRAINING_PERIOD_S, RAY_SERVE_DIRECT_INGRESS_SHUTDOWN_BUFFER_S, RAY_SERVE_ENABLE_DIRECT_INGRESS, SERVE_LOGGER_NAME, ) from ray.serve._private.deployment_info import DeploymentInfo from ray.serve.exceptions import RayServeException from ray.serve.schema import ServeApplicationSchema logger = logging.getLogger(SERVE_LOGGER_NAME) def get_deploy_args( name: str, replica_config: ReplicaConfig, ingress: bool = False, ingress_request_router: bool = False, deployment_config: Optional[Union[DeploymentConfig, Dict[str, Any]]] = None, version: Optional[str] = None, route_prefix: Optional[str] = None, serialized_autoscaling_policy_def: Optional[bytes] = None, serialized_request_router_cls: Optional[bytes] = None, serialized_deployment_actors: Optional[Dict[str, bytes]] = None, uses_multiplexing: bool = False, ) -> Dict: """ Takes a deployment's configuration, and returns the arguments needed for the controller to deploy it. """ if deployment_config is None: deployment_config = {} if isinstance(deployment_config, dict): deployment_config = DeploymentConfig.model_validate(deployment_config) elif not isinstance(deployment_config, DeploymentConfig): raise TypeError("config must be a DeploymentConfig or a dictionary.") deployment_config.version = version controller_deploy_args = { "deployment_name": name, "deployment_config_proto_bytes": deployment_config.to_proto_bytes(), "replica_config_proto_bytes": replica_config.to_proto_bytes(), "route_prefix": route_prefix, "deployer_job_id": ray.get_runtime_context().get_job_id(), "ingress": ingress, "ingress_request_router": ingress_request_router, "serialized_autoscaling_policy_def": serialized_autoscaling_policy_def, "serialized_request_router_cls": serialized_request_router_cls, "serialized_deployment_actors": serialized_deployment_actors, "uses_multiplexing": uses_multiplexing, } return controller_deploy_args def deploy_args_to_deployment_info( deployment_name: str, deployment_config_proto_bytes: bytes, replica_config_proto_bytes: bytes, deployer_job_id: Union[str, bytes], app_name: Optional[str] = None, ingress: bool = False, ingress_request_router: bool = False, route_prefix: Optional[str] = None, uses_multiplexing: bool = False, **kwargs, ) -> DeploymentInfo: """Takes deployment args passed to the controller after building an application and constructs a DeploymentInfo object. """ deployment_config = DeploymentConfig.from_proto_bytes(deployment_config_proto_bytes) if ingress and RAY_SERVE_ENABLE_DIRECT_INGRESS: # Model multiplexing relies on the multiplexed model ID being propagated through # the proxy, which direct ingress bypasses (the model ID is never populated). # Only the *statically* detectable case is caught here; dynamically-initialized # multiplexing is caught at replica initialization. if uses_multiplexing: raise RayServeException( f'Ingress deployment "{deployment_name}" in application "{app_name}" uses ' "model multiplexing (`@serve.multiplexed`), which is not supported on the " "ingress deployment when direct ingress or HAProxy is enabled." ) # Floor the timeout so the controller's force-kill can't cut the # direct-ingress drain (min draining period) short. floor_s = ( RAY_SERVE_DIRECT_INGRESS_MIN_DRAINING_PERIOD_S + RAY_SERVE_DIRECT_INGRESS_SHUTDOWN_BUFFER_S ) if deployment_config.graceful_shutdown_timeout_s < floor_s: logger.info( f"Raising graceful_shutdown_timeout_s for ingress deployment " f"'{deployment_name}' from " f"{deployment_config.graceful_shutdown_timeout_s}s to {floor_s}s so " f"the force-kill deadline covers the direct-ingress drain period." ) deployment_config.graceful_shutdown_timeout_s = floor_s version = deployment_config.version replica_config = ReplicaConfig.from_proto_bytes( replica_config_proto_bytes, deployment_config.needs_pickle() ) # Java API passes in JobID as bytes if isinstance(deployer_job_id, bytes): deployer_job_id = ray.JobID.from_int( int.from_bytes(deployer_job_id, "little") ).hex() return DeploymentInfo( actor_name=DeploymentID( name=deployment_name, app_name=app_name ).to_replica_actor_class_name(), version=version, deployment_config=deployment_config, replica_config=replica_config, deployer_job_id=deployer_job_id, start_time_ms=int(time.time() * 1000), route_prefix=route_prefix, ingress=ingress, ingress_request_router=ingress_request_router, ) def get_app_code_version(app_config: ServeApplicationSchema) -> str: """Returns the code version of an application. Args: app_config: The application config. Returns: str: A hash of the import path and (application level) runtime env representing the code version of the application. """ request_router_configs = [ deployment.request_router_config for deployment in app_config.deployments if isinstance(deployment.request_router_config, dict) ] deployment_autoscaling_policies = [ deployment_config.autoscaling_config.get("policy", None) for deployment_config in app_config.deployments if isinstance(deployment_config.autoscaling_config, dict) ] deployment_actors_configs = [ deployment.deployment_actors for deployment in app_config.deployments if isinstance(deployment.deployment_actors, list) ] encoded = json.dumps( { "import_path": app_config.import_path, "runtime_env": app_config.runtime_env, "args": app_config.args, # NOTE: trigger a change in the code version when # application level autoscaling policy is changed or # any one of the deployment level autoscaling policy is changed "autoscaling_policy": app_config.autoscaling_policy, "deployment_autoscaling_policies": deployment_autoscaling_policies, "request_router_configs": request_router_configs, "deployment_actors": deployment_actors_configs, }, sort_keys=True, ).encode("utf-8") return hashlib.sha256(encoded).hexdigest()