blob: 0007acba7d60a958f73ca59ed6ccf762783d4e81 [file]
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Optional
from dubbo.compression import Compressor, Decompressor
from dubbo.compression.identities import Identity
from dubbo.extension import ExtensionError, extensionLoader
from dubbo.protocol.triple.coders import TriDecoder, TriEncoder
from dubbo.protocol.triple.constants import (
GRpcCode,
TripleHeaderName,
TripleHeaderValue,
)
from dubbo.protocol.triple.status import TriRpcStatus
from dubbo.protocol.triple.stream import ClientStream
from dubbo.remoting.aio.http2.headers import Http2Headers
from dubbo.remoting.aio.http2.registries import Http2ErrorCode
from dubbo.remoting.aio.http2.stream import Http2Stream
__all__ = ["TriClientStream"]
class TriClientStream(ClientStream):
"""
Triple client stream.
"""
def __init__(
self,
listener: ClientStream.Listener,
compressor: Optional[Compressor],
):
"""
Initialize the triple client stream.
:param listener: The listener.
:type listener: ClientStream.Listener
:param compressor: The compression.
"""
self._transport_listener = ClientTransportListener(listener)
self._encoder = TriEncoder(compressor)
self._stream: Optional[Http2Stream] = None
@property
def transport_listener(self) -> "ClientTransportListener":
"""
Get the transport listener.
:return: The transport listener.
:rtype: ClientTransportListener
"""
return self._transport_listener
def bind(self, stream: Http2Stream) -> None:
"""
Bind the stream.
:param stream: The stream to bind.
:type stream: Http2Stream
"""
self._stream = stream
def send_headers(self, headers: Http2Headers) -> None:
"""
Send headers to remote peer.
:param headers: The headers to send.
:type headers: Http2Headers
"""
self._stream.send_headers(headers)
def send_message(self, data: bytes, compress_flag: int, last: bool) -> None:
"""
Send message to remote peer.
:param data: The message data.
:type data: bytes
:param compress_flag: The compress flag (0: no compress, 1: compress).
:type compress_flag: int
:param last: Whether this is the last message.
:type last: bool
"""
# encode the data
encoded_data = self._encoder.encode(data, compress_flag)
self._stream.send_data(encoded_data, last)
def cancel_by_local(self, status: TriRpcStatus) -> None:
"""
Cancel the stream by local
:param status: The status
:type status: TriRpcStatus
"""
self._stream.cancel_by_local(Http2ErrorCode.CANCEL)
self._transport_listener.rst = True
class ClientTransportListener(Http2Stream.Listener, TriDecoder.Listener):
"""
Client transport listener.
"""
__slots__ = [
"_listener",
"_decoder",
"_rpc_status",
"_headers_received",
"_rst",
]
def __init__(self, listener: ClientStream.Listener):
"""
Initialize the client transport listener.
:param listener: The listener.
"""
super().__init__()
self._listener = listener
self._decoder: Optional[TriDecoder] = None
self._rpc_status: Optional[TriRpcStatus] = None
self._headers_received = False
self._rst = False
self._trailers: Http2Headers = Http2Headers()
@property
def rst(self) -> bool:
"""
Whether the stream is rest.
:return: True if the stream is rest, otherwise False.
:rtype: bool
"""
return self._rst
@rst.setter
def rst(self, value: bool) -> None:
"""
Set whether the stream is rest.
:param value: True if the stream is rest, otherwise False.
:type value: bool
"""
self._rst = value
def on_headers(self, headers: Http2Headers, end_stream: bool) -> None:
if not end_stream:
# handle headers
self._on_headers_received(headers)
else:
# handle trailers
self._on_trailers_received(headers)
if end_stream and not self._headers_received:
self._handle_transport_error(self._rpc_status)
def on_data(self, data: bytes, end_stream: bool) -> None:
if self._rpc_status:
self._rpc_status.append_description(f"Data: {data.decode('utf-8')}")
if len(self._rpc_status.description) > 512 or end_stream:
self._handle_transport_error(self._rpc_status)
return
# decode the data
self._decoder.decode(data)
def cancel_by_remote(self, error_code: Http2ErrorCode) -> None:
self.rst = True
self._rpc_status = TriRpcStatus(
GRpcCode.CANCELLED,
description=f"Cancelled by remote peer, error code: {error_code}",
)
self._listener.on_complete(self._rpc_status, self._trailers.to_dict())
def _on_headers_received(self, headers: Http2Headers) -> None:
"""
Handle the headers received.
:param headers: The headers.
:type headers: Http2Headers
"""
self._headers_received = True
# validate headers
self._validate_headers(headers)
if self._rpc_status:
return
# get messageEncoding
decompressor: Optional[Decompressor] = None
message_encoding = headers.get(TripleHeaderName.GRPC_ENCODING.value, Identity.get_message_encoding())
if message_encoding != Identity.get_message_encoding():
try:
# get decompressor by messageEncoding
decompressor = extensionLoader.get_extension(Decompressor, message_encoding)()
except ExtensionError:
# unsupported
self._rpc_status = TriRpcStatus(
GRpcCode.UNIMPLEMENTED,
description="Unsupported message encoding",
)
return
self._decoder = TriDecoder(self, decompressor)
def _validate_headers(self, headers: Http2Headers) -> None:
"""
Validate the headers.
:param headers: The headers.
:type headers: Http2Headers
"""
status_code = int(headers.status) if headers.status else None
if status_code:
content_type = headers.get(TripleHeaderName.CONTENT_TYPE.value, "")
if not content_type.startswith(TripleHeaderValue.APPLICATION_GRPC.value):
self._rpc_status = TriRpcStatus.from_http_code(status_code).with_description(
f"Invalid content type: {content_type}"
)
else:
self._rpc_status = TriRpcStatus(GRpcCode.INTERNAL, description="Missing HTTP status code")
def _on_trailers_received(self, trailers: Http2Headers) -> None:
"""
Handle the trailers received.
:param trailers: The trailers.
:type trailers: Http2Headers
"""
if not self._rpc_status and not self._headers_received:
self._validate_headers(trailers)
if self._rpc_status:
self._rpc_status.append_description(f"Trailers: {trailers}")
else:
self._rpc_status = self._get_status_from_trailers(trailers)
self._trailers = trailers
if self._decoder:
self._decoder.close()
else:
self._listener.on_complete(self._rpc_status, trailers.to_dict())
def _get_status_from_trailers(self, trailers: Http2Headers) -> TriRpcStatus:
"""
Validate the trailers.
:param trailers: The trailers.
:type trailers: Http2Headers
:return: The RPC status.
:rtype: TriRpcStatus
"""
grpc_status_code = int(trailers.get(TripleHeaderName.GRPC_STATUS.value, "-1"))
if grpc_status_code != -1:
status = TriRpcStatus.from_rpc_code(grpc_status_code)
message = trailers.get(TripleHeaderName.GRPC_MESSAGE.value, "")
status.append_description(message)
return status
# If the status code is not found , something is broken. Try to provide a rational error.
if self._headers_received:
return TriRpcStatus(GRpcCode.UNKNOWN, description="Missing GRPC status in response")
# Try to get status from headers
status_code = int(trailers.status) if trailers.status else None
if status_code is not None:
status = TriRpcStatus.from_http_code(status_code)
else:
status = TriRpcStatus(GRpcCode.INTERNAL, description="Missing HTTP status code")
status.append_description("Missing GRPC status, please infer the error from the HTTP status code")
return status
def _handle_transport_error(self, transport_error: TriRpcStatus) -> None:
"""
Handle the transport error.
:param transport_error: The transport error.
:type transport_error: TriRpcStatus
"""
self._stream.cancel_by_local(Http2ErrorCode.NO_ERROR)
self.rst = True
self._listener.on_complete(transport_error, self._trailers.to_dict())
def on_message(self, message: bytes) -> None:
"""
Called when a message is received (TriDecoder.Listener callback).
:param message: The message received.
"""
self._listener.on_message(message)
def close(self) -> None:
"""
Called when the stream is closed (TriDecoder.Listener callback).
"""
self._listener.on_complete(self._rpc_status, self._trailers.to_dict())