import pickle from typing import Callable from unittest.mock import Mock import grpc import pytest from google.protobuf.any_pb2 import Any as AnyProto from ray import cloudpickle from ray.serve._private.default_impl import add_grpc_address from ray.serve._private.grpc_util import ( GRPC_MAX_STATUS_DETAILS_LENGTH, _truncate_message, get_grpc_response_status, gRPCGenericServer, ) from ray.serve._private.proxy_request_response import gRPCStreamingType from ray.serve._private.test_utils import FakeGrpcContext from ray.serve.exceptions import BackPressureError, gRPCStatusError from ray.serve.grpc_util import RayServegRPCContext class FakeGrpcServer: def __init__(self): self.address = None def add_insecure_port(self, address): self.address = address def fake_service_handler_factory( service_method: str, streaming_type: gRPCStreamingType ) -> Callable: def foo() -> bytes: return f"{streaming_type.value} call from {service_method}".encode() return foo def test_grpc_server(): """Test `gRPCGenericServer` did the correct overrides. When a add_servicer_to_server function is called on an instance of `gRPCGenericServer`, it correctly overrides `response_serializer` to None, and `unary_unary`, `unary_stream`, `stream_unary`, and `stream_stream` to be generated from the factory function. """ service_name = "ray.serve.ServeAPIService" method_name = "ServeRoutes" def add_test_servicer_to_server(servicer, server): rpc_method_handlers = { method_name: grpc.unary_unary_rpc_method_handler( servicer.ServeRoutes, request_deserializer=AnyProto.FromString, response_serializer=AnyProto.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( service_name, rpc_method_handlers ) server.add_generic_rpc_handlers((generic_handler,)) grpc_server = gRPCGenericServer(fake_service_handler_factory) dummy_servicer = Mock() # Ensure `generic_rpc_handlers` is not populated before calling # the add_servicer_to_server function. assert grpc_server.generic_rpc_handlers == [] add_test_servicer_to_server(dummy_servicer, grpc_server) # `generic_rpc_handlers` should be populated after add_servicer_to_server is called. assert len(grpc_server.generic_rpc_handlers) == 1 # The populated rpc handler should have the correct service name. rpc_handler = grpc_server.generic_rpc_handlers[0][0] assert rpc_handler.service_name() == service_name # The populated method handlers should have the correct response_serializer, # unary_unary, unary_stream, stream_unary, and stream_stream. service_method = f"/{service_name}/{method_name}" method_handlers = rpc_handler._method_handlers.get(service_method) assert method_handlers.response_serializer is None assert ( method_handlers.unary_unary() == f"unary_unary call from {service_method}".encode() ) assert ( method_handlers.unary_stream() == f"unary_stream call from {service_method}".encode() ) assert ( method_handlers.stream_unary() == f"stream_unary call from {service_method}".encode() ) assert ( method_handlers.stream_stream() == f"stream_stream call from {service_method}".encode() ) def test_ray_serve_grpc_context_serializable(): """RayServegRPCContext should be serializable.""" context = RayServegRPCContext(FakeGrpcContext()) pickled_context = pickle.dumps(context) deserialized_context = pickle.loads(pickled_context) assert deserialized_context.__dict__ == context.__dict__ cloudpickled_context = cloudpickle.dumps(context) deserialized_context = pickle.loads(cloudpickled_context) assert deserialized_context.__dict__ == context.__dict__ def test_add_grpc_address(): """Test `add_grpc_address` adds the address to the gRPC server.""" fake_grpc_server = FakeGrpcServer() grpc_address = "fake_address:50051" assert fake_grpc_server.address is None add_grpc_address(fake_grpc_server, grpc_address) assert fake_grpc_server.address == grpc_address def test_get_grpc_response_status_backpressure_error(): """Test that BackPressureError returns RESOURCE_EXHAUSTED status.""" backpressure_error = BackPressureError( num_queued_requests=10, max_queued_requests=5 ) status = get_grpc_response_status( exc=backpressure_error, request_timeout_s=30.0, request_id="test_request_123" ) assert status.code == grpc.StatusCode.RESOURCE_EXHAUSTED assert status.is_error is True assert status.message == backpressure_error.message def test_get_grpc_response_status_grpc_status_error(): """Test that gRPCStatusError preserves user-set status code.""" original_error = RuntimeError("test error") user_status_code = grpc.StatusCode.INVALID_ARGUMENT user_details = "Invalid argument provided" grpc_status_error = gRPCStatusError( original_exception=original_error, code=user_status_code, details=user_details, ) status = get_grpc_response_status( exc=grpc_status_error, request_timeout_s=30.0, request_id="test_request_123" ) assert status.code == user_status_code assert status.is_error is True assert status.message == user_details def test_get_grpc_response_status_grpc_status_error_no_details(): """Test that gRPCStatusError without details uses original exception message.""" original_error = RuntimeError("original error message") user_status_code = grpc.StatusCode.RESOURCE_EXHAUSTED grpc_status_error = gRPCStatusError( original_exception=original_error, code=user_status_code, details=None, ) status = get_grpc_response_status( exc=grpc_status_error, request_timeout_s=30.0, request_id="test_request_123" ) assert status.code == user_status_code assert status.is_error is True assert "original error message" in status.message def test_truncate_message_short(): """Test that short messages are not truncated.""" short_message = "short error message" result = _truncate_message(short_message) assert result == short_message def test_truncate_message_long(): """Test that long messages are truncated.""" # Create a message longer than the max length long_message = "a" * (GRPC_MAX_STATUS_DETAILS_LENGTH + 1000) result = _truncate_message(long_message) assert len(result) <= GRPC_MAX_STATUS_DETAILS_LENGTH assert result.endswith("... [truncated]") def test_truncate_message_at_boundary(): """Test truncation at the exact boundary.""" # Create a message exactly at the limit exact_message = "a" * GRPC_MAX_STATUS_DETAILS_LENGTH result = _truncate_message(exact_message) assert result == exact_message assert len(result) == GRPC_MAX_STATUS_DETAILS_LENGTH def test_get_grpc_response_status_truncates_long_message(): """Test that long error messages are truncated in INTERNAL errors.""" long_message = "a" * (GRPC_MAX_STATUS_DETAILS_LENGTH + 1000) long_error = RuntimeError(long_message) status = get_grpc_response_status( exc=long_error, request_timeout_s=30.0, request_id="test_request_123" ) assert status.code == grpc.StatusCode.INTERNAL assert status.is_error is True assert len(status.message) <= GRPC_MAX_STATUS_DETAILS_LENGTH assert status.message.endswith("... [truncated]") if __name__ == "__main__": import sys sys.exit(pytest.main(["-v", "-s", __file__]))