From a9d52959fdd6c1ef19ad6b6201ee6dd84614f307 Mon Sep 17 00:00:00 2001 From: earthchen Date: Mon, 29 Dec 2025 20:32:09 +0800 Subject: [PATCH 01/30] support disableAutoInboundFlowControl api --- .../protocol/tri/ClientStreamObserver.java | 2 + .../rpc/protocol/tri/call/ClientCall.java | 12 +++- .../ObserverToClientCallListenerAdapter.java | 9 +-- .../protocol/tri/call/TripleClientCall.java | 63 ++++++++----------- .../tri/call/UnaryClientCallListener.java | 6 +- .../tri/observer/CallStreamObserver.java | 7 +++ .../observer/ClientCallToObserverAdapter.java | 11 +++- 7 files changed, 65 insertions(+), 45 deletions(-) diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ClientStreamObserver.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ClientStreamObserver.java index 00f7cb0df278..180fc9bf9a44 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ClientStreamObserver.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ClientStreamObserver.java @@ -30,4 +30,6 @@ public interface ClientStreamObserver extends CallStreamObserver { default void disableAutoRequest() { disableAutoFlowControl(); } + + void disableAutoRequestWithInitial(int request); } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java index 8b0e1be1140c..dd20d9d6c3e7 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java @@ -32,6 +32,13 @@ public interface ClientCall { */ interface Listener { + /** + * Whether the response is streaming response. + * + * @return + */ + boolean streamingResponse(); + /** * Called when the call is started, user can use this to set some configurations. * @@ -42,7 +49,7 @@ interface Listener { /** * Callback when message received. * - * @param message message received + * @param message message received * @param actualContentLength actual content length from body */ void onMessage(Object message, int actualContentLength); @@ -89,6 +96,9 @@ interface Listener { */ boolean isAutoRequest(); + + void setAutoRequestWithInitial(int initialRequest); + /** * Set auto request for this call * diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ObserverToClientCallListenerAdapter.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ObserverToClientCallListenerAdapter.java index fce504e9582b..b51594e5b1fb 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ObserverToClientCallListenerAdapter.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ObserverToClientCallListenerAdapter.java @@ -53,13 +53,14 @@ public void onClose(TriRpcStatus status, Map trailers, boolean i } } + @Override + public boolean streamingResponse() { + return true; + } + @Override public void onStart(ClientCall call) { this.call = call; - if (call.isAutoRequest()) { - call.request(1); - } - onStartConsumer.accept(call); } } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java index 99651914974b..40b3e6d66b66 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java @@ -42,6 +42,7 @@ import static org.apache.dubbo.common.constants.LoggerCodeConstants.PROTOCOL_STREAM_LISTENER; public class TripleClientCall implements ClientCall, ClientStream.Listener { + private static final ErrorTypeAwareLogger LOGGER = LoggerFactory.getErrorTypeAwareLogger(TripleClientCall.class); private final AbstractConnectionClient connectionClient; private final Executor executor; @@ -53,7 +54,9 @@ public class TripleClientCall implements ClientCall, ClientStream.Listener { private boolean canceled; private boolean headerSent; private boolean autoRequest = true; + private int initialRequest = 1; private boolean done; + private boolean streamingResponse; private StreamException streamException; public TripleClientCall( @@ -71,32 +74,19 @@ public TripleClientCall( @Override public void onMessage(byte[] message, boolean isReturnTriException) { if (done) { - LOGGER.warn( - PROTOCOL_STREAM_LISTENER, - "", - "", + LOGGER.warn(PROTOCOL_STREAM_LISTENER, "", "", "Received message from closed stream,connection=" + connectionClient + " service=" - + requestMetadata.service + " method=" - + requestMetadata.method.getMethodName()); + + requestMetadata.service + " method=" + requestMetadata.method.getMethodName()); return; } try { Object unpacked = requestMetadata.packableMethod.parseResponse(message, isReturnTriException); listener.onMessage(unpacked, message.length); } catch (Throwable t) { - TriRpcStatus status = TriRpcStatus.INTERNAL - .withDescription("Deserialize response failed") - .withCause(t); + TriRpcStatus status = TriRpcStatus.INTERNAL.withDescription("Deserialize response failed").withCause(t); cancelByLocal(status.asException()); listener.onClose(status, null, false); - LOGGER.error( - PROTOCOL_FAILED_RESPONSE, - "", - "", - String.format( - "Failed to deserialize triple response, service=%s, method=%s,connection=%s", - requestMetadata.service, requestMetadata.service, requestMetadata.method.getMethodName()), - t); + LOGGER.error(PROTOCOL_FAILED_RESPONSE, "", "", String.format("Failed to deserialize triple response, service=%s, method=%s,connection=%s", requestMetadata.service, requestMetadata.service, requestMetadata.method.getMethodName()), t); } } @@ -125,10 +115,7 @@ public void onComplete( try { listener.onClose(status, StreamUtils.toAttachments(attachments), isReturnTriException); } catch (Throwable t) { - cancelByLocal(TriRpcStatus.INTERNAL - .withDescription("Close stream error") - .withCause(t) - .asException()); + cancelByLocal(TriRpcStatus.INTERNAL.withDescription("Close stream error").withCause(t).asException()); } if (requestMetadata.cancellationContext != null) { requestMetadata.cancellationContext.cancel(null); @@ -145,6 +132,11 @@ public void onClose() { @Override public void onStart() { + if (streamingResponse) { + request(initialRequest); + } else { + request(2); + } listener.onStart(this); } @@ -162,8 +154,7 @@ public void cancelByLocal(Throwable t) { return; } if (t instanceof StreamException && ((StreamException) t).error().equals(FLOW_CONTROL_ERROR)) { - TriRpcStatus status = TriRpcStatus.CANCELLED - .withCause(t) + TriRpcStatus status = TriRpcStatus.CANCELLED.withCause(t) .withDescription("Due flowcontrol over pendingbytes, Cancelled by client"); stream.cancelByLocal(status); streamException = (StreamException) t; @@ -205,21 +196,10 @@ public void sendMessage(Object message) { } }); } catch (Throwable t) { - LOGGER.error( - PROTOCOL_FAILED_SERIALIZE_TRIPLE, - "", - "", - String.format( - "Serialize triple request failed, service=%s method=%s", - requestMetadata.service, requestMetadata.method.getMethodName()), - t); + LOGGER.error(PROTOCOL_FAILED_SERIALIZE_TRIPLE, "", "", String.format("Serialize triple request failed, service=%s method=%s", requestMetadata.service, requestMetadata.method.getMethodName()), t); cancelByLocal(t); - listener.onClose( - TriRpcStatus.INTERNAL - .withDescription("Serialize request failed") - .withCause(t), - null, - false); + listener.onClose(TriRpcStatus.INTERNAL.withDescription("Serialize request failed") + .withCause(t), null, false); } } // stream listener end @@ -253,7 +233,8 @@ public StreamObserver start(RequestMetadata metadata, ClientCall.Listene this.requestMetadata = metadata; this.listener = responseListener; this.stream = stream; - return new ClientCallToObserverAdapter<>(this); + this.streamingResponse = responseListener.streamingResponse(); + return new ClientCallToObserverAdapter<>(this, responseListener.streamingResponse()); } } throw new IllegalStateException("No available ClientStreamFactory"); @@ -264,6 +245,12 @@ public boolean isAutoRequest() { return autoRequest; } + @Override + public void setAutoRequestWithInitial(int initialRequest) { + setAutoRequest(false); + this.initialRequest = initialRequest; + } + @Override public void setAutoRequest(boolean autoRequest) { this.autoRequest = autoRequest; diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/UnaryClientCallListener.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/UnaryClientCallListener.java index ca6e0d34bfc3..7e4c7d92794c 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/UnaryClientCallListener.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/UnaryClientCallListener.java @@ -56,9 +56,13 @@ public void onClose(TriRpcStatus status, Map trailers, boolean i future.received(status, result); } + @Override + public boolean streamingResponse() { + return false; + } + @Override public void onStart(ClientCall call) { future.addTimeoutListener(() -> call.cancelByLocal(new IllegalStateException("client timeout"))); - call.request(2); } } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java index d7d74dfb8824..f3eed1a06547 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java @@ -48,4 +48,11 @@ public interface CallStreamObserver extends StreamObserver { * specified. */ void disableAutoFlowControl(); + + /** + * compatible method for gRPC + */ + default void disableAutoInboundFlowControl() { + disableAutoFlowControl(); + } } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java index 9f9b05402954..a0f5da77451d 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java @@ -20,13 +20,17 @@ import org.apache.dubbo.rpc.protocol.tri.ClientStreamObserver; import org.apache.dubbo.rpc.protocol.tri.call.ClientCall; +import java.sql.Timestamp; + public class ClientCallToObserverAdapter extends CancelableStreamObserver implements ClientStreamObserver { private final ClientCall call; + private final boolean streamingResponse; private boolean terminated; - public ClientCallToObserverAdapter(ClientCall call) { + public ClientCallToObserverAdapter(ClientCall call, boolean streamingResponse) { this.call = call; + this.streamingResponse = streamingResponse; } public boolean isAutoRequestEnabled() { @@ -76,4 +80,9 @@ public void request(int count) { public void disableAutoFlowControl() { call.setAutoRequest(false); } + + @Override + public void disableAutoRequestWithInitial(int request) { + call.setAutoRequestWithInitial(request); + } } From 37d6693966106d6bfc53602054619f75bab17b33 Mon Sep 17 00:00:00 2001 From: earthchen Date: Tue, 30 Dec 2025 23:19:00 +0800 Subject: [PATCH 02/30] support back press --- .../dubbo/config/nested/TripleConfig.java | 25 + .../remoting/http12/h2/H2StreamChannel.java | 9 + .../http12/h2/Http2ChannelDelegate.java | 5 + .../message/LengthFieldStreamingDecoder.java | 15 +- .../http12/message/StreamingDecoder.java | 29 +- .../netty4/h2/NettyH2StreamChannel.java | 52 +- .../h2/NettyHttp2ProtocolSelectorHandler.java | 16 +- .../tri/TriHttp2RemoteFlowController.java | 780 ------------------ .../rpc/protocol/tri/TripleHttp2Protocol.java | 57 +- .../rpc/protocol/tri/call/ClientCall.java | 1 - .../protocol/tri/call/TripleClientCall.java | 43 +- .../rpc/protocol/tri/frame/TriDecoder.java | 22 +- .../GrpcHttp2ServerTransportListener.java | 10 + .../GenericHttp2ServerTransportListener.java | 45 +- .../h12/http2/Http2TripleClientStream.java | 82 ++ .../tri/h3/Http3TripleClientStream.java | 6 + .../observer/ClientCallToObserverAdapter.java | 2 - .../stream/AbstractTripleClientStream.java | 22 + .../TripleHttp2LocalFlowController.java | 74 ++ .../protocol/tri/frame/RecordListener.java | 3 + 20 files changed, 467 insertions(+), 831 deletions(-) delete mode 100644 dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TriHttp2RemoteFlowController.java create mode 100644 dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2LocalFlowController.java diff --git a/dubbo-common/src/main/java/org/apache/dubbo/config/nested/TripleConfig.java b/dubbo-common/src/main/java/org/apache/dubbo/config/nested/TripleConfig.java index 3ad3a08c5bef..12750902498a 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/config/nested/TripleConfig.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/config/nested/TripleConfig.java @@ -42,6 +42,7 @@ public class TripleConfig implements Serializable { public static final int DEFAULT_MAX_FRAME_SIZE = 8_388_608; public static final int DEFAULT_MAX_HEADER_LIST_SIZE = 32_768; public static final int DEFAULT_MAX_MESSAGE_SIZE = 50 * 1024 * 1024; + public static final float DEFAULT_WINDOW_UPDATE_RATIO = 0.5f; public static final String H2_SETTINGS_MAX_MESSAGE_SIZE_KEY = "dubbo.protocol.triple.max-message-size"; @@ -151,6 +152,17 @@ public class TripleConfig implements Serializable { */ private Integer maxMessageSize; + /** + * Window update ratio for HTTP/2 flow control. + * Determines when to send WINDOW_UPDATE frames based on the ratio of consumed bytes + * to the initial window size. For example, 0.5 means WINDOW_UPDATE is sent when + * 50% of the initial window has been consumed. + *

Valid range: 0.0 to 1.0 (exclusive of 0, as 0 would disable window updates) + *

The default value is 0.5. + *

For HTTP/2 + */ + private Float windowUpdateRatio; + @Nested private RestConfig rest; @@ -355,6 +367,19 @@ public void setMaxMessageSize(Integer maxMessageSize) { this.maxMessageSize = maxMessageSize; } + public Float getWindowUpdateRatio() { + return windowUpdateRatio; + } + + @Parameter(excluded = true) + public float getWindowUpdateRatioOrDefault() { + return windowUpdateRatio == null ? DEFAULT_WINDOW_UPDATE_RATIO : windowUpdateRatio; + } + + public void setWindowUpdateRatio(Float windowUpdateRatio) { + this.windowUpdateRatio = windowUpdateRatio; + } + public RestConfig getRest() { return rest; } diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/H2StreamChannel.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/H2StreamChannel.java index 005043dcf3d4..7d5620f413f8 100644 --- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/H2StreamChannel.java +++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/H2StreamChannel.java @@ -30,4 +30,13 @@ default Http2OutputMessage newOutputMessage() { } Http2OutputMessage newOutputMessage(boolean endStream); + + /** + * Consume bytes from the local flow controller to trigger WINDOW_UPDATE frames. + * This method should be called when data has been processed and more data can be received. + * + * @param numBytes the number of bytes to consume + * @throws Exception if an error occurs during consumption + */ + void consumeBytes(int numBytes) throws Exception; } diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ChannelDelegate.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ChannelDelegate.java index e3c489ed5e3d..4f9ab3392132 100644 --- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ChannelDelegate.java +++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ChannelDelegate.java @@ -69,6 +69,11 @@ public Http2OutputMessage newOutputMessage(boolean endStream) { return h2StreamChannel.newOutputMessage(endStream); } + @Override + public void consumeBytes(int numBytes) throws Exception { + h2StreamChannel.consumeBytes(numBytes); + } + @Override public String toString() { return "Http2ChannelDelegate{" + "h2StreamChannel=" + h2StreamChannel + '}'; diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/message/LengthFieldStreamingDecoder.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/message/LengthFieldStreamingDecoder.java index b68a3714119b..282398d1815e 100644 --- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/message/LengthFieldStreamingDecoder.java +++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/message/LengthFieldStreamingDecoder.java @@ -184,7 +184,20 @@ private void skipOffset(InputStream inputStream, int lengthFieldOffset) throws I } private void processBody() throws IOException { - byte[] rawMessage = readRawMessage(accumulate, requiredLength); + // Calculate total bytes read: header (offset + length field) + payload + int totalBytesRead = lengthFieldOffset + lengthFieldLength + requiredLength; + + byte[] rawMessage; + try { + rawMessage = readRawMessage(accumulate, requiredLength); + } finally { + // Notify listener about bytes read for flow control immediately after reading bytes + // This must be in finally block to ensure flow control works even if reading fails + // Following gRPC's pattern: bytesRead is called as soon as bytes are consumed from input + listener.bytesRead(totalBytesRead); + } + + // Process the message after notifying about bytes read InputStream inputStream = new ByteArrayInputStream(rawMessage); invokeListener(inputStream); diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/message/StreamingDecoder.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/message/StreamingDecoder.java index b0471f090b3e..08de725c88ce 100644 --- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/message/StreamingDecoder.java +++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/message/StreamingDecoder.java @@ -34,6 +34,13 @@ public interface StreamingDecoder { interface FragmentListener { + /** + * Called when the given number of bytes has been read from the input source of the deframer. + * This is typically used to indicate to the underlying transport that more data can be + * accepted. + */ + void bytesRead(int numBytes); + /** * @param rawMessage raw message */ @@ -42,31 +49,15 @@ interface FragmentListener { default void onClose() {} } - final class DefaultFragmentListener implements FragmentListener { - - private final ListeningDecoder listeningDecoder; - - public DefaultFragmentListener(ListeningDecoder listeningDecoder) { - this.listeningDecoder = listeningDecoder; - } - - @Override - public void onFragmentMessage(InputStream rawMessage) { - listeningDecoder.decode(rawMessage); - } - - @Override - public void onClose() { - listeningDecoder.close(); - } - } - final class NoopFragmentListener implements FragmentListener { static final FragmentListener NOOP = new NoopFragmentListener(); private NoopFragmentListener() {} + @Override + public void bytesRead(int numBytes) {} + @Override public void onFragmentMessage(InputStream rawMessage) {} } diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyH2StreamChannel.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyH2StreamChannel.java index c28449d28bbc..83ee8139c769 100644 --- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyH2StreamChannel.java +++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyH2StreamChannel.java @@ -16,6 +16,8 @@ */ package org.apache.dubbo.remoting.http12.netty4.h2; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; import org.apache.dubbo.config.nested.TripleConfig; import org.apache.dubbo.remoting.http12.HttpMetadata; import org.apache.dubbo.remoting.http12.HttpOutputMessage; @@ -31,17 +33,29 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufOutputStream; import io.netty.handler.codec.http2.DefaultHttp2ResetFrame; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2LocalFlowController; +import io.netty.handler.codec.http2.Http2Stream; import io.netty.handler.codec.http2.Http2StreamChannel; +import static org.apache.dubbo.common.constants.LoggerCodeConstants.PROTOCOL_FAILED_RESPONSE; + public class NettyH2StreamChannel implements H2StreamChannel { + private static final ErrorTypeAwareLogger LOGGER = + LoggerFactory.getErrorTypeAwareLogger(NettyH2StreamChannel.class); + private final Http2StreamChannel http2StreamChannel; private final TripleConfig tripleConfig; - public NettyH2StreamChannel(Http2StreamChannel http2StreamChannel, TripleConfig tripleConfig) { + private final Http2Connection http2Connection; + + public NettyH2StreamChannel( + Http2StreamChannel http2StreamChannel, TripleConfig tripleConfig, Http2Connection http2Connection) { this.http2StreamChannel = http2StreamChannel; this.tripleConfig = tripleConfig; + this.http2Connection = http2Connection; } @Override @@ -89,4 +103,40 @@ public CompletableFuture writeResetFrame(long errorCode) { http2StreamChannel.write(resetFrame).addListener(nettyHttpChannelFutureListener); return nettyHttpChannelFutureListener; } + + @Override + public void consumeBytes(int numBytes) throws Exception { + if (numBytes <= 0) { + return; + } + + if (http2Connection == null) { + LOGGER.debug("Http2Connection not available, skip consumeBytes"); + return; + } + + Http2LocalFlowController localFlowController = http2Connection.local().flowController(); + + // Get the stream from connection using stream id + int streamId = http2StreamChannel.stream().id(); + Http2Stream stream = http2Connection.stream(streamId); + if (stream == null) { + LOGGER.debug("Stream {} not found in connection, skip consumeBytes", streamId); + return; + } + + // Consume bytes to trigger WINDOW_UPDATE frame + // This must be executed in the event loop thread + if (http2StreamChannel.eventLoop().inEventLoop()) { + localFlowController.consumeBytes(stream, numBytes); + } else { + http2StreamChannel.eventLoop().execute(() -> { + try { + localFlowController.consumeBytes(stream, numBytes); + } catch (Exception e) { + LOGGER.warn(PROTOCOL_FAILED_RESPONSE, "", "", "Failed to consumeBytes for stream " + streamId, e); + } + }); + } + } } diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyHttp2ProtocolSelectorHandler.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyHttp2ProtocolSelectorHandler.java index c23e176531e1..bd17e2067cca 100644 --- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyHttp2ProtocolSelectorHandler.java +++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyHttp2ProtocolSelectorHandler.java @@ -36,6 +36,7 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http2.Http2Connection; import io.netty.handler.codec.http2.Http2StreamChannel; public class NettyHttp2ProtocolSelectorHandler extends SimpleChannelInboundHandler { @@ -49,15 +50,27 @@ public class NettyHttp2ProtocolSelectorHandler extends SimpleChannelInboundHandl private final Http2ServerTransportListenerFactory defaultHttp2ServerTransportListenerFactory; + private final Http2Connection http2Connection; + public NettyHttp2ProtocolSelectorHandler( URL url, FrameworkModel frameworkModel, TripleConfig tripleConfig, Http2ServerTransportListenerFactory defaultHttp2ServerTransportListenerFactory) { + this(url, frameworkModel, tripleConfig, defaultHttp2ServerTransportListenerFactory, null); + } + + public NettyHttp2ProtocolSelectorHandler( + URL url, + FrameworkModel frameworkModel, + TripleConfig tripleConfig, + Http2ServerTransportListenerFactory defaultHttp2ServerTransportListenerFactory, + Http2Connection http2Connection) { this.url = url; this.frameworkModel = frameworkModel; this.tripleConfig = tripleConfig; this.defaultHttp2ServerTransportListenerFactory = defaultHttp2ServerTransportListenerFactory; + this.http2Connection = http2Connection; } @Override @@ -74,7 +87,8 @@ protected void channelRead0(ChannelHandlerContext ctx, HttpMetadata metadata) { throw new UnsupportedMediaTypeException(contentType); } Channel channel = ctx.channel(); - H2StreamChannel h2StreamChannel = new NettyH2StreamChannel((Http2StreamChannel) channel, tripleConfig); + H2StreamChannel h2StreamChannel = + new NettyH2StreamChannel((Http2StreamChannel) channel, tripleConfig, http2Connection); HttpWriteQueueHandler writeQueueHandler = channel.parent().pipeline().get(HttpWriteQueueHandler.class); if (writeQueueHandler != null) { HttpWriteQueue writeQueue = writeQueueHandler.getWriteQueue(); diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TriHttp2RemoteFlowController.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TriHttp2RemoteFlowController.java deleted file mode 100644 index 2b23e1766479..000000000000 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TriHttp2RemoteFlowController.java +++ /dev/null @@ -1,780 +0,0 @@ -/* - * Copyright 2014 The Netty Project - * - * The Netty Project 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. - */ - -package org.apache.dubbo.rpc.protocol.tri; - -import org.apache.dubbo.config.nested.TripleConfig; - -import java.util.ArrayDeque; -import java.util.Deque; - -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http2.Http2Connection; -import io.netty.handler.codec.http2.Http2ConnectionAdapter; -import io.netty.handler.codec.http2.Http2Error; -import io.netty.handler.codec.http2.Http2Exception; -import io.netty.handler.codec.http2.Http2RemoteFlowController; -import io.netty.handler.codec.http2.Http2Stream; -import io.netty.handler.codec.http2.Http2StreamVisitor; -import io.netty.handler.codec.http2.StreamByteDistributor; -import io.netty.handler.codec.http2.WeightedFairQueueByteDistributor; -import io.netty.util.internal.UnstableApi; -import io.netty.util.internal.logging.InternalLogger; -import io.netty.util.internal.logging.InternalLoggerFactory; - -import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_WEIGHT; -import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_WEIGHT; -import static io.netty.handler.codec.http2.Http2Error.FLOW_CONTROL_ERROR; -import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR; -import static io.netty.handler.codec.http2.Http2Error.STREAM_CLOSED; -import static io.netty.handler.codec.http2.Http2Exception.streamError; -import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_LOCAL; -import static io.netty.util.internal.ObjectUtil.checkNotNull; -import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; -import static java.lang.Math.max; -import static java.lang.Math.min; - -/** - * This design is learning from {@see io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController} which is in Netty. - */ -@UnstableApi -public class TriHttp2RemoteFlowController implements Http2RemoteFlowController { - private static final InternalLogger logger = InternalLoggerFactory.getInstance(TriHttp2RemoteFlowController.class); - private static final int MIN_WRITABLE_CHUNK = 32 * 1024; - private final Http2Connection connection; - private final Http2Connection.PropertyKey stateKey; - private final StreamByteDistributor streamByteDistributor; - private final FlowState connectionState; - private int initialWindowSize; - private WritabilityMonitor monitor; - private ChannelHandlerContext ctx; - - public TriHttp2RemoteFlowController(Http2Connection connection, TripleConfig config) { - this(connection, (Listener) null, config); - } - - public TriHttp2RemoteFlowController( - Http2Connection connection, StreamByteDistributor streamByteDistributor, TripleConfig config) { - this(connection, streamByteDistributor, null, config); - } - - public TriHttp2RemoteFlowController(Http2Connection connection, final Listener listener, TripleConfig config) { - this(connection, new WeightedFairQueueByteDistributor(connection), listener, config); - } - - public TriHttp2RemoteFlowController( - Http2Connection connection, - StreamByteDistributor streamByteDistributor, - final Listener listener, - TripleConfig config) { - this.connection = checkNotNull(connection, "connection"); - this.streamByteDistributor = checkNotNull(streamByteDistributor, "streamWriteDistributor"); - this.initialWindowSize = config.getConnectionInitialWindowSizeOrDefault(); - - // Add a flow state for the connection. - stateKey = connection.newKey(); - connectionState = new FlowState(connection.connectionStream()); - connection.connectionStream().setProperty(stateKey, connectionState); - - // Monitor may depend upon connectionState, and so initialize after connectionState - listener(listener); - monitor.windowSize(connectionState, initialWindowSize); - - // Register for notification of new streams. - connection.addListener(new Http2ConnectionAdapter() { - @Override - public void onStreamAdded(Http2Stream stream) { - // If the stream state is not open then the stream is not yet eligible for flow controlled frames and - // only requires the ReducedFlowState. Otherwise the full amount of memory is required. - stream.setProperty(stateKey, new FlowState(stream)); - } - - @Override - public void onStreamActive(Http2Stream stream) { - // If the object was previously created, but later activated then we have to ensure the proper - // initialWindowSize is used. - monitor.windowSize(state(stream), initialWindowSize); - } - - @Override - public void onStreamClosed(Http2Stream stream) { - // Any pending frames can never be written, cancel and - // write errors for any pending frames. - state(stream).cancel(STREAM_CLOSED, null); - } - - @Override - public void onStreamHalfClosed(Http2Stream stream) { - if (HALF_CLOSED_LOCAL == stream.state()) { - /* - * When this method is called there should not be any - * pending frames left if the API is used correctly. However, - * it is possible that a erroneous application can sneak - * in a frame even after having already written a frame with the - * END_STREAM flag set, as the stream state might not transition - * immediately to HALF_CLOSED_LOCAL / CLOSED due to flow control - * delaying the write. - * - * This is to cancel any such illegal writes. - */ - state(stream).cancel(STREAM_CLOSED, null); - } - } - }); - } - - /** - * {@inheritDoc} - *

- * Any queued {@link FlowControlled} objects will be sent. - */ - @Override - public void channelHandlerContext(ChannelHandlerContext ctx) throws Http2Exception { - this.ctx = checkNotNull(ctx, "ctx"); - - // Writing the pending bytes will not check writability change and instead a writability change notification - // to be provided by an explicit call. - channelWritabilityChanged(); - - // Don't worry about cleaning up queued frames here if ctx is null. It is expected that all streams will be - // closed and the queue cleanup will occur when the stream state transitions occur. - - // If any frames have been queued up, we should send them now that we have a channel context. - if (isChannelWritable()) { - writePendingBytes(); - } - } - - @Override - public ChannelHandlerContext channelHandlerContext() { - return ctx; - } - - @Override - public void initialWindowSize(int newWindowSize) throws Http2Exception { - assert ctx == null || ctx.executor().inEventLoop(); - monitor.initialWindowSize(newWindowSize); - } - - @Override - public int initialWindowSize() { - return initialWindowSize; - } - - @Override - public int windowSize(Http2Stream stream) { - return state(stream).windowSize(); - } - - @Override - public boolean isWritable(Http2Stream stream) { - return monitor.isWritable(state(stream)); - } - - @Override - public void channelWritabilityChanged() throws Http2Exception { - monitor.channelWritabilityChange(); - } - - @Override - public void updateDependencyTree(int childStreamId, int parentStreamId, short weight, boolean exclusive) { - // It is assumed there are all validated at a higher level. For example in the Http2FrameReader. - assert weight >= MIN_WEIGHT && weight <= MAX_WEIGHT : "Invalid weight"; - assert childStreamId != parentStreamId : "A stream cannot depend on itself"; - assert childStreamId > 0 && parentStreamId >= 0 : "childStreamId must be > 0. parentStreamId must be >= 0."; - - streamByteDistributor.updateDependencyTree(childStreamId, parentStreamId, weight, exclusive); - } - - private boolean isChannelWritable() { - return ctx != null && isChannelWritable0(); - } - - private boolean isChannelWritable0() { - return ctx.channel().isWritable(); - } - - @Override - public void listener(Listener listener) { - monitor = listener == null ? new WritabilityMonitor() : new ListenerWritabilityMonitor(listener); - } - - @Override - public void incrementWindowSize(Http2Stream stream, int delta) throws Http2Exception { - assert ctx == null || ctx.executor().inEventLoop(); - monitor.incrementWindowSize(state(stream), delta); - } - - @Override - public void addFlowControlled(Http2Stream stream, FlowControlled frame) { - // The context can be null assuming the frame will be queued and send later when the context is set. - assert ctx == null || ctx.executor().inEventLoop(); - checkNotNull(frame, "frame"); - try { - monitor.enqueueFrame(state(stream), frame); - } catch (Throwable t) { - frame.error(ctx, t); - } - } - - @Override - public boolean hasFlowControlled(Http2Stream stream) { - return state(stream).hasFrame(); - } - - private FlowState state(Http2Stream stream) { - return (FlowState) stream.getProperty(stateKey); - } - - /** - * Returns the flow control window for the entire connection. - */ - private int connectionWindowSize() { - return connectionState.windowSize(); - } - - private int minUsableChannelBytes() { - // The current allocation algorithm values "fairness" and doesn't give any consideration to "goodput". It - // is possible that 1 byte will be allocated to many streams. In an effort to try to make "goodput" - // reasonable with the current allocation algorithm we have this "cheap" check up front to ensure there is - // an "adequate" amount of connection window before allocation is attempted. This is not foolproof as if the - // number of streams is >= this minimal number then we may still have the issue, but the idea is to narrow the - // circumstances in which this can happen without rewriting the allocation algorithm. - return max(ctx.channel().config().getWriteBufferLowWaterMark(), MIN_WRITABLE_CHUNK); - } - - private int maxUsableChannelBytes() { - // If the channel isWritable, allow at least minUsableChannelBytes. - int channelWritableBytes = (int) min(Integer.MAX_VALUE, ctx.channel().bytesBeforeUnwritable()); - int usableBytes = channelWritableBytes > 0 ? max(channelWritableBytes, minUsableChannelBytes()) : 0; - - // Clip the usable bytes by the connection window. - return min(connectionState.windowSize(), usableBytes); - } - - /** - * The amount of bytes that can be supported by underlying {@link io.netty.channel.Channel} without - * queuing "too-much". - */ - private int writableBytes() { - return min(connectionWindowSize(), maxUsableChannelBytes()); - } - - @Override - public void writePendingBytes() throws Http2Exception { - monitor.writePendingBytes(); - } - - /** - * The remote flow control state for a single stream. - */ - private final class FlowState implements StreamByteDistributor.StreamState { - private final Http2Stream stream; - private final Deque pendingWriteQueue; - private int window; - private long pendingBytes; - private boolean markedWritable; - - /** - * Set to true while a frame is being written, false otherwise. - */ - private boolean writing; - /** - * Set to true if cancel() was called. - */ - private boolean cancelled; - - FlowState(Http2Stream stream) { - this.stream = stream; - pendingWriteQueue = new ArrayDeque<>(2); - } - - /** - * Determine if the stream associated with this object is writable. - * @return {@code true} if the stream associated with this object is writable. - */ - boolean isWritable() { - return windowSize() > pendingBytes() && !cancelled; - } - - /** - * The stream this state is associated with. - */ - @Override - public Http2Stream stream() { - return stream; - } - - /** - * Returns the parameter from the last call to {@link #markedWritability(boolean)}. - */ - boolean markedWritability() { - return markedWritable; - } - - /** - * Save the state of writability. - */ - void markedWritability(boolean isWritable) { - this.markedWritable = isWritable; - } - - @Override - public int windowSize() { - return window; - } - - /** - * Reset the window size for this stream. - */ - void windowSize(int initialWindowSize) { - window = initialWindowSize; - } - - /** - * Write the allocated bytes for this stream. - * @return the number of bytes written for a stream or {@code -1} if no write occurred. - */ - int writeAllocatedBytes(int allocated) { - final int initialAllocated = allocated; - int writtenBytes; - // In case an exception is thrown we want to remember it and pass it to cancel(Throwable). - Throwable cause = null; - FlowControlled frame; - try { - assert !writing; - writing = true; - - // Write the remainder of frames that we are allowed to - boolean writeOccurred = false; - while (!cancelled && (frame = peek()) != null) { - int maxBytes = min(allocated, writableWindow()); - if (maxBytes <= 0 && frame.size() > 0) { - // The frame still has data, but the amount of allocated bytes has been exhausted. - // Don't write needless empty frames. - break; - } - writeOccurred = true; - int initialFrameSize = frame.size(); - try { - frame.write(ctx, max(0, maxBytes)); - if (frame.size() == 0) { - // This frame has been fully written, remove this frame and notify it. - // Since we remove this frame first, we're guaranteed that its error - // method will not be called when we call cancel. - pendingWriteQueue.remove(); - frame.writeComplete(); - } - } finally { - // Decrement allocated by how much was actually written. - allocated -= initialFrameSize - frame.size(); - } - } - - if (!writeOccurred) { - // Either there was no frame, or the amount of allocated bytes has been exhausted. - return -1; - } - - } catch (Throwable t) { - // Mark the state as cancelled, we'll clear the pending queue via cancel() below. - cancelled = true; - cause = t; - } finally { - writing = false; - // Make sure we always decrement the flow control windows - // by the bytes written. - writtenBytes = initialAllocated - allocated; - - decrementPendingBytes(writtenBytes, false); - decrementFlowControlWindow(writtenBytes); - - // If a cancellation occurred while writing, call cancel again to - // clear and error all of the pending writes. - if (cancelled) { - cancel(INTERNAL_ERROR, cause); - } - - // does not check overflow anymore: Let receiver continue receiving the pending bytes. - } - return writtenBytes; - } - - /** - * Increments the flow control window for this stream by the given delta and returns the new value. - */ - int incrementStreamWindow(int delta) throws Http2Exception { - if (delta > 0 && Integer.MAX_VALUE - delta < window) { - throw streamError(stream.id(), FLOW_CONTROL_ERROR, "Window size overflow for stream: %d", stream.id()); - } - window += delta; - streamByteDistributor.updateStreamableBytes(this); - return window; - } - - /** - * Returns the maximum writable window (minimum of the stream and connection windows). - */ - private int writableWindow() { - return min(window, connectionWindowSize()); - } - - @Override - public long pendingBytes() { - return pendingBytes; - } - - /** - * Adds the {@code frame} to the pending queue and increments the pending byte count. - */ - void enqueueFrame(FlowControlled frame) { - FlowControlled last = pendingWriteQueue.peekLast(); - if (last == null) { - enqueueFrameWithoutMerge(frame); - return; - } - - int lastSize = last.size(); - if (last.merge(ctx, frame)) { - incrementPendingBytes(last.size() - lastSize, true); - return; - } - enqueueFrameWithoutMerge(frame); - } - - private void enqueueFrameWithoutMerge(FlowControlled frame) { - pendingWriteQueue.offer(frame); - // This must be called after adding to the queue in order so that hasFrame() is - // updated before updating the stream state. - incrementPendingBytes(frame.size(), true); - } - - @Override - public boolean hasFrame() { - return !pendingWriteQueue.isEmpty(); - } - - /** - * Returns the head of the pending queue, or {@code null} if empty. - */ - private FlowControlled peek() { - return pendingWriteQueue.peek(); - } - - /** - * Clears the pending queue and writes errors for each remaining frame. - * @param error the {@link Http2Error} to use. - * @param cause the {@link Throwable} that caused this method to be invoked. - */ - void cancel(Http2Error error, Throwable cause) { - cancelled = true; - // Ensure that the queue can't be modified while we are writing. - if (writing) { - return; - } - - FlowControlled frame = pendingWriteQueue.poll(); - if (frame != null) { - // Only create exception once and reuse to reduce overhead of filling in the stacktrace. - final Http2Exception exception = - streamError(stream.id(), error, cause, "Stream closed before write could take place"); - do { - writeError(frame, exception); - frame = pendingWriteQueue.poll(); - } while (frame != null); - } - - streamByteDistributor.updateStreamableBytes(this); - - monitor.stateCancelled(this); - } - - /** - * Increments the number of pending bytes for this node and optionally updates the - * {@link StreamByteDistributor}. - */ - private void incrementPendingBytes(int numBytes, boolean updateStreamableBytes) { - pendingBytes += numBytes; - monitor.incrementPendingBytes(numBytes); - if (updateStreamableBytes) { - streamByteDistributor.updateStreamableBytes(this); - } - } - - /** - * If this frame is in the pending queue, decrements the number of pending bytes for the stream. - */ - private void decrementPendingBytes(int bytes, boolean updateStreamableBytes) { - incrementPendingBytes(-bytes, updateStreamableBytes); - } - - /** - * Decrement the per stream and connection flow control window by {@code bytes}. - */ - private void decrementFlowControlWindow(int bytes) { - try { - int negativeBytes = -bytes; - connectionState.incrementStreamWindow(negativeBytes); - incrementStreamWindow(negativeBytes); - } catch (Http2Exception e) { - // Should never get here since we're decrementing. - throw new IllegalStateException("Invalid window state when writing frame: " + e.getMessage(), e); - } - } - - /** - * Discards this {@link FlowControlled}, writing an error. If this frame is in the pending queue, - * the unwritten bytes are removed from this branch of the priority tree. - */ - private void writeError(FlowControlled frame, Http2Exception cause) { - assert ctx != null; - decrementPendingBytes(frame.size(), true); - frame.error(ctx, cause); - } - } - - /** - * Abstract class which provides common functionality for writability monitor implementations. - */ - private class WritabilityMonitor implements StreamByteDistributor.Writer { - private boolean inWritePendingBytes; - private long totalPendingBytes; - - @Override - public final void write(Http2Stream stream, int numBytes) { - state(stream).writeAllocatedBytes(numBytes); - } - - /** - * Called when the writability of the underlying channel changes. - * @throws Http2Exception If a write occurs and an exception happens in the write operation. - */ - void channelWritabilityChange() throws Http2Exception {} - - /** - * Called when the state is cancelled. - * @param state the state that was cancelled. - */ - void stateCancelled(FlowState state) {} - - /** - * Set the initial window size for {@code state}. - * @param state the state to change the initial window size for. - * @param initialWindowSize the size of the window in bytes. - */ - void windowSize(FlowState state, int initialWindowSize) { - state.windowSize(initialWindowSize); - } - - /** - * Increment the window size for a particular stream. - * @param state the state associated with the stream whose window is being incremented. - * @param delta The amount to increment by. - * @throws Http2Exception If this operation overflows the window for {@code state}. - */ - void incrementWindowSize(FlowState state, int delta) throws Http2Exception { - state.incrementStreamWindow(delta); - } - - /** - * Add a frame to be sent via flow control. - * @param state The state associated with the stream which the {@code frame} is associated with. - * @param frame the frame to enqueue. - * @throws Http2Exception If a writability error occurs. - */ - void enqueueFrame(FlowState state, FlowControlled frame) throws Http2Exception { - state.enqueueFrame(frame); - } - - /** - * Increment the total amount of pending bytes for all streams. When any stream's pending bytes changes - * method should be called. - * @param delta The amount to increment by. - */ - final void incrementPendingBytes(int delta) { - totalPendingBytes += delta; - - // Notification of writibilty change should be delayed until the end of the top level event. - // This is to ensure the flow controller is more consistent state before calling external listener methods. - } - - /** - * Determine if the stream associated with {@code state} is writable. - * @param state The state which is associated with the stream to test writability for. - * @return {@code true} if {@link FlowState#stream()} is writable. {@code false} otherwise. - */ - final boolean isWritable(FlowState state) { - return isWritableConnection() && state.isWritable(); - } - - final void writePendingBytes() throws Http2Exception { - // Reentry is not permitted during the byte distribution process. It may lead to undesirable distribution of - // bytes and even infinite loops. We protect against reentry and make sure each call has an opportunity to - // cause a distribution to occur. This may be useful for example if the channel's writability changes from - // Writable -> Not Writable (because we are writing) -> Writable (because the user flushed to make more room - // in the channel outbound buffer). - if (inWritePendingBytes) { - return; - } - inWritePendingBytes = true; - try { - int bytesToWrite = writableBytes(); - // Make sure we always write at least once, regardless if we have bytesToWrite or not. - // This ensures that zero-length frames will always be written. - for (; ; ) { - if (!streamByteDistributor.distribute(bytesToWrite, this) - || (bytesToWrite = writableBytes()) <= 0 - || !isChannelWritable0()) { - break; - } - } - } finally { - inWritePendingBytes = false; - } - } - - void initialWindowSize(int newWindowSize) throws Http2Exception { - checkPositiveOrZero(newWindowSize, "newWindowSize"); - - final int delta = newWindowSize - initialWindowSize; - initialWindowSize = newWindowSize; - connection.forEachActiveStream(new Http2StreamVisitor() { - @Override - public boolean visit(Http2Stream stream) throws Http2Exception { - state(stream).incrementStreamWindow(delta); - return true; - } - }); - - if (delta > 0 && isChannelWritable()) { - // The window size increased, send any pending frames for all streams. - writePendingBytes(); - } - } - - final boolean isWritableConnection() { - return connectionState.windowSize() - totalPendingBytes > 0 && isChannelWritable(); - } - } - - /** - * Writability of a {@code stream} is calculated using the following: - *

-     * Connection Window - Total Queued Bytes > 0 &&
-     * Stream Window - Bytes Queued for Stream > 0 &&
-     * isChannelWritable()
-     * 
- */ - private final class ListenerWritabilityMonitor extends WritabilityMonitor implements Http2StreamVisitor { - private final Listener listener; - - ListenerWritabilityMonitor(Listener listener) { - this.listener = listener; - } - - @Override - public boolean visit(Http2Stream stream) throws Http2Exception { - FlowState state = state(stream); - if (isWritable(state) != state.markedWritability()) { - notifyWritabilityChanged(state); - } - return true; - } - - @Override - void windowSize(FlowState state, int initialWindowSize) { - super.windowSize(state, initialWindowSize); - try { - checkStateWritability(state); - } catch (Http2Exception e) { - throw new RuntimeException("Caught unexpected exception from window", e); - } - } - - @Override - void incrementWindowSize(FlowState state, int delta) throws Http2Exception { - super.incrementWindowSize(state, delta); - checkStateWritability(state); - } - - @Override - void initialWindowSize(int newWindowSize) throws Http2Exception { - super.initialWindowSize(newWindowSize); - if (isWritableConnection()) { - // If the write operation does not occur we still need to check all streams because they - // may have transitioned from writable to not writable. - checkAllWritabilityChanged(); - } - } - - @Override - void enqueueFrame(FlowState state, FlowControlled frame) throws Http2Exception { - super.enqueueFrame(state, frame); - checkConnectionThenStreamWritabilityChanged(state); - } - - @Override - void stateCancelled(FlowState state) { - try { - checkConnectionThenStreamWritabilityChanged(state); - } catch (Http2Exception e) { - throw new RuntimeException("Caught unexpected exception from checkAllWritabilityChanged", e); - } - } - - @Override - void channelWritabilityChange() throws Http2Exception { - if (connectionState.markedWritability() != isChannelWritable()) { - checkAllWritabilityChanged(); - } - } - - private void checkStateWritability(FlowState state) throws Http2Exception { - if (isWritable(state) != state.markedWritability()) { - if (state == connectionState) { - checkAllWritabilityChanged(); - } else { - notifyWritabilityChanged(state); - } - } - } - - private void notifyWritabilityChanged(FlowState state) { - state.markedWritability(!state.markedWritability()); - try { - listener.writabilityChanged(state.stream); - } catch (Throwable cause) { - logger.error("Caught Throwable from listener.writabilityChanged", cause); - } - } - - private void checkConnectionThenStreamWritabilityChanged(FlowState state) throws Http2Exception { - // It is possible that the connection window and/or the individual stream writability could change. - if (isWritableConnection() != connectionState.markedWritability()) { - checkAllWritabilityChanged(); - } else if (isWritable(state) != state.markedWritability()) { - notifyWritabilityChanged(state); - } - // does not check overflow anymore: Let receiver continue receiving the pending bytes. - } - - private void checkAllWritabilityChanged() throws Http2Exception { - // Make sure we mark that we have notified as a result of this change. - connectionState.markedWritability(isWritableConnection()); - connection.forEachActiveStream(this); - } - } -} diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleHttp2Protocol.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleHttp2Protocol.java index eac0646b3acc..918bb82024ad 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleHttp2Protocol.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleHttp2Protocol.java @@ -42,6 +42,7 @@ import org.apache.dubbo.rpc.protocol.tri.h12.http1.DefaultHttp11ServerTransportListenerFactory; import org.apache.dubbo.rpc.protocol.tri.h12.http2.GenericHttp2ServerTransportListenerFactory; import org.apache.dubbo.rpc.protocol.tri.transport.TripleGoAwayHandler; +import org.apache.dubbo.rpc.protocol.tri.transport.TripleHttp2LocalFlowController; import org.apache.dubbo.rpc.protocol.tri.transport.TripleServerConnectionHandler; import org.apache.dubbo.rpc.protocol.tri.transport.TripleTailHandler; import org.apache.dubbo.rpc.protocol.tri.websocket.DefaultWebSocketServerTransportListenerFactory; @@ -61,14 +62,16 @@ import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolConfig; import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler; +import io.netty.handler.codec.http2.DefaultHttp2Connection; import io.netty.handler.codec.http2.Http2CodecUtil; +import io.netty.handler.codec.http2.Http2Connection; import io.netty.handler.codec.http2.Http2FrameCodec; -import io.netty.handler.codec.http2.Http2FrameCodecBuilder; import io.netty.handler.codec.http2.Http2FrameLogger; import io.netty.handler.codec.http2.Http2MultiplexHandler; import io.netty.handler.codec.http2.Http2ServerUpgradeCodec; import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2StreamChannel; +import io.netty.handler.codec.http2.Http2StreamChannelOption; import io.netty.handler.flush.FlushConsolidationHandler; import io.netty.handler.logging.LogLevel; import io.netty.util.AsciiString; @@ -99,7 +102,8 @@ public void close() { @Override public void configClientPipeline(URL url, ChannelOperator operator, ContextOperator contextOperator) { TripleConfig tripleConfig = ConfigManager.getProtocolOrDefault(url).getTripleOrDefault(); - Http2FrameCodec codec = Http2FrameCodecBuilder.forClient() + Http2Connection connection = createHttp2ClientConnection(tripleConfig); + Http2FrameCodec codec = TripleHttp2FrameCodecBuilder.fromConnection(connection) .gracefulShutdownTimeoutMillis(10000) .initialSettings(new Http2Settings() .headerTableSize(tripleConfig.getHeaderTableSizeOrDefault()) @@ -111,7 +115,6 @@ public void configClientPipeline(URL url, ChannelOperator operator, ContextOpera .frameLogger(CLIENT_LOGGER) .validateHeaders(false) .build(); - // codec.connection().local().flowController().frameWriter(codec.encoder().frameWriter()); List handlers = new ArrayList<>(); handlers.add(new ChannelHandlerPretender(codec)); handlers.add(new ChannelHandlerPretender(new Http2MultiplexHandler(new ChannelDuplexHandler()))); @@ -121,6 +124,13 @@ public void configClientPipeline(URL url, ChannelOperator operator, ContextOpera operator.configChannelHandler(handlers); } + private Http2Connection createHttp2ClientConnection(TripleConfig tripleConfig) { + Http2Connection connection = new DefaultHttp2Connection(false); + float windowUpdateRatio = tripleConfig.getWindowUpdateRatioOrDefault(); + connection.local().flowController(new TripleHttp2LocalFlowController(connection, windowUpdateRatio)); + return connection; + } + @Override public void configServerProtocolHandler(URL url, ChannelOperator operator) { String httpVersion = operator.detectResult().getAttribute(TripleProtocolDetector.HTTP_VERSION); @@ -157,13 +167,16 @@ private void configurerHttp1Handlers(URL url, List handlers) { protocol -> { if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { NettyHttp2SettingsHandler nettyHttp2SettingsHandler = new NettyHttp2SettingsHandler(); + Http2Connection http2ServerConnection = createHttp2ServerConnection(tripleConfig); + Http2FrameCodec http2FrameCodec = buildHttp2FrameCodec(http2ServerConnection, tripleConfig); return new Http2ServerUpgradeCodec( - buildHttp2FrameCodec(tripleConfig), + http2FrameCodec, nettyHttp2SettingsHandler, new HttpWriteQueueHandler(), new FlushConsolidationHandler(64, true), new TripleServerConnectionHandler(), - buildHttp2MultiplexHandler(nettyHttp2SettingsHandler, url, tripleConfig), + buildHttp2MultiplexHandler( + nettyHttp2SettingsHandler, url, tripleConfig, http2ServerConnection), new TripleTailHandler()); } else if (AsciiString.contentEquals(HttpHeaderValues.WEBSOCKET, protocol)) { return new WebSocketServerUpgradeCodec( @@ -195,14 +208,25 @@ private void configurerHttp1Handlers(URL url, List handlers) { } private Http2MultiplexHandler buildHttp2MultiplexHandler( - NettyHttp2SettingsHandler nettyHttp2SettingsHandler, URL url, TripleConfig tripleConfig) { + NettyHttp2SettingsHandler nettyHttp2SettingsHandler, + URL url, + TripleConfig tripleConfig, + Http2Connection http2Connection) { return new Http2MultiplexHandler(new ChannelInitializer() { @Override protected void initChannel(Http2StreamChannel ch) { + // Disable Netty's automatic stream flow control to enable manual flow control + // This prevents Netty from automatically sending WINDOW_UPDATE frames + // Application code will call consumeBytes() to trigger WINDOW_UPDATE when ready + ch.config().setOption(Http2StreamChannelOption.AUTO_STREAM_FLOW_CONTROL, false); ChannelPipeline p = ch.pipeline(); p.addLast(new NettyHttp2FrameCodec(nettyHttp2SettingsHandler)); p.addLast(new NettyHttp2ProtocolSelectorHandler( - url, frameworkModel, tripleConfig, GenericHttp2ServerTransportListenerFactory.INSTANCE)); + url, + frameworkModel, + tripleConfig, + GenericHttp2ServerTransportListenerFactory.INSTANCE, + http2Connection)); } }); } @@ -210,8 +234,10 @@ protected void initChannel(Http2StreamChannel ch) { private void configurerHttp2Handlers(URL url, List handlers) { NettyHttp2SettingsHandler nettyHttp2SettingsHandler = new NettyHttp2SettingsHandler(); TripleConfig tripleConfig = ConfigManager.getProtocolOrDefault(url).getTripleOrDefault(); - Http2FrameCodec codec = buildHttp2FrameCodec(tripleConfig); - Http2MultiplexHandler handler = buildHttp2MultiplexHandler(nettyHttp2SettingsHandler, url, tripleConfig); + Http2Connection connection = createHttp2ServerConnection(tripleConfig); + Http2FrameCodec codec = buildHttp2FrameCodec(connection, tripleConfig); + Http2MultiplexHandler handler = + buildHttp2MultiplexHandler(nettyHttp2SettingsHandler, url, tripleConfig, connection); handlers.add(new ChannelHandlerPretender(new HttpWriteQueueHandler())); handlers.add(new ChannelHandlerPretender(codec)); handlers.add(new ChannelHandlerPretender(nettyHttp2SettingsHandler)); @@ -221,10 +247,8 @@ private void configurerHttp2Handlers(URL url, List handlers) { handlers.add(new ChannelHandlerPretender(new TripleTailHandler())); } - private Http2FrameCodec buildHttp2FrameCodec(TripleConfig tripleConfig) { - return TripleHttp2FrameCodecBuilder.forServer() - .customizeConnection((connection) -> - connection.remote().flowController(new TriHttp2RemoteFlowController(connection, tripleConfig))) + private Http2FrameCodec buildHttp2FrameCodec(Http2Connection connection, TripleConfig tripleConfig) { + return TripleHttp2FrameCodecBuilder.fromConnection(connection) .gracefulShutdownTimeoutMillis(10000) .initialSettings(new Http2Settings() .headerTableSize(tripleConfig.getHeaderTableSizeOrDefault()) @@ -237,6 +261,13 @@ private Http2FrameCodec buildHttp2FrameCodec(TripleConfig tripleConfig) { .build(); } + private Http2Connection createHttp2ServerConnection(TripleConfig tripleConfig) { + Http2Connection connection = new DefaultHttp2Connection(true); + float windowUpdateRatio = tripleConfig.getWindowUpdateRatioOrDefault(); + connection.local().flowController(new TripleHttp2LocalFlowController(connection, windowUpdateRatio)); + return connection; + } + private WebSocketServerProtocolHandler buildWebSocketServerProtocolHandler(TripleConfig tripleConfig) { return new WebSocketServerProtocolHandler(WebSocketServerProtocolConfig.newBuilder() .checkStartsWith(true) diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java index dd20d9d6c3e7..f724aea87d0d 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java @@ -96,7 +96,6 @@ interface Listener { */ boolean isAutoRequest(); - void setAutoRequestWithInitial(int initialRequest); /** diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java index 40b3e6d66b66..0bf0b03c90e2 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java @@ -74,7 +74,10 @@ public TripleClientCall( @Override public void onMessage(byte[] message, boolean isReturnTriException) { if (done) { - LOGGER.warn(PROTOCOL_STREAM_LISTENER, "", "", + LOGGER.warn( + PROTOCOL_STREAM_LISTENER, + "", + "", "Received message from closed stream,connection=" + connectionClient + " service=" + requestMetadata.service + " method=" + requestMetadata.method.getMethodName()); return; @@ -83,10 +86,19 @@ public void onMessage(byte[] message, boolean isReturnTriException) { Object unpacked = requestMetadata.packableMethod.parseResponse(message, isReturnTriException); listener.onMessage(unpacked, message.length); } catch (Throwable t) { - TriRpcStatus status = TriRpcStatus.INTERNAL.withDescription("Deserialize response failed").withCause(t); + TriRpcStatus status = TriRpcStatus.INTERNAL + .withDescription("Deserialize response failed") + .withCause(t); cancelByLocal(status.asException()); listener.onClose(status, null, false); - LOGGER.error(PROTOCOL_FAILED_RESPONSE, "", "", String.format("Failed to deserialize triple response, service=%s, method=%s,connection=%s", requestMetadata.service, requestMetadata.service, requestMetadata.method.getMethodName()), t); + LOGGER.error( + PROTOCOL_FAILED_RESPONSE, + "", + "", + String.format( + "Failed to deserialize triple response, service=%s, method=%s,connection=%s", + requestMetadata.service, requestMetadata.service, requestMetadata.method.getMethodName()), + t); } } @@ -115,7 +127,10 @@ public void onComplete( try { listener.onClose(status, StreamUtils.toAttachments(attachments), isReturnTriException); } catch (Throwable t) { - cancelByLocal(TriRpcStatus.INTERNAL.withDescription("Close stream error").withCause(t).asException()); + cancelByLocal(TriRpcStatus.INTERNAL + .withDescription("Close stream error") + .withCause(t) + .asException()); } if (requestMetadata.cancellationContext != null) { requestMetadata.cancellationContext.cancel(null); @@ -154,7 +169,8 @@ public void cancelByLocal(Throwable t) { return; } if (t instanceof StreamException && ((StreamException) t).error().equals(FLOW_CONTROL_ERROR)) { - TriRpcStatus status = TriRpcStatus.CANCELLED.withCause(t) + TriRpcStatus status = TriRpcStatus.CANCELLED + .withCause(t) .withDescription("Due flowcontrol over pendingbytes, Cancelled by client"); stream.cancelByLocal(status); streamException = (StreamException) t; @@ -196,10 +212,21 @@ public void sendMessage(Object message) { } }); } catch (Throwable t) { - LOGGER.error(PROTOCOL_FAILED_SERIALIZE_TRIPLE, "", "", String.format("Serialize triple request failed, service=%s method=%s", requestMetadata.service, requestMetadata.method.getMethodName()), t); + LOGGER.error( + PROTOCOL_FAILED_SERIALIZE_TRIPLE, + "", + "", + String.format( + "Serialize triple request failed, service=%s method=%s", + requestMetadata.service, requestMetadata.method.getMethodName()), + t); cancelByLocal(t); - listener.onClose(TriRpcStatus.INTERNAL.withDescription("Serialize request failed") - .withCause(t), null, false); + listener.onClose( + TriRpcStatus.INTERNAL + .withDescription("Serialize request failed") + .withCause(t), + null, + false); } } // stream listener end diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/frame/TriDecoder.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/frame/TriDecoder.java index b511fff2844f..026399627bd4 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/frame/TriDecoder.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/frame/TriDecoder.java @@ -145,11 +145,23 @@ private void processHeader() { * Processes the GRPC message body, which depending on frame header flags may be compressed. */ private void processBody() { - // There is no reliable way to get the uncompressed size per message when it's compressed, - // because the uncompressed bytes are provided through an InputStream whose total size is - // unknown until all bytes are read, and we don't know when it happens. - byte[] stream = compressedFlag ? getCompressedBody() : getUncompressedBody(); + // Calculate total bytes read: header + payload (before decompression) + int totalBytesRead = HEADER_LENGTH + requiredLength; + byte[] stream; + try { + // There is no reliable way to get the uncompressed size per message when it's compressed, + // because the uncompressed bytes are provided through an InputStream whose total size is + // unknown until all bytes are read, and we don't know when it happens. + stream = compressedFlag ? getCompressedBody() : getUncompressedBody(); + } finally { + // Notify listener about bytes read for flow control immediately after reading bytes + // This must be in finally block to ensure flow control works even if reading fails + // Following gRPC's pattern: bytesRead is called as soon as bytes are consumed from input + listener.bytesRead(totalBytesRead); + } + + // Process the message after notifying about bytes read listener.onRawMessage(stream); // Done with this frame, begin processing the next header. @@ -176,6 +188,8 @@ private enum GrpcDecodeState { public interface Listener { + void bytesRead(int numBytes); + void onRawMessage(byte[] data); void close(); diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/grpc/GrpcHttp2ServerTransportListener.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/grpc/GrpcHttp2ServerTransportListener.java index 8f6c3350d3d5..9aa3f5254249 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/grpc/GrpcHttp2ServerTransportListener.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/grpc/GrpcHttp2ServerTransportListener.java @@ -183,6 +183,16 @@ public void onMessage(InputStream inputStream) { private class DetermineMethodDescriptorListener implements StreamingDecoder.FragmentListener { + @Override + public void bytesRead(int numBytes) { + // Delegate to the H2StreamChannel for flow control + try { + getH2StreamChannel().consumeBytes(numBytes); + } catch (Exception e) { + LOGGER.warn(PROTOCOL_FAILED_PARSE, "", "", "Failed to consume bytes for flow control", e); + } + } + @Override public void onClose() { getStreamingDecoder().close(); diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java index f4f0aeefb3dd..e209665c8aa3 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java @@ -17,6 +17,8 @@ package org.apache.dubbo.rpc.protocol.tri.h12.http2; import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; import org.apache.dubbo.remoting.Constants; import org.apache.dubbo.remoting.http12.HttpHeaderNames; import org.apache.dubbo.remoting.http12.h2.CancelStreamException; @@ -30,6 +32,7 @@ import org.apache.dubbo.remoting.http12.message.ListeningDecoder; import org.apache.dubbo.remoting.http12.message.MediaType; import org.apache.dubbo.remoting.http12.message.StreamingDecoder; +import org.apache.dubbo.remoting.http12.message.StreamingDecoder.FragmentListener; import org.apache.dubbo.remoting.http12.message.codec.JsonCodec; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.RpcContext; @@ -47,9 +50,14 @@ import java.io.InputStream; +import static org.apache.dubbo.common.constants.LoggerCodeConstants.PROTOCOL_FAILED_RESPONSE; + public class GenericHttp2ServerTransportListener extends AbstractServerTransportListener implements Http2TransportListener { + private static final ErrorTypeAwareLogger LOGGER = + LoggerFactory.getErrorTypeAwareLogger(GenericHttp2ServerTransportListener.class); + private final H2StreamChannel h2StreamChannel; private final StreamingDecoder streamingDecoder; private Http2ServerChannelObserver responseObserver; @@ -97,10 +105,45 @@ protected HttpMessageListener buildHttpMessageListener() { DefaultListeningDecoder listeningDecoder = new DefaultListeningDecoder( context.getHttpMessageDecoder(), context.getMethodMetadata().getActualRequestTypes()); listeningDecoder.setListener(new Http2StreamingDecodeListener(serverCallListener)); - streamingDecoder.setFragmentListener(new StreamingDecoder.DefaultFragmentListener(listeningDecoder)); + streamingDecoder.setFragmentListener(new DefaultFragmentListener(listeningDecoder)); return new StreamingHttpMessageListener(streamingDecoder); } + final class DefaultFragmentListener implements FragmentListener { + + private final ListeningDecoder listeningDecoder; + + public DefaultFragmentListener(ListeningDecoder listeningDecoder) { + this.listeningDecoder = listeningDecoder; + } + + @Override + public void bytesRead(int numBytes) { + try { + getH2StreamChannel().consumeBytes(numBytes); + } catch (Exception e) { + LOGGER.warn(PROTOCOL_FAILED_RESPONSE, "", "", "Failed to consume bytes for flow control", e); + } + } + + @Override + public void onFragmentMessage(InputStream rawMessage) { + listeningDecoder.decode(rawMessage); + } + + @Override + public void onClose() { + listeningDecoder.close(); + } + } + + /** + * Get the H2StreamChannel for flow control. + */ + protected H2StreamChannel getH2StreamChannel() { + return h2StreamChannel; + } + private ServerCallListener startListener( RpcInvocation invocation, MethodDescriptor methodDescriptor, Invoker invoker) { switch (methodDescriptor.getRpcType()) { diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/Http2TripleClientStream.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/Http2TripleClientStream.java index 0f4bd44851db..f677e92229cb 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/Http2TripleClientStream.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/Http2TripleClientStream.java @@ -16,6 +16,8 @@ */ package org.apache.dubbo.rpc.protocol.tri.h12.http2; +import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; +import org.apache.dubbo.common.logger.LoggerFactory; import org.apache.dubbo.rpc.model.FrameworkModel; import org.apache.dubbo.rpc.protocol.tri.command.CreateStreamQueueCommand; import org.apache.dubbo.rpc.protocol.tri.stream.AbstractTripleClientStream; @@ -30,11 +32,23 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2FrameCodec; +import io.netty.handler.codec.http2.Http2LocalFlowController; +import io.netty.handler.codec.http2.Http2Stream; import io.netty.handler.codec.http2.Http2StreamChannel; import io.netty.handler.codec.http2.Http2StreamChannelBootstrap; +import io.netty.handler.codec.http2.Http2StreamChannelOption; + +import static org.apache.dubbo.common.constants.LoggerCodeConstants.PROTOCOL_FAILED_RESPONSE; public final class Http2TripleClientStream extends AbstractTripleClientStream { + private static final ErrorTypeAwareLogger LOGGER = + LoggerFactory.getErrorTypeAwareLogger(Http2TripleClientStream.class); + + private final Channel parent; + public Http2TripleClientStream( FrameworkModel frameworkModel, Executor executor, @@ -42,6 +56,7 @@ public Http2TripleClientStream( ClientStream.Listener listener, TripleWriteQueue writeQueue) { super(frameworkModel, executor, writeQueue, listener, parent); + this.parent = parent; } /** @@ -54,11 +69,14 @@ public Http2TripleClientStream( ClientStream.Listener listener, Http2StreamChannel http2StreamChannel) { super(frameworkModel, executor, writeQueue, listener, http2StreamChannel); + this.parent = http2StreamChannel.parent(); } @Override protected TripleStreamChannelFuture initStreamChannel(Channel parent) { Http2StreamChannelBootstrap bootstrap = new Http2StreamChannelBootstrap(parent); + // Disable Netty's automatic stream flow control to enable manual flow control + bootstrap.option(Http2StreamChannelOption.AUTO_STREAM_FLOW_CONTROL, false); bootstrap.handler(new ChannelInboundHandlerAdapter() { @Override public void handlerAdded(ChannelHandlerContext ctx) { @@ -72,4 +90,68 @@ public void handlerAdded(ChannelHandlerContext ctx) { writeQueue.enqueue(CreateStreamQueueCommand.create(bootstrap, streamChannelFuture)); return streamChannelFuture; } + + @Override + protected void consumeBytes(int numBytes) { + if (numBytes <= 0) { + return; + } + + // todo The current implementation is not optimal, and alternative implementations should be considered. + + Channel streamChannel = getStreamChannelFuture().getNow(); + if (!(streamChannel instanceof Http2StreamChannel)) { + return; + } + + Http2StreamChannel http2StreamChannel = (Http2StreamChannel) streamChannel; + + // Get Http2Connection from parent channel pipeline + Http2Connection http2Connection = getHttp2Connection(); + if (http2Connection == null) { + LOGGER.debug("Http2Connection not available for flow control"); + return; + } + + Http2LocalFlowController localFlowController = http2Connection.local().flowController(); + int streamId = http2StreamChannel.stream().id(); + Http2Stream stream = http2Connection.stream(streamId); + if (stream == null) { + LOGGER.debug("Stream {} not found in connection, skip consumeBytes", streamId); + return; + } + + // Consume bytes to trigger WINDOW_UPDATE frame + // This must be executed in the event loop thread + if (http2StreamChannel.eventLoop().inEventLoop()) { + try { + localFlowController.consumeBytes(stream, numBytes); + } catch (Exception e) { + LOGGER.warn(PROTOCOL_FAILED_RESPONSE, "", "", "Failed to consumeBytes for stream " + streamId, e); + } + } else { + http2StreamChannel.eventLoop().execute(() -> { + try { + localFlowController.consumeBytes(stream, numBytes); + } catch (Exception e) { + LOGGER.warn(PROTOCOL_FAILED_RESPONSE, "", "", "Failed to consumeBytes for stream " + streamId, e); + } + }); + } + } + + /** + * Get Http2Connection from parent channel pipeline. + */ + private Http2Connection getHttp2Connection() { + if (parent == null) { + return null; + } + ChannelHandlerContext ctx = parent.pipeline().context(Http2FrameCodec.class); + if (ctx == null) { + return null; + } + Http2FrameCodec codec = (Http2FrameCodec) ctx.handler(); + return codec.connection(); + } } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h3/Http3TripleClientStream.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h3/Http3TripleClientStream.java index df157faaad2c..8fe8b4172381 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h3/Http3TripleClientStream.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h3/Http3TripleClientStream.java @@ -74,4 +74,10 @@ protected void initRequestStream(QuicStreamChannel ch) { writeQueue.enqueue(Http3CreateStreamQueueCommand.create(initializer, future)); return future; } + + @Override + protected void consumeBytes(int numBytes) { + // HTTP/3 flow control is handled differently by QUIC layer + // No explicit flow control needed here + } } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java index a0f5da77451d..75a5f497081a 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java @@ -20,8 +20,6 @@ import org.apache.dubbo.rpc.protocol.tri.ClientStreamObserver; import org.apache.dubbo.rpc.protocol.tri.call.ClientCall; -import java.sql.Timestamp; - public class ClientCallToObserverAdapter extends CancelableStreamObserver implements ClientStreamObserver { private final ClientCall call; diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/AbstractTripleClientStream.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/AbstractTripleClientStream.java index 8b677e74eeba..a60226e17774 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/AbstractTripleClientStream.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/AbstractTripleClientStream.java @@ -113,6 +113,13 @@ protected AbstractTripleClientStream( protected abstract TripleStreamChannelFuture initStreamChannel(Channel parent); + /** + * Get the stream channel future for flow control. + */ + protected TripleStreamChannelFuture getStreamChannelFuture() { + return streamChannelFuture; + } + public ChannelFuture sendHeader(Http2Headers headers) { if (this.writeQueue == null) { // already processed at createStream() @@ -207,6 +214,15 @@ protected H2TransportListener createTransportListener() { return new ClientTransportListener(); } + /** + * Consume bytes for flow control. This method is called after bytes are read from the stream. + * It triggers WINDOW_UPDATE frames to allow more data from the remote peer. + * Subclasses can override this method to provide protocol-specific flow control. + * + * @param numBytes the number of bytes consumed + */ + protected abstract void consumeBytes(int numBytes); + class ClientTransportListener extends AbstractH2TransportListener implements H2TransportListener { private TriRpcStatus transportError; @@ -292,6 +308,12 @@ void onHeaderReceived(Http2Headers headers) { } } TriDecoder.Listener listener = new TriDecoder.Listener() { + + @Override + public void bytesRead(int numBytes) { + consumeBytes(numBytes); + } + @Override public void onRawMessage(byte[] data) { AbstractTripleClientStream.this.listener.onMessage(data, isReturnTriException); diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2LocalFlowController.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2LocalFlowController.java new file mode 100644 index 000000000000..6d206c585354 --- /dev/null +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2LocalFlowController.java @@ -0,0 +1,74 @@ +/* + * 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. + */ +package org.apache.dubbo.rpc.protocol.tri.transport; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2Stream; + +/** + * Custom HTTP/2 local flow controller for Triple protocol with manual flow control. + * + *

This flow controller works together with {@code Http2StreamChannelOption.AUTO_STREAM_FLOW_CONTROL = false} + * to enable manual flow control. The complete mechanism requires two parts: + * + *

1. Disable Netty's automatic WINDOW_UPDATE (at Http2StreamChannel level)

+ *

Set {@code Http2StreamChannelOption.AUTO_STREAM_FLOW_CONTROL = false} when creating Http2StreamChannel. + * This prevents Netty's AbstractHttp2StreamChannel from automatically sending WINDOW_UPDATE frames. + * + *

2. Manual flow control pattern (at Http2Connection level)

+ *
    + *
  1. Data is received and tracked via {@link #receiveFlowControlledFrame} - decreases window size
  2. + *
  3. Application processes the data (decoding, business logic)
  4. + *
  5. Application calls {@link #consumeBytes} to return processed bytes
  6. + *
  7. WINDOW_UPDATE frame is sent when consumed bytes reach threshold (default: 50% of initial window)
  8. + *
+ * + *

Flow Control Call Chain

+ *
+ * Server: StreamingDecoder.bytesRead() → FragmentListener.bytesRead() → H2StreamChannel.consumeBytes()
+ *         → Http2LocalFlowController.consumeBytes() → WINDOW_UPDATE
+ * Client: TriDecoder.Listener.bytesRead() → AbstractTripleClientStream.consumeBytes()
+ *         → Http2LocalFlowController.consumeBytes() → WINDOW_UPDATE
+ * 
+ * + * @see org.apache.dubbo.remoting.http12.h2.H2StreamChannel#consumeBytes(int) + * @see io.netty.handler.codec.http2.Http2StreamChannelOption#AUTO_STREAM_FLOW_CONTROL + */ +public class TripleHttp2LocalFlowController extends DefaultHttp2LocalFlowController { + + /** + * Creates a new flow controller with custom windowUpdateRatio. + * + * @param connection the HTTP/2 connection + * @param windowUpdateRatio the ratio of consumed bytes to initial window size at which + * WINDOW_UPDATE frames are sent. Must be between 0 (exclusive) and 1 (inclusive). + * For example, 0.5 means WINDOW_UPDATE is sent when 50% of the + * initial window has been consumed. + */ + public TripleHttp2LocalFlowController(Http2Connection connection, float windowUpdateRatio) { + super(connection, windowUpdateRatio, true); + } + + @Override + public void receiveFlowControlledFrame(Http2Stream stream, ByteBuf data, int padding, boolean endOfStream) + throws Http2Exception { + super.receiveFlowControlledFrame(stream, data, padding, endOfStream); + } +} diff --git a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/frame/RecordListener.java b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/frame/RecordListener.java index c9ec2776d233..ac74331562f1 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/frame/RecordListener.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/frame/RecordListener.java @@ -21,6 +21,9 @@ public class RecordListener implements TriDecoder.Listener { int dataCount; boolean close; + @Override + public void bytesRead(int numBytes) {} + @Override public void onRawMessage(byte[] data) { dataCount += 1; From f2a427d8d06d7472ac9fd0ed6bdb0a07c76a3eda Mon Sep 17 00:00:00 2001 From: earthchen Date: Tue, 30 Dec 2025 23:27:30 +0800 Subject: [PATCH 03/30] fix --- .../remoting/websocket/netty4/NettyWebSocketChannel.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dubbo-remoting/dubbo-remoting-websocket/src/main/java/org/apache/dubbo/remoting/websocket/netty4/NettyWebSocketChannel.java b/dubbo-remoting/dubbo-remoting-websocket/src/main/java/org/apache/dubbo/remoting/websocket/netty4/NettyWebSocketChannel.java index 261687562c8c..44d2b662bbb8 100644 --- a/dubbo-remoting/dubbo-remoting-websocket/src/main/java/org/apache/dubbo/remoting/websocket/netty4/NettyWebSocketChannel.java +++ b/dubbo-remoting/dubbo-remoting-websocket/src/main/java/org/apache/dubbo/remoting/websocket/netty4/NettyWebSocketChannel.java @@ -56,6 +56,11 @@ public Http2OutputMessage newOutputMessage(boolean endStream) { endStream); } + @Override + public void consumeBytes(int numBytes) throws Exception { + // do nothing + } + @Override public CompletableFuture writeHeader(HttpMetadata httpMetadata) { NettyHttpChannelFutureListener futureListener = new NettyHttpChannelFutureListener(); From 75f85f8bbd183b5db54a1e56fc51cb6225243b85 Mon Sep 17 00:00:00 2001 From: earthchen Date: Tue, 30 Dec 2025 23:30:35 +0800 Subject: [PATCH 04/30] fix --- .../dubbo/remoting/http3/netty4/NettyHttp3StreamChannel.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dubbo-remoting/dubbo-remoting-http3/src/main/java/org/apache/dubbo/remoting/http3/netty4/NettyHttp3StreamChannel.java b/dubbo-remoting/dubbo-remoting-http3/src/main/java/org/apache/dubbo/remoting/http3/netty4/NettyHttp3StreamChannel.java index dc3fb94283cb..4b06e6dfd86a 100644 --- a/dubbo-remoting/dubbo-remoting-http3/src/main/java/org/apache/dubbo/remoting/http3/netty4/NettyHttp3StreamChannel.java +++ b/dubbo-remoting/dubbo-remoting-http3/src/main/java/org/apache/dubbo/remoting/http3/netty4/NettyHttp3StreamChannel.java @@ -51,6 +51,11 @@ public Http2OutputMessage newOutputMessage(boolean endStream) { return new Http2OutputMessageFrame(new ByteBufOutputStream(buffer), endStream); } + @Override + public void consumeBytes(int numBytes) throws Exception { + // HTTP/3 flow control is handled by Netty and QUIC implementation. + } + @Override public CompletableFuture writeHeader(HttpMetadata httpMetadata) { NettyHttpChannelFutureListener futureListener = new NettyHttpChannelFutureListener(); From 72a6bed58a386560334fde49c65055dce0a8fa3e Mon Sep 17 00:00:00 2001 From: earthchen Date: Tue, 30 Dec 2025 23:31:22 +0800 Subject: [PATCH 05/30] Update dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../rpc/protocol/tri/observer/CallStreamObserver.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java index f3eed1a06547..5bd248b214f1 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java @@ -50,7 +50,12 @@ public interface CallStreamObserver extends StreamObserver { void disableAutoFlowControl(); /** - * compatible method for gRPC + * Compatibility method to mirror gRPC Java + * {@code io.grpc.stub.CallStreamObserver#disableAutoInboundFlowControl()}. + *

+ * This allows code written against gRPC's {@code CallStreamObserver} API to be + * more easily reused with Dubbo by providing an equivalent entry point that + * delegates to {@link #disableAutoFlowControl()}. */ default void disableAutoInboundFlowControl() { disableAutoFlowControl(); From 1edffe2d27ce3e6afb41446c870a608b824cb43a Mon Sep 17 00:00:00 2001 From: earthchen Date: Tue, 30 Dec 2025 23:31:31 +0800 Subject: [PATCH 06/30] Update dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2LocalFlowController.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../tri/transport/TripleHttp2LocalFlowController.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2LocalFlowController.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2LocalFlowController.java index 6d206c585354..1f79cccb5c83 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2LocalFlowController.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2LocalFlowController.java @@ -65,10 +65,4 @@ public class TripleHttp2LocalFlowController extends DefaultHttp2LocalFlowControl public TripleHttp2LocalFlowController(Http2Connection connection, float windowUpdateRatio) { super(connection, windowUpdateRatio, true); } - - @Override - public void receiveFlowControlledFrame(Http2Stream stream, ByteBuf data, int padding, boolean endOfStream) - throws Http2Exception { - super.receiveFlowControlledFrame(stream, data, padding, endOfStream); - } } From 00be4d8e32c3e43031b1828b52ea35f6fcff405d Mon Sep 17 00:00:00 2001 From: earthchen Date: Tue, 30 Dec 2025 23:31:38 +0800 Subject: [PATCH 07/30] Update dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../apache/dubbo/rpc/protocol/tri/call/ClientCall.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java index f724aea87d0d..c9900044ee8a 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java @@ -96,6 +96,16 @@ interface Listener { */ boolean isAutoRequest(); + /** + * Enable auto request for this call with an initial number of messages to request. + *

+ * This variant of auto request allows specifying how many response messages should be + * requested from the server immediately when the call starts or auto request is enabled. + * It is similar to {@link #setAutoRequest(boolean)} but also configures the initial + * {@link #request(int) request} amount. + * + * @param initialRequest the initial number of messages to request from the server + */ void setAutoRequestWithInitial(int initialRequest); /** From b4f943f4ce63bb04163fef91521f8364bd729f4c Mon Sep 17 00:00:00 2001 From: earthchen Date: Tue, 30 Dec 2025 23:32:00 +0800 Subject: [PATCH 08/30] Update dubbo-common/src/main/java/org/apache/dubbo/config/nested/TripleConfig.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../main/java/org/apache/dubbo/config/nested/TripleConfig.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dubbo-common/src/main/java/org/apache/dubbo/config/nested/TripleConfig.java b/dubbo-common/src/main/java/org/apache/dubbo/config/nested/TripleConfig.java index 12750902498a..c9b1a34f71f4 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/config/nested/TripleConfig.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/config/nested/TripleConfig.java @@ -377,6 +377,9 @@ public float getWindowUpdateRatioOrDefault() { } public void setWindowUpdateRatio(Float windowUpdateRatio) { + if (windowUpdateRatio != null && (windowUpdateRatio <= 0.0f || windowUpdateRatio > 1.0f)) { + throw new IllegalArgumentException("windowUpdateRatio must be > 0 and <= 1, but was: " + windowUpdateRatio); + } this.windowUpdateRatio = windowUpdateRatio; } From 5e2e7c987bc1e8f18e3da94808d5057fc1ef9294 Mon Sep 17 00:00:00 2001 From: earthchen Date: Tue, 30 Dec 2025 23:32:11 +0800 Subject: [PATCH 09/30] Update dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java index c9900044ee8a..b3442e606caa 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java @@ -35,7 +35,7 @@ interface Listener { /** * Whether the response is streaming response. * - * @return + * @return true if the response is a streaming response */ boolean streamingResponse(); From 72462bbf9613806943ccbb784b63726d81f7eb71 Mon Sep 17 00:00:00 2001 From: earthchen Date: Tue, 30 Dec 2025 23:34:31 +0800 Subject: [PATCH 10/30] fix --- .../dubbo/rpc/protocol/tri/servlet/ServletStreamChannel.java | 5 +++++ .../rpc/protocol/tri/websocket/WebSocketStreamChannel.java | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/dubbo-plugin/dubbo-triple-servlet/src/main/java/org/apache/dubbo/rpc/protocol/tri/servlet/ServletStreamChannel.java b/dubbo-plugin/dubbo-triple-servlet/src/main/java/org/apache/dubbo/rpc/protocol/tri/servlet/ServletStreamChannel.java index af1e12a906ed..3e9942904137 100644 --- a/dubbo-plugin/dubbo-triple-servlet/src/main/java/org/apache/dubbo/rpc/protocol/tri/servlet/ServletStreamChannel.java +++ b/dubbo-plugin/dubbo-triple-servlet/src/main/java/org/apache/dubbo/rpc/protocol/tri/servlet/ServletStreamChannel.java @@ -154,6 +154,11 @@ public Http2OutputMessage newOutputMessage(boolean endStream) { return new Http2OutputMessageFrame(new ByteArrayOutputStream(256), endStream); } + @Override + public void consumeBytes(int numBytes) throws Exception { + // No flow control for servlet + } + @Override public CompletableFuture writeHeader(HttpMetadata httpMetadata) { if (writeable.get()) { diff --git a/dubbo-plugin/dubbo-triple-websocket/src/main/java/org/apache/dubbo/rpc/protocol/tri/websocket/WebSocketStreamChannel.java b/dubbo-plugin/dubbo-triple-websocket/src/main/java/org/apache/dubbo/rpc/protocol/tri/websocket/WebSocketStreamChannel.java index a92235883a63..a91239bf1fe4 100644 --- a/dubbo-plugin/dubbo-triple-websocket/src/main/java/org/apache/dubbo/rpc/protocol/tri/websocket/WebSocketStreamChannel.java +++ b/dubbo-plugin/dubbo-triple-websocket/src/main/java/org/apache/dubbo/rpc/protocol/tri/websocket/WebSocketStreamChannel.java @@ -83,6 +83,11 @@ public Http2OutputMessage newOutputMessage(boolean endStream) { new LimitedByteArrayOutputStream(256, tripleConfig.getMaxResponseBodySizeOrDefault()), endStream); } + @Override + public void consumeBytes(int numBytes) throws Exception { + // do nothing + } + @Override public CompletableFuture writeHeader(HttpMetadata httpMetadata) { Http2Header http2Header = (Http2Header) httpMetadata; From 0006218921449a7b92136d6af564fa8f3ac7efba Mon Sep 17 00:00:00 2001 From: earthchen Date: Tue, 30 Dec 2025 23:37:21 +0800 Subject: [PATCH 11/30] fix --- .../protocol/tri/transport/TripleHttp2LocalFlowController.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2LocalFlowController.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2LocalFlowController.java index 1f79cccb5c83..aaea39940163 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2LocalFlowController.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2LocalFlowController.java @@ -16,11 +16,8 @@ */ package org.apache.dubbo.rpc.protocol.tri.transport; -import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController; import io.netty.handler.codec.http2.Http2Connection; -import io.netty.handler.codec.http2.Http2Exception; -import io.netty.handler.codec.http2.Http2Stream; /** * Custom HTTP/2 local flow controller for Triple protocol with manual flow control. From b74c57aad6cd61e15173e58246727b011ee0843f Mon Sep 17 00:00:00 2001 From: earthchen Date: Wed, 31 Dec 2025 10:26:54 +0800 Subject: [PATCH 12/30] fix test --- .../dubbo/rpc/protocol/tri/test/MockH2StreamChannel.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/test/MockH2StreamChannel.java b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/test/MockH2StreamChannel.java index d9d30a64b1e8..481d673e75b0 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/test/MockH2StreamChannel.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/test/MockH2StreamChannel.java @@ -72,6 +72,11 @@ public Http2OutputMessage newOutputMessage(boolean endStream) { return new MockHttp2OutputMessage(endStream); } + @Override + public void consumeBytes(int numBytes) { + // no-op for mock + } + public HttpMetadata getHttpMetadata() { return httpMetadata; } From 2796b5ee86f5ac7dd4f3f76325a1bdca66150703 Mon Sep 17 00:00:00 2001 From: earthchen Date: Sun, 4 Jan 2026 19:57:56 +0800 Subject: [PATCH 13/30] add isReady and setOnReadyHandler api --- .../tri/servlet/ServletStreamChannel.java | 5 + .../tri/websocket/WebSocketStreamChannel.java | 5 + .../remoting/http12/h2/H2StreamChannel.java | 12 + .../http12/h2/Http2ChannelDelegate.java | 5 + .../http12/h2/Http2ServerChannelObserver.java | 28 ++ .../http12/h2/Http2TransportListener.java | 6 + .../netty4/h2/NettyH2StreamChannel.java | 5 + .../netty4/h2/NettyHttp2FrameHandler.java | 8 + .../http3/netty4/NettyHttp3StreamChannel.java | 5 + .../netty4/NettyWebSocketChannel.java | 5 + .../dubbo/rpc/protocol/tri/TripleInvoker.java | 6 + .../rpc/protocol/tri/call/ClientCall.java | 20 + .../ObserverToClientCallListenerAdapter.java | 31 ++ .../protocol/tri/call/TripleClientCall.java | 40 ++ .../GenericHttp2ServerTransportListener.java | 5 + .../tri/observer/CallStreamObserver.java | 48 +++ .../observer/ClientCallToObserverAdapter.java | 19 + .../stream/AbstractTripleClientStream.java | 28 ++ .../rpc/protocol/tri/stream/ClientStream.java | 7 + .../dubbo/rpc/protocol/tri/stream/Stream.java | 8 + .../tri/transport/H2TransportListener.java | 6 + .../TripleHttp2ClientResponseHandler.java | 8 + .../protocol/tri/call/BackpressureTest.java | 367 ++++++++++++++++++ .../tri/test/MockH2StreamChannel.java | 5 + 24 files changed, 682 insertions(+) create mode 100644 dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/call/BackpressureTest.java diff --git a/dubbo-plugin/dubbo-triple-servlet/src/main/java/org/apache/dubbo/rpc/protocol/tri/servlet/ServletStreamChannel.java b/dubbo-plugin/dubbo-triple-servlet/src/main/java/org/apache/dubbo/rpc/protocol/tri/servlet/ServletStreamChannel.java index 3e9942904137..281e5b65bd96 100644 --- a/dubbo-plugin/dubbo-triple-servlet/src/main/java/org/apache/dubbo/rpc/protocol/tri/servlet/ServletStreamChannel.java +++ b/dubbo-plugin/dubbo-triple-servlet/src/main/java/org/apache/dubbo/rpc/protocol/tri/servlet/ServletStreamChannel.java @@ -262,6 +262,11 @@ public SocketAddress localAddress() { @Override public void flush() {} + @Override + public boolean isReady() { + return writeable.get(); + } + private static CompletableFuture completed() { return CompletableFuture.completedFuture(null); } diff --git a/dubbo-plugin/dubbo-triple-websocket/src/main/java/org/apache/dubbo/rpc/protocol/tri/websocket/WebSocketStreamChannel.java b/dubbo-plugin/dubbo-triple-websocket/src/main/java/org/apache/dubbo/rpc/protocol/tri/websocket/WebSocketStreamChannel.java index a91239bf1fe4..7da094a7ff41 100644 --- a/dubbo-plugin/dubbo-triple-websocket/src/main/java/org/apache/dubbo/rpc/protocol/tri/websocket/WebSocketStreamChannel.java +++ b/dubbo-plugin/dubbo-triple-websocket/src/main/java/org/apache/dubbo/rpc/protocol/tri/websocket/WebSocketStreamChannel.java @@ -129,6 +129,11 @@ public SocketAddress localAddress() { @Override public void flush() {} + @Override + public boolean isReady() { + return session.isOpen(); + } + private CloseReason encodeCloseReason(Http2Header http2Header) { HttpHeaders headers = http2Header.headers(); List statusHeaders = headers.remove(HttpHeaderNames.STATUS.getName()); diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/H2StreamChannel.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/H2StreamChannel.java index 7d5620f413f8..aa5438af2189 100644 --- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/H2StreamChannel.java +++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/H2StreamChannel.java @@ -39,4 +39,16 @@ default Http2OutputMessage newOutputMessage() { * @throws Exception if an error occurs during consumption */ void consumeBytes(int numBytes) throws Exception; + + /** + * Returns whether the stream is ready for writing. If false, the caller should avoid + * calling {@link #writeMessage(org.apache.dubbo.remoting.http12.HttpOutputMessage)} + * to avoid blocking or excessive buffering. + * + *

This method is used for outgoing flow control / backpressure. When the underlying + * transport buffer is full, this returns false. + * + * @return true if the stream is ready for writing + */ + boolean isReady(); } diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ChannelDelegate.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ChannelDelegate.java index 4f9ab3392132..0eede83d169d 100644 --- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ChannelDelegate.java +++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ChannelDelegate.java @@ -74,6 +74,11 @@ public void consumeBytes(int numBytes) throws Exception { h2StreamChannel.consumeBytes(numBytes); } + @Override + public boolean isReady() { + return h2StreamChannel.isReady(); + } + @Override public String toString() { return "Http2ChannelDelegate{" + "h2StreamChannel=" + h2StreamChannel + '}'; diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ServerChannelObserver.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ServerChannelObserver.java index f86ad12cfbdc..fb82a6c2e610 100644 --- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ServerChannelObserver.java +++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ServerChannelObserver.java @@ -38,10 +38,38 @@ public class Http2ServerChannelObserver extends AbstractServerHttpChannelObserve private boolean autoRequestN = true; + private Runnable onReadyHandler; + public Http2ServerChannelObserver(H2StreamChannel h2StreamChannel) { super(h2StreamChannel); } + /** + * Returns whether the stream is ready for writing. + * If false, the caller should avoid calling onNext to prevent blocking or excessive buffering. + */ + public boolean isReady() { + return getHttpChannel().isReady(); + } + + /** + * Sets a callback to be invoked when the stream becomes ready for writing. + */ + public void setOnReadyHandler(Runnable onReadyHandler) { + this.onReadyHandler = onReadyHandler; + } + + /** + * Called when the channel writability changes. + * Triggers the onReadyHandler if the channel is now writable. + */ + public void onWritabilityChanged() { + Runnable handler = this.onReadyHandler; + if (handler != null && isReady()) { + handler.run(); + } + } + public void setStreamingDecoder(StreamingDecoder streamingDecoder) { this.streamingDecoder = streamingDecoder; } diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2TransportListener.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2TransportListener.java index 2c4de2450493..b27df3480824 100644 --- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2TransportListener.java +++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2TransportListener.java @@ -19,4 +19,10 @@ public interface Http2TransportListener extends CancelableTransportListener { void close(); + + /** + * Called when the channel writability changes. + * This is used for backpressure support via isReady/setOnReadyHandler. + */ + default void onWritabilityChanged() {} } diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyH2StreamChannel.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyH2StreamChannel.java index 83ee8139c769..52c7f8db87f0 100644 --- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyH2StreamChannel.java +++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyH2StreamChannel.java @@ -139,4 +139,9 @@ public void consumeBytes(int numBytes) throws Exception { }); } } + + @Override + public boolean isReady() { + return http2StreamChannel.isWritable(); + } } diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyHttp2FrameHandler.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyHttp2FrameHandler.java index 22514f1474c2..aec00de6b2ba 100644 --- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyHttp2FrameHandler.java +++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/netty4/h2/NettyHttp2FrameHandler.java @@ -78,4 +78,12 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws E } h2StreamChannel.writeResetFrame(statusCode); } + + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { + // Notify the transport listener when writability changes + // This enables application-level backpressure via isReady/onReadyHandler + transportListener.onWritabilityChanged(); + super.channelWritabilityChanged(ctx); + } } diff --git a/dubbo-remoting/dubbo-remoting-http3/src/main/java/org/apache/dubbo/remoting/http3/netty4/NettyHttp3StreamChannel.java b/dubbo-remoting/dubbo-remoting-http3/src/main/java/org/apache/dubbo/remoting/http3/netty4/NettyHttp3StreamChannel.java index 4b06e6dfd86a..709ac90c741b 100644 --- a/dubbo-remoting/dubbo-remoting-http3/src/main/java/org/apache/dubbo/remoting/http3/netty4/NettyHttp3StreamChannel.java +++ b/dubbo-remoting/dubbo-remoting-http3/src/main/java/org/apache/dubbo/remoting/http3/netty4/NettyHttp3StreamChannel.java @@ -84,4 +84,9 @@ public SocketAddress localAddress() { public void flush() { http3StreamChannel.flush(); } + + @Override + public boolean isReady() { + return http3StreamChannel.isWritable(); + } } diff --git a/dubbo-remoting/dubbo-remoting-websocket/src/main/java/org/apache/dubbo/remoting/websocket/netty4/NettyWebSocketChannel.java b/dubbo-remoting/dubbo-remoting-websocket/src/main/java/org/apache/dubbo/remoting/websocket/netty4/NettyWebSocketChannel.java index 44d2b662bbb8..a22ee45b4813 100644 --- a/dubbo-remoting/dubbo-remoting-websocket/src/main/java/org/apache/dubbo/remoting/websocket/netty4/NettyWebSocketChannel.java +++ b/dubbo-remoting/dubbo-remoting-websocket/src/main/java/org/apache/dubbo/remoting/websocket/netty4/NettyWebSocketChannel.java @@ -89,4 +89,9 @@ public SocketAddress localAddress() { public void flush() { channel.flush(); } + + @Override + public boolean isReady() { + return channel.isWritable(); + } } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java index 1a6bac6073d4..5bc020a3d17f 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java @@ -246,6 +246,12 @@ StreamObserver streamCall( ClientCall call, RequestMetadata metadata, StreamObserver responseObserver) { ObserverToClientCallListenerAdapter listener = new ObserverToClientCallListenerAdapter(responseObserver); StreamObserver streamObserver = call.start(metadata, listener); + + // Set the request adapter on the listener for onReady() to access onReadyHandler + if (streamObserver instanceof ClientCallToObserverAdapter) { + listener.setRequestAdapter((ClientCallToObserverAdapter) streamObserver); + } + if (responseObserver instanceof CancelableStreamObserver) { final CancellationContext context = new CancellationContext(); CancelableStreamObserver cancelableStreamObserver = diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java index b3442e606caa..785b63140a04 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java @@ -61,6 +61,26 @@ interface Listener { * @param trailers response trailers */ void onClose(TriRpcStatus status, Map trailers, boolean isReturnTriException); + + /** + * Called when the call becomes ready for writing after previously returning false from + * {@link ClientCall#isReady()}. This callback is invoked by the transport layer when + * backpressure is relieved and more messages can be sent. + * + *

Implementations should use this method to resume sending messages that were + * paused due to backpressure. + */ + default void onReady() {} + } + + /** + * Returns whether the stream is ready for writing. + * If false, the caller should avoid calling sendMessage to prevent blocking or excessive buffering. + * + * @return true if the stream is ready for writing + */ + default boolean isReady() { + return true; } /** diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ObserverToClientCallListenerAdapter.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ObserverToClientCallListenerAdapter.java index b51594e5b1fb..2c0c9ae174f3 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ObserverToClientCallListenerAdapter.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ObserverToClientCallListenerAdapter.java @@ -18,15 +18,24 @@ import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.rpc.TriRpcStatus; +import org.apache.dubbo.rpc.protocol.tri.observer.ClientCallToObserverAdapter; import java.util.Map; import java.util.function.Consumer; +/** + * Adapts a response StreamObserver to a ClientCall.Listener. + * + *

This follows gRPC's StreamObserverToCallListenerAdapter pattern where + * the listener holds a reference to the request adapter (ClientCallToObserverAdapter) + * to access the onReadyHandler. + */ public class ObserverToClientCallListenerAdapter implements ClientCall.Listener { private final StreamObserver delegate; private ClientCall call; private Consumer onStartConsumer = clientCall -> {}; + private ClientCallToObserverAdapter requestAdapter; public ObserverToClientCallListenerAdapter(StreamObserver delegate) { this.delegate = delegate; @@ -36,6 +45,13 @@ public void setOnStartConsumer(Consumer onStartConsumer) { this.onStartConsumer = onStartConsumer; } + /** + * Set the request adapter to access onReadyHandler. + */ + public void setRequestAdapter(ClientCallToObserverAdapter requestAdapter) { + this.requestAdapter = requestAdapter; + } + @Override public void onMessage(Object message, int actualContentLength) { delegate.onNext(message); @@ -63,4 +79,19 @@ public void onStart(ClientCall call) { this.call = call; onStartConsumer.accept(call); } + + /** + * Called when the stream becomes ready for writing. + */ + @Override + public void onReady() { + if (requestAdapter == null) { + return; + } + Runnable handler = requestAdapter.getOnReadyHandler(); + if (handler == null) { + return; + } + handler.run(); + } } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java index 0bf0b03c90e2..46f2cb09875d 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java @@ -70,6 +70,17 @@ public TripleClientCall( this.writeQueue = writeQueue; } + @Override + public boolean isReady() { + if (canceled) { + return false; + } + if (done) { + return false; + } + return stream.isReady(); + } + // stream listener start @Override public void onMessage(byte[] message, boolean isReturnTriException) { @@ -137,6 +148,35 @@ public void onComplete( } } + /** + * Called when the stream becomes ready for writing. + * This method is invoked synchronously from the transport layer (AbstractTripleClientStream.onWritabilityChanged), + * and it asynchronously dispatches the callback to the business executor to avoid blocking the Netty EventLoop. + * + *

The call chain is: + *

+     * Netty channelWritabilityChanged
+     *   → AbstractTripleClientStream.onWritabilityChanged() [sync]
+     *   → TripleClientCall.onReady() [this method, schedules async execution]
+     *   → executor.execute(() → listener.onReady())
+     *   → ObserverToClientCallListenerAdapter.onReady() [triggers onReadyHandler]
+     * 
+ */ + @Override + public void onReady() { + if (listener == null) { + return; + } + // ObserverToClientCallListenerAdapter.onReady() triggers the onReadyHandler + executor.execute(() -> { + try { + listener.onReady(); + } catch (Throwable t) { + LOGGER.warn(PROTOCOL_STREAM_LISTENER, "", "", "Error executing listener.onReady()", t); + } + }); + } + @Override public void onClose() { if (done) { diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java index e209665c8aa3..5a2872bebef6 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java @@ -231,6 +231,11 @@ public void close() { responseObserver.close(); } + @Override + public void onWritabilityChanged() { + responseObserver.onWritabilityChanged(); + } + private static final class Http2StreamingDecodeListener implements ListeningDecoder.Listener { private final ServerCallListener serverCallListener; diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java index 5bd248b214f1..46da513406c0 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java @@ -19,8 +19,56 @@ import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.rpc.protocol.tri.compressor.Compressor; +/** + * An extension of {@link StreamObserver} that provides additional functionality for flow control + * and backpressure. This interface mirrors gRPC's {@code CallStreamObserver} for compatibility. + * + *

Key features: + *

    + *
  • {@link #isReady()} - Check if the stream is ready to accept more messages
  • + *
  • {@link #setOnReadyHandler(Runnable)} - Set a callback for when the stream becomes ready
  • + *
  • {@link #request(int)} - Request more messages from the peer (for inbound flow control)
  • + *
  • {@link #disableAutoFlowControl()} - Switch to manual flow control mode
  • + *
+ * + * @param the type of value passed to the stream + */ public interface CallStreamObserver extends StreamObserver { + /** + * Returns {@code true} if the stream is ready to accept more messages. + * + *

If {@code false} is returned, the caller should avoid calling + * {@link StreamObserver#onNext(Object)} to prevent excessive buffering. + * Instead, the caller should wait for the {@link #setOnReadyHandler(Runnable) onReadyHandler} + * to be called before sending more messages. + * + *

This method is safe to call from multiple threads. + * + * @return {@code true} if the stream is ready for writing, {@code false} otherwise + */ + boolean isReady(); + + /** + * Sets a callback to be invoked when the stream becomes ready for writing after + * previously returning {@code false} from {@link #isReady()}. + * + *

The handler will be called on the event loop thread, so any long-running + * operations should be offloaded to a separate executor. + * + *

Typical usage pattern: + *

{@code
+     * observer.setOnReadyHandler(() -> {
+     *     while (observer.isReady() && hasMoreData()) {
+     *         observer.onNext(getNextData());
+     *     }
+     * });
+     * }
+ * + * @param onReadyHandler the handler to invoke when the stream becomes ready + */ + void setOnReadyHandler(Runnable onReadyHandler); + /** * Requests the peer to produce {@code count} more messages to be delivered to the 'inbound' * {@link StreamObserver}. diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java index 75a5f497081a..f832677ecc36 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java @@ -25,6 +25,7 @@ public class ClientCallToObserverAdapter extends CancelableStreamObserver private final ClientCall call; private final boolean streamingResponse; private boolean terminated; + private Runnable onReadyHandler; public ClientCallToObserverAdapter(ClientCall call, boolean streamingResponse) { this.call = call; @@ -69,6 +70,24 @@ public void setCompression(String compression) { call.setCompression(compression); } + @Override + public boolean isReady() { + return call.isReady(); + } + + @Override + public void setOnReadyHandler(Runnable onReadyHandler) { + // Store locally, to be triggered by ObserverToClientCallListenerAdapter.onReady() + this.onReadyHandler = onReadyHandler; + } + + /** + * Get the onReadyHandler for use by ClientCall.Listener.onReady(). + */ + public Runnable getOnReadyHandler() { + return onReadyHandler; + } + @Override public void request(int count) { call.request(count); diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/AbstractTripleClientStream.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/AbstractTripleClientStream.java index a60226e17774..7bbd0239b03c 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/AbstractTripleClientStream.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/AbstractTripleClientStream.java @@ -223,6 +223,29 @@ protected H2TransportListener createTransportListener() { */ protected abstract void consumeBytes(int numBytes); + @Override + public boolean isReady() { + Channel channel = streamChannelFuture.getNow(); + if (channel == null) { + return false; + } + return channel.isWritable(); + } + + /** + * Called when the channel writability changes. + * This method should be invoked by the transport handler when channelWritabilityChanged is triggered. + * It synchronously notifies the listener (TripleClientCall) which is responsible for + * asynchronously triggering all necessary callbacks through its executor. + */ + protected void onWritabilityChanged() { + Channel channel = streamChannelFuture.getNow(); + if (channel != null && channel.isWritable()) { + // Synchronously call listener.onReady(), which will use executor to run the callback + listener.onReady(); + } + } + class ClientTransportListener extends AbstractH2TransportListener implements H2TransportListener { private TriRpcStatus transportError; @@ -496,5 +519,10 @@ public void cancelByRemote(long errorCode) { public void onClose() { executor.execute(listener::onClose); } + + @Override + public void onWritabilityChanged() { + AbstractTripleClientStream.this.onWritabilityChanged(); + } } } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/ClientStream.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/ClientStream.java index 94bff9865d37..feb2037e335e 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/ClientStream.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/ClientStream.java @@ -58,6 +58,13 @@ default void onComplete( } void onClose(); + + /** + * Called when the stream becomes ready for writing after previously returning false from + * {@link Stream#isReady()}. This callback is invoked by the transport layer when + * backpressure is relieved and more messages can be sent. + */ + default void onReady() {} } /** diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/Stream.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/Stream.java index 2ff8d7c83157..d9e56346efbf 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/Stream.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/Stream.java @@ -53,6 +53,14 @@ interface Listener { void onCancelByRemote(TriRpcStatus status); } + /** + * Returns whether the stream is ready for writing. + * If false, the caller should avoid calling sendMessage to prevent blocking or excessive buffering. + * + * @return true if the stream is ready for writing + */ + boolean isReady(); + /** * Send headers to remote peer. * diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/H2TransportListener.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/H2TransportListener.java index 78f0569ac41a..66569d2bb8f5 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/H2TransportListener.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/H2TransportListener.java @@ -44,4 +44,10 @@ public interface H2TransportListener { void cancelByRemote(long errorCode); void onClose(); + + /** + * Called when the channel writability changes. + * This is used for backpressure support via isReady/setOnReadyHandler. + */ + default void onWritabilityChanged() {} } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2ClientResponseHandler.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2ClientResponseHandler.java index e7a1d8b29714..deec517a0ead 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2ClientResponseHandler.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/transport/TripleHttp2ClientResponseHandler.java @@ -96,4 +96,12 @@ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { transportListener.cancelByRemote(Http2Error.INTERNAL_ERROR.code()); ctx.close(); } + + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { + // Notify the transport listener when writability changes + // This enables application-level backpressure via isReady/onReadyHandler + transportListener.onWritabilityChanged(); + super.channelWritabilityChanged(ctx); + } } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/call/BackpressureTest.java b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/call/BackpressureTest.java new file mode 100644 index 000000000000..a67c90f33375 --- /dev/null +++ b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/call/BackpressureTest.java @@ -0,0 +1,367 @@ +/* + * 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. + */ +package org.apache.dubbo.rpc.protocol.tri.call; + +import org.apache.dubbo.common.stream.StreamObserver; +import org.apache.dubbo.rpc.TriRpcStatus; +import org.apache.dubbo.rpc.protocol.tri.observer.ClientCallToObserverAdapter; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for backpressure implementation: isReady() and onReadyHandler. + */ +@Timeout(10) // 10 seconds timeout for all tests to prevent hanging +class BackpressureTest { + + /** + * Test that ClientCallToObserverAdapter stores onReadyHandler locally. + */ + @Test + void testSetOnReadyHandlerStoresLocally() { + MockClientCall mockCall = new MockClientCall(); + ClientCallToObserverAdapter adapter = new ClientCallToObserverAdapter<>(mockCall, true); + + assertNull(adapter.getOnReadyHandler()); + + Runnable handler = () -> {}; + adapter.setOnReadyHandler(handler); + + assertNotNull(adapter.getOnReadyHandler()); + assertEquals(handler, adapter.getOnReadyHandler()); + } + + /** + * Test that isReady() delegates to ClientCall.isReady(). + */ + @Test + void testIsReadyDelegatesToClientCall() { + MockClientCall mockCall = new MockClientCall(); + ClientCallToObserverAdapter adapter = new ClientCallToObserverAdapter<>(mockCall, true); + + mockCall.setReady(true); + assertTrue(adapter.isReady()); + + mockCall.setReady(false); + assertFalse(adapter.isReady()); + } + + /** + * Test that ObserverToClientCallListenerAdapter.onReady() triggers the onReadyHandler. + */ + @Test + void testOnReadyTriggersHandler() { + MockClientCall mockCall = new MockClientCall(); + ClientCallToObserverAdapter adapter = new ClientCallToObserverAdapter<>(mockCall, true); + + AtomicBoolean handlerCalled = new AtomicBoolean(false); + adapter.setOnReadyHandler(() -> handlerCalled.set(true)); + + // Create listener and set request adapter + MockStreamObserver mockObserver = new MockStreamObserver(); + ObserverToClientCallListenerAdapter listener = new ObserverToClientCallListenerAdapter(mockObserver); + listener.setRequestAdapter(adapter); + + // Trigger onReady + listener.onReady(); + + assertTrue(handlerCalled.get()); + } + + /** + * Test that onReady does nothing when no handler is set. + */ + @Test + void testOnReadyWithNoHandler() { + MockStreamObserver mockObserver = new MockStreamObserver(); + ObserverToClientCallListenerAdapter listener = new ObserverToClientCallListenerAdapter(mockObserver); + + // No adapter set - should not throw + listener.onReady(); + + // Adapter set but no handler - should not throw + MockClientCall mockCall = new MockClientCall(); + ClientCallToObserverAdapter adapter = new ClientCallToObserverAdapter<>(mockCall, true); + listener.setRequestAdapter(adapter); + listener.onReady(); + } + + /** + * Test that onReadyHandler can be triggered multiple times. + */ + @Test + void testOnReadyHandlerMultipleTriggers() { + MockClientCall mockCall = new MockClientCall(); + ClientCallToObserverAdapter adapter = new ClientCallToObserverAdapter<>(mockCall, true); + + AtomicInteger triggerCount = new AtomicInteger(0); + adapter.setOnReadyHandler(triggerCount::incrementAndGet); + + MockStreamObserver mockObserver = new MockStreamObserver(); + ObserverToClientCallListenerAdapter listener = new ObserverToClientCallListenerAdapter(mockObserver); + listener.setRequestAdapter(adapter); + + // Trigger multiple times + listener.onReady(); + listener.onReady(); + listener.onReady(); + + assertEquals(3, triggerCount.get()); + } + + /** + * Test ClientCall.Listener.onReady() default implementation. + */ + @Test + void testClientCallListenerOnReadyDefault() { + ClientCall.Listener listener = new ClientCall.Listener() { + @Override + public boolean streamingResponse() { + return true; + } + + @Override + public void onStart(ClientCall call) {} + + @Override + public void onMessage(Object message, int actualContentLength) {} + + @Override + public void onClose(TriRpcStatus status, Map trailers, boolean isReturnTriException) {} + }; + + // Default implementation should not throw + listener.onReady(); + } + + /** + * Test disableAutoFlowControl delegates to ClientCall.setAutoRequest(false). + */ + @Test + void testDisableAutoFlowControl() { + MockClientCall mockCall = new MockClientCall(); + ClientCallToObserverAdapter adapter = new ClientCallToObserverAdapter<>(mockCall, true); + + assertTrue(mockCall.isAutoRequest()); + adapter.disableAutoFlowControl(); + assertFalse(mockCall.isAutoRequest()); + } + + /** + * Test disableAutoRequestWithInitial delegates to ClientCall.setAutoRequestWithInitial(). + */ + @Test + void testDisableAutoRequestWithInitial() { + MockClientCall mockCall = new MockClientCall(); + ClientCallToObserverAdapter adapter = new ClientCallToObserverAdapter<>(mockCall, true); + + adapter.disableAutoRequestWithInitial(5); + assertEquals(5, mockCall.getInitialRequest()); + assertFalse(mockCall.isAutoRequest()); + } + + /** + * Test request() delegates to ClientCall.request(). + */ + @Test + void testRequestDelegation() { + MockClientCall mockCall = new MockClientCall(); + ClientCallToObserverAdapter adapter = new ClientCallToObserverAdapter<>(mockCall, true); + + adapter.request(10); + assertEquals(10, mockCall.getRequestedCount()); + } + + /** + * Test that ObserverToClientCallListenerAdapter.streamingResponse() returns true. + */ + @Test + void testStreamingResponseReturnsTrue() { + MockStreamObserver mockObserver = new MockStreamObserver(); + ObserverToClientCallListenerAdapter listener = new ObserverToClientCallListenerAdapter(mockObserver); + assertTrue(listener.streamingResponse()); + } + + /** + * Test onNext calls delegate.onNext(). + */ + @Test + void testOnNextCallsDelegate() { + AtomicBoolean onNextCalled = new AtomicBoolean(false); + StreamObserver delegate = new StreamObserver() { + @Override + public void onNext(Object data) { + onNextCalled.set(true); + } + + @Override + public void onError(Throwable throwable) {} + + @Override + public void onCompleted() {} + }; + + MockClientCall mockCall = new MockClientCall(); + ObserverToClientCallListenerAdapter listener = new ObserverToClientCallListenerAdapter(delegate); + // Must call onStart first to initialize the call reference + listener.onStart(mockCall); + listener.onMessage("test", 4); + + assertTrue(onNextCalled.get()); + } + + /** + * Test onClose with OK status calls delegate.onCompleted(). + */ + @Test + void testOnCloseWithOkStatus() { + AtomicBoolean onCompletedCalled = new AtomicBoolean(false); + StreamObserver delegate = new StreamObserver() { + @Override + public void onNext(Object data) {} + + @Override + public void onError(Throwable throwable) {} + + @Override + public void onCompleted() { + onCompletedCalled.set(true); + } + }; + + ObserverToClientCallListenerAdapter listener = new ObserverToClientCallListenerAdapter(delegate); + listener.onClose(TriRpcStatus.OK, null, false); + + assertTrue(onCompletedCalled.get()); + } + + /** + * Test onClose with error status calls delegate.onError(). + */ + @Test + void testOnCloseWithErrorStatus() { + AtomicBoolean onErrorCalled = new AtomicBoolean(false); + StreamObserver delegate = new StreamObserver() { + @Override + public void onNext(Object data) {} + + @Override + public void onError(Throwable throwable) { + onErrorCalled.set(true); + } + + @Override + public void onCompleted() {} + }; + + ObserverToClientCallListenerAdapter listener = new ObserverToClientCallListenerAdapter(delegate); + listener.onClose(TriRpcStatus.INTERNAL.withDescription("error"), null, false); + + assertTrue(onErrorCalled.get()); + } + + /** + * Mock ClientCall for testing. + */ + private static class MockClientCall implements ClientCall { + private boolean ready = true; + private boolean autoRequest = true; + private int initialRequest = 0; + private int requestedCount = 0; + + public void setReady(boolean ready) { + this.ready = ready; + } + + public int getInitialRequest() { + return initialRequest; + } + + public int getRequestedCount() { + return requestedCount; + } + + @Override + public boolean isReady() { + return ready; + } + + @Override + public void cancelByLocal(Throwable t) {} + + @Override + public void request(int messageNumber) { + this.requestedCount = messageNumber; + } + + @Override + public void sendMessage(Object message) {} + + @Override + public StreamObserver start( + org.apache.dubbo.rpc.protocol.tri.RequestMetadata metadata, Listener responseListener) { + return null; + } + + @Override + public boolean isAutoRequest() { + return autoRequest; + } + + @Override + public void setAutoRequestWithInitial(int initialRequest) { + this.initialRequest = initialRequest; + this.autoRequest = false; + } + + @Override + public void setAutoRequest(boolean autoRequest) { + this.autoRequest = autoRequest; + } + + @Override + public void halfClose() {} + + @Override + public void setCompression(String compression) {} + } + + /** + * Mock StreamObserver for testing. + */ + private static class MockStreamObserver implements StreamObserver { + @Override + public void onNext(Object data) {} + + @Override + public void onError(Throwable throwable) {} + + @Override + public void onCompleted() {} + } +} diff --git a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/test/MockH2StreamChannel.java b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/test/MockH2StreamChannel.java index 481d673e75b0..7488319a36f7 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/test/MockH2StreamChannel.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/test/MockH2StreamChannel.java @@ -77,6 +77,11 @@ public void consumeBytes(int numBytes) { // no-op for mock } + @Override + public boolean isReady() { + return true; + } + public HttpMetadata getHttpMetadata() { return httpMetadata; } From f959c8acba380d71347d6c4a9685484d53833813 Mon Sep 17 00:00:00 2001 From: earthchen Date: Sun, 4 Jan 2026 20:17:18 +0800 Subject: [PATCH 14/30] fix --- .../h12/AbstractServerTransportListener.java | 4 +++ .../GenericHttp2ServerTransportListener.java | 27 +++++++++++-------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/AbstractServerTransportListener.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/AbstractServerTransportListener.java index 28dc4dce3184..e50796b82020 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/AbstractServerTransportListener.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/AbstractServerTransportListener.java @@ -345,4 +345,8 @@ protected void setMethodDescriptor(MethodDescriptor methodDescriptor) { context.setMethodDescriptor(methodDescriptor); exceptionCustomizerWrapper.setMethodDescriptor(methodDescriptor); } + + protected Executor getExecutor() { + return executor; + } } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java index 5a2872bebef6..cec9a97f7cc2 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java @@ -55,8 +55,7 @@ public class GenericHttp2ServerTransportListener extends AbstractServerTransportListener implements Http2TransportListener { - private static final ErrorTypeAwareLogger LOGGER = - LoggerFactory.getErrorTypeAwareLogger(GenericHttp2ServerTransportListener.class); + private static final ErrorTypeAwareLogger LOGGER = LoggerFactory.getErrorTypeAwareLogger(GenericHttp2ServerTransportListener.class); private final H2StreamChannel h2StreamChannel; private final StreamingDecoder streamingDecoder; @@ -64,7 +63,9 @@ public class GenericHttp2ServerTransportListener extends AbstractServerTransport private ServerCallListener serverCallListener; public GenericHttp2ServerTransportListener( - H2StreamChannel h2StreamChannel, URL url, FrameworkModel frameworkModel) { + H2StreamChannel h2StreamChannel, + URL url, + FrameworkModel frameworkModel) { super(frameworkModel, url, h2StreamChannel); this.h2StreamChannel = h2StreamChannel; streamingDecoder = newStreamingDecoder(); @@ -80,10 +81,8 @@ protected Http2ServerChannelObserver newResponseObserver(H2StreamChannel h2Strea } protected Http2ServerChannelObserver newStreamResponseObserver(H2StreamChannel h2StreamChannel) { - Http2ServerChannelObserver responseObserver = - new Http2SseServerChannelObserver(getFrameworkModel(), h2StreamChannel); - responseObserver.addHeadersCustomizer( - (hs, t) -> hs.set(HttpHeaderNames.CONTENT_TYPE.getKey(), MediaType.TEXT_EVENT_STREAM.getName())); + Http2ServerChannelObserver responseObserver = new Http2SseServerChannelObserver(getFrameworkModel(), h2StreamChannel); + responseObserver.addHeadersCustomizer((hs, t) -> hs.set(HttpHeaderNames.CONTENT_TYPE.getKey(), MediaType.TEXT_EVENT_STREAM.getName())); return responseObserver; } @@ -102,8 +101,8 @@ protected HttpMessageListener buildHttpMessageListener() { RpcInvocation rpcInvocation = buildRpcInvocation(context); serverCallListener = startListener(rpcInvocation, context.getMethodDescriptor(), context.getInvoker()); - DefaultListeningDecoder listeningDecoder = new DefaultListeningDecoder( - context.getHttpMessageDecoder(), context.getMethodMetadata().getActualRequestTypes()); + DefaultListeningDecoder listeningDecoder = new DefaultListeningDecoder(context.getHttpMessageDecoder(), context.getMethodMetadata() + .getActualRequestTypes()); listeningDecoder.setListener(new Http2StreamingDecodeListener(serverCallListener)); streamingDecoder.setFragmentListener(new DefaultFragmentListener(listeningDecoder)); return new StreamingHttpMessageListener(streamingDecoder); @@ -145,7 +144,9 @@ protected H2StreamChannel getH2StreamChannel() { } private ServerCallListener startListener( - RpcInvocation invocation, MethodDescriptor methodDescriptor, Invoker invoker) { + RpcInvocation invocation, + MethodDescriptor methodDescriptor, + Invoker invoker) { switch (methodDescriptor.getRpcType()) { case UNARY: prepareUnaryServerCall(); @@ -233,7 +234,11 @@ public void close() { @Override public void onWritabilityChanged() { - responseObserver.onWritabilityChanged(); + if (getExecutor() == null) { + responseObserver.onWritabilityChanged(); + } else { + getExecutor().execute(responseObserver::onWritabilityChanged); + } } private static final class Http2StreamingDecodeListener implements ListeningDecoder.Listener { From 38dc6824b3cd6fb4542066861ed5f27c5edd98f6 Mon Sep 17 00:00:00 2001 From: earthchen Date: Sun, 4 Jan 2026 20:19:40 +0800 Subject: [PATCH 15/30] fix --- .../GenericHttp2ServerTransportListener.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java index cec9a97f7cc2..03d6807393fd 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/GenericHttp2ServerTransportListener.java @@ -55,7 +55,8 @@ public class GenericHttp2ServerTransportListener extends AbstractServerTransportListener implements Http2TransportListener { - private static final ErrorTypeAwareLogger LOGGER = LoggerFactory.getErrorTypeAwareLogger(GenericHttp2ServerTransportListener.class); + private static final ErrorTypeAwareLogger LOGGER = + LoggerFactory.getErrorTypeAwareLogger(GenericHttp2ServerTransportListener.class); private final H2StreamChannel h2StreamChannel; private final StreamingDecoder streamingDecoder; @@ -63,9 +64,7 @@ public class GenericHttp2ServerTransportListener extends AbstractServerTransport private ServerCallListener serverCallListener; public GenericHttp2ServerTransportListener( - H2StreamChannel h2StreamChannel, - URL url, - FrameworkModel frameworkModel) { + H2StreamChannel h2StreamChannel, URL url, FrameworkModel frameworkModel) { super(frameworkModel, url, h2StreamChannel); this.h2StreamChannel = h2StreamChannel; streamingDecoder = newStreamingDecoder(); @@ -81,8 +80,10 @@ protected Http2ServerChannelObserver newResponseObserver(H2StreamChannel h2Strea } protected Http2ServerChannelObserver newStreamResponseObserver(H2StreamChannel h2StreamChannel) { - Http2ServerChannelObserver responseObserver = new Http2SseServerChannelObserver(getFrameworkModel(), h2StreamChannel); - responseObserver.addHeadersCustomizer((hs, t) -> hs.set(HttpHeaderNames.CONTENT_TYPE.getKey(), MediaType.TEXT_EVENT_STREAM.getName())); + Http2ServerChannelObserver responseObserver = + new Http2SseServerChannelObserver(getFrameworkModel(), h2StreamChannel); + responseObserver.addHeadersCustomizer( + (hs, t) -> hs.set(HttpHeaderNames.CONTENT_TYPE.getKey(), MediaType.TEXT_EVENT_STREAM.getName())); return responseObserver; } @@ -101,8 +102,8 @@ protected HttpMessageListener buildHttpMessageListener() { RpcInvocation rpcInvocation = buildRpcInvocation(context); serverCallListener = startListener(rpcInvocation, context.getMethodDescriptor(), context.getInvoker()); - DefaultListeningDecoder listeningDecoder = new DefaultListeningDecoder(context.getHttpMessageDecoder(), context.getMethodMetadata() - .getActualRequestTypes()); + DefaultListeningDecoder listeningDecoder = new DefaultListeningDecoder( + context.getHttpMessageDecoder(), context.getMethodMetadata().getActualRequestTypes()); listeningDecoder.setListener(new Http2StreamingDecodeListener(serverCallListener)); streamingDecoder.setFragmentListener(new DefaultFragmentListener(listeningDecoder)); return new StreamingHttpMessageListener(streamingDecoder); @@ -144,9 +145,7 @@ protected H2StreamChannel getH2StreamChannel() { } private ServerCallListener startListener( - RpcInvocation invocation, - MethodDescriptor methodDescriptor, - Invoker invoker) { + RpcInvocation invocation, MethodDescriptor methodDescriptor, Invoker invoker) { switch (methodDescriptor.getRpcType()) { case UNARY: prepareUnaryServerCall(); From f0a5c871bdc3f4aeadb075fcb3c3dd27590a809b Mon Sep 17 00:00:00 2001 From: earthchen Date: Mon, 5 Jan 2026 19:46:39 +0800 Subject: [PATCH 16/30] fix Backpressure it --- .../dubbo/rpc/protocol/tri/TripleInvoker.java | 49 +++++++++++++------ .../rpc/protocol/tri/call/ClientCall.java | 6 +-- .../protocol/tri/call/TripleClientCall.java | 17 ++++--- .../h12/http2/Http2TripleClientStream.java | 2 +- .../tri/h3/Http3TripleClientStream.java | 2 +- .../stream/AbstractTripleClientStream.java | 17 ++++++- .../rpc/protocol/tri/TripleInvokerTest.java | 5 +- .../protocol/tri/call/BackpressureTest.java | 5 +- 8 files changed, 70 insertions(+), 33 deletions(-) diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java index 5bc020a3d17f..15eaebc269fe 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java @@ -242,26 +242,43 @@ AsyncRpcResult invokeBiOrClientStream(MethodDescriptor methodDescriptor, Invocat return result; } + /** + * Start a streaming call following gRPC's pattern. + *

+ * The call sequence is: + *

+     * 1. Create adapter and listener
+     * 2. Call beforeStart() if observer is CancelableStreamObserver (allows configuring onReadyHandler)
+     * 3. Start the call (creates stream, may trigger initial onReady)
+     * 
+ */ StreamObserver streamCall( ClientCall call, RequestMetadata metadata, StreamObserver responseObserver) { - ObserverToClientCallListenerAdapter listener = new ObserverToClientCallListenerAdapter(responseObserver); - StreamObserver streamObserver = call.start(metadata, listener); + // Create adapter (streaming calls always have streamingResponse=true) + ClientCallToObserverAdapter adapter = new ClientCallToObserverAdapter<>(call, true); - // Set the request adapter on the listener for onReady() to access onReadyHandler - if (streamObserver instanceof ClientCallToObserverAdapter) { - listener.setRequestAdapter((ClientCallToObserverAdapter) streamObserver); - } + // Create listener and associate with adapter + ObserverToClientCallListenerAdapter listener = new ObserverToClientCallListenerAdapter(responseObserver); + listener.setRequestAdapter(adapter); + // Configure CancelableStreamObserver before starting the call if (responseObserver instanceof CancelableStreamObserver) { - final CancellationContext context = new CancellationContext(); - CancelableStreamObserver cancelableStreamObserver = - (CancelableStreamObserver) responseObserver; - cancelableStreamObserver.setCancellationContext(context); - context.addListener(context1 -> call.cancelByLocal(new IllegalStateException("Canceled by app"))); - listener.setOnStartConsumer(dummy -> cancelableStreamObserver.startRequest()); - cancelableStreamObserver.beforeStart((ClientCallToObserverAdapter) streamObserver); + CancelableStreamObserver cancelableObserver = (CancelableStreamObserver) responseObserver; + // Set up cancellation context + CancellationContext context = new CancellationContext(); + cancelableObserver.setCancellationContext(context); + context.addListener(ctx -> call.cancelByLocal(new IllegalStateException("Canceled by app"))); + listener.setOnStartConsumer(dummy -> cancelableObserver.startRequest()); + + // Call beforeStart BEFORE starting the call - this is the gRPC pattern + // This allows users to configure onReadyHandler before the stream starts + cancelableObserver.beforeStart(adapter); } - return streamObserver; + + // Start the call - creates stream and may trigger initial onReady + call.start(metadata, listener); + + return adapter; } AsyncRpcResult invokeUnary( @@ -302,7 +319,9 @@ AsyncRpcResult invokeUnary( result.setExecutor(callbackExecutor); ClientCall.Listener callListener = new UnaryClientCallListener(future); - final StreamObserver requestObserver = call.start(request, callListener); + // Create adapter for unary call (streamingResponse=false) + ClientCallToObserverAdapter requestObserver = new ClientCallToObserverAdapter<>(call, false); + call.start(request, callListener); requestObserver.onNext(pureArgument); requestObserver.onCompleted(); return result; diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java index 785b63140a04..070f07f233ab 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/ClientCall.java @@ -16,7 +16,6 @@ */ package org.apache.dubbo.rpc.protocol.tri.call; -import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.rpc.TriRpcStatus; import org.apache.dubbo.rpc.protocol.tri.RequestMetadata; @@ -105,11 +104,12 @@ default boolean isReady() { void sendMessage(Object message); /** + * Start the call with the given metadata and response listener. + * * @param metadata request metadata * @param responseListener the listener to receive response - * @return the stream observer representing the request sink */ - StreamObserver start(RequestMetadata metadata, Listener responseListener); + void start(RequestMetadata metadata, Listener responseListener); /** * @return true if this call is auto request diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java index 46f2cb09875d..12e56e2ee037 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/call/TripleClientCall.java @@ -18,14 +18,12 @@ import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; import org.apache.dubbo.common.logger.LoggerFactory; -import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.remoting.api.connection.AbstractConnectionClient; import org.apache.dubbo.rpc.TriRpcStatus; import org.apache.dubbo.rpc.model.FrameworkModel; import org.apache.dubbo.rpc.protocol.tri.RequestMetadata; import org.apache.dubbo.rpc.protocol.tri.compressor.Compressor; import org.apache.dubbo.rpc.protocol.tri.compressor.Identity; -import org.apache.dubbo.rpc.protocol.tri.observer.ClientCallToObserverAdapter; import org.apache.dubbo.rpc.protocol.tri.stream.ClientStream; import org.apache.dubbo.rpc.protocol.tri.stream.ClientStreamFactory; import org.apache.dubbo.rpc.protocol.tri.stream.StreamUtils; @@ -78,6 +76,9 @@ public boolean isReady() { if (done) { return false; } + if (stream == null) { + return false; + } return stream.isReady(); } @@ -292,16 +293,18 @@ public void setCompression(String compression) { } @Override - public StreamObserver start(RequestMetadata metadata, ClientCall.Listener responseListener) { + public void start(RequestMetadata metadata, ClientCall.Listener responseListener) { + // Set listener BEFORE creating stream, so onReady() can access it + this.requestMetadata = metadata; + this.listener = responseListener; + this.streamingResponse = responseListener.streamingResponse(); + ClientStream stream; for (ClientStreamFactory factory : frameworkModel.getActivateExtensions(ClientStreamFactory.class)) { stream = factory.createClientStream(connectionClient, frameworkModel, executor, this, writeQueue); if (stream != null) { - this.requestMetadata = metadata; - this.listener = responseListener; this.stream = stream; - this.streamingResponse = responseListener.streamingResponse(); - return new ClientCallToObserverAdapter<>(this, responseListener.streamingResponse()); + return; } } throw new IllegalStateException("No available ClientStreamFactory"); diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/Http2TripleClientStream.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/Http2TripleClientStream.java index f677e92229cb..87f358bade99 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/Http2TripleClientStream.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/Http2TripleClientStream.java @@ -73,7 +73,7 @@ public Http2TripleClientStream( } @Override - protected TripleStreamChannelFuture initStreamChannel(Channel parent) { + protected TripleStreamChannelFuture initStreamChannel0(Channel parent) { Http2StreamChannelBootstrap bootstrap = new Http2StreamChannelBootstrap(parent); // Disable Netty's automatic stream flow control to enable manual flow control bootstrap.option(Http2StreamChannelOption.AUTO_STREAM_FLOW_CONTROL, false); diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h3/Http3TripleClientStream.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h3/Http3TripleClientStream.java index 8fe8b4172381..0ffd784c6628 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h3/Http3TripleClientStream.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h3/Http3TripleClientStream.java @@ -58,7 +58,7 @@ public Http3TripleClientStream( } @Override - protected TripleStreamChannelFuture initStreamChannel(Channel parent) { + protected TripleStreamChannelFuture initStreamChannel0(Channel parent) { Http3RequestStreamInitializer initializer = new Http3RequestStreamInitializer() { @Override protected void initRequestStream(QuicStreamChannel ch) { diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/AbstractTripleClientStream.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/AbstractTripleClientStream.java index 7bbd0239b03c..d377a774c0fc 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/AbstractTripleClientStream.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/stream/AbstractTripleClientStream.java @@ -30,6 +30,7 @@ import org.apache.dubbo.rpc.protocol.tri.command.DataQueueCommand; import org.apache.dubbo.rpc.protocol.tri.command.EndStreamQueueCommand; import org.apache.dubbo.rpc.protocol.tri.command.HeaderQueueCommand; +import org.apache.dubbo.rpc.protocol.tri.command.InitOnReadyQueueCommand; import org.apache.dubbo.rpc.protocol.tri.compressor.DeCompressor; import org.apache.dubbo.rpc.protocol.tri.compressor.Identity; import org.apache.dubbo.rpc.protocol.tri.frame.Deframer; @@ -111,7 +112,21 @@ protected AbstractTripleClientStream( this.streamChannelFuture = initStreamChannel(parent); } - protected abstract TripleStreamChannelFuture initStreamChannel(Channel parent); + private TripleStreamChannelFuture initStreamChannel(Channel parent) { + TripleStreamChannelFuture tripleStreamChannelFuture = initStreamChannel0(parent); + /** + * Enqueue InitOnReadyQueueCommand after the stream creation command. + * Since WriteQueue executes commands in order within the EventLoop, + * this command will run after the stream channel has been created by CreateStreamQueueCommand. + * + * This is necessary because onReady is only triggered by channelWritabilityChanged, + * which won't fire if the channel is always writable from creation. + */ + writeQueue.enqueue(InitOnReadyQueueCommand.create(tripleStreamChannelFuture, listener)); + return tripleStreamChannelFuture; + } + + protected abstract TripleStreamChannelFuture initStreamChannel0(Channel parent); /** * Get the stream channel future for flow control. diff --git a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/TripleInvokerTest.java b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/TripleInvokerTest.java index a468d9d81558..941153fb2bde 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/TripleInvokerTest.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/TripleInvokerTest.java @@ -40,6 +40,7 @@ import org.mockito.Mockito; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.when; class TripleInvokerTest { @@ -59,8 +60,8 @@ void testNewCall() throws NoSuchMethodException { .createExecutorIfAbsent(url); TripleClientCall call = Mockito.mock(TripleClientCall.class); StreamObserver streamObserver = Mockito.mock(StreamObserver.class); - when(call.start(any(RequestMetadata.class), any(ClientCall.Listener.class))) - .thenReturn(streamObserver); + // start() now returns void, just verify it's called + doNothing().when(call).start(any(RequestMetadata.class), any(ClientCall.Listener.class)); RpcInvocation invocation = new RpcInvocation(); invocation.setMethodName("test"); invocation.setArguments(new Object[] {streamObserver, streamObserver}); diff --git a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/call/BackpressureTest.java b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/call/BackpressureTest.java index a67c90f33375..4f8ed4c47703 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/call/BackpressureTest.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/call/BackpressureTest.java @@ -323,9 +323,8 @@ public void request(int messageNumber) { public void sendMessage(Object message) {} @Override - public StreamObserver start( - org.apache.dubbo.rpc.protocol.tri.RequestMetadata metadata, Listener responseListener) { - return null; + public void start(org.apache.dubbo.rpc.protocol.tri.RequestMetadata metadata, Listener responseListener) { + // No-op for mock } @Override From bfa04b2d1c8a250e566a8775dd34e41c80059905 Mon Sep 17 00:00:00 2001 From: earthchen Date: Mon, 5 Jan 2026 20:31:52 +0800 Subject: [PATCH 17/30] fix --- .../tri/command/InitOnReadyQueueCommand.java | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/command/InitOnReadyQueueCommand.java diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/command/InitOnReadyQueueCommand.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/command/InitOnReadyQueueCommand.java new file mode 100644 index 000000000000..61505532d079 --- /dev/null +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/command/InitOnReadyQueueCommand.java @@ -0,0 +1,67 @@ +/* + * 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. + */ +package org.apache.dubbo.rpc.protocol.tri.command; + +import org.apache.dubbo.rpc.protocol.tri.stream.ClientStream; +import org.apache.dubbo.rpc.protocol.tri.stream.TripleStreamChannelFuture; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; + +/** + * Command to trigger initial onReady after the stream channel is created. + * This is necessary because onReady is only triggered by channelWritabilityChanged, + * which won't fire if the channel is always writable from creation. + * + *

This command should be enqueued immediately after CreateStreamQueueCommand. + * Since the WriteQueue executes commands in order within the EventLoop, + * this command will run after the stream channel has been created. + */ +public class InitOnReadyQueueCommand extends QueuedCommand { + + private final TripleStreamChannelFuture streamChannelFuture; + + private final ClientStream.Listener listener; + + private InitOnReadyQueueCommand(TripleStreamChannelFuture streamChannelFuture, ClientStream.Listener listener) { + this.streamChannelFuture = streamChannelFuture; + this.listener = listener; + this.promise(streamChannelFuture.getParentChannel().newPromise()); + this.channel(streamChannelFuture.getParentChannel()); + } + + public static InitOnReadyQueueCommand create( + TripleStreamChannelFuture streamChannelFuture, ClientStream.Listener listener) { + return new InitOnReadyQueueCommand(streamChannelFuture, listener); + } + + @Override + public void doSend(ChannelHandlerContext ctx, ChannelPromise promise) { + // NOOP - this command does not send any data + } + + @Override + public void run(Channel channel) { + // Work in I/O thread, after CreateStreamQueueCommand has completed + Channel streamChannel = streamChannelFuture.getNow(); + if (streamChannel != null && streamChannel.isWritable()) { + // Trigger initial onReady to allow application to start sending. + listener.onReady(); + } + } +} From e6683c8df7bca23685f100cec43d7b9b96c0781c Mon Sep 17 00:00:00 2001 From: earthchen Date: Mon, 5 Jan 2026 20:39:41 +0800 Subject: [PATCH 18/30] fix --- .../org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java index 15eaebc269fe..0a3fe14b2ad0 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java @@ -243,7 +243,7 @@ AsyncRpcResult invokeBiOrClientStream(MethodDescriptor methodDescriptor, Invocat } /** - * Start a streaming call following gRPC's pattern. + * Start a streaming call *

* The call sequence is: *

@@ -254,14 +254,12 @@ AsyncRpcResult invokeBiOrClientStream(MethodDescriptor methodDescriptor, Invocat
      */
     StreamObserver streamCall(
             ClientCall call, RequestMetadata metadata, StreamObserver responseObserver) {
-        // Create adapter (streaming calls always have streamingResponse=true)
         ClientCallToObserverAdapter adapter = new ClientCallToObserverAdapter<>(call, true);
 
         // Create listener and associate with adapter
         ObserverToClientCallListenerAdapter listener = new ObserverToClientCallListenerAdapter(responseObserver);
         listener.setRequestAdapter(adapter);
 
-        // Configure CancelableStreamObserver before starting the call
         if (responseObserver instanceof CancelableStreamObserver) {
             CancelableStreamObserver cancelableObserver = (CancelableStreamObserver) responseObserver;
             // Set up cancellation context
@@ -270,7 +268,7 @@ StreamObserver streamCall(
             context.addListener(ctx -> call.cancelByLocal(new IllegalStateException("Canceled by app")));
             listener.setOnStartConsumer(dummy -> cancelableObserver.startRequest());
 
-            // Call beforeStart BEFORE starting the call - this is the gRPC pattern
+            // Call beforeStart BEFORE starting the call
             // This allows users to configure onReadyHandler before the stream starts
             cancelableObserver.beforeStart(adapter);
         }

From 9b50e05331d026c619dd30f854132eedaa6beb62 Mon Sep 17 00:00:00 2001
From: earthchen 
Date: Mon, 5 Jan 2026 22:24:01 +0800
Subject: [PATCH 19/30] ClientCallStreamObserver & ServerCallStreamObserver

---
 .../apache/dubbo/common/stream}/CallStreamObserver.java  | 7 +------
 .../dubbo/common/stream/ClientCallStreamObserver.java    | 8 ++++++++
 .../dubbo/common/stream/ServerCallStreamObserver.java    | 3 +++
 .../dubbo/reactive/AbstractTripleReactorPublisher.java   | 4 ++--
 .../dubbo/reactive/AbstractTripleReactorSubscriber.java  | 4 ++--
 .../dubbo/reactive/ClientTripleReactorPublisher.java     | 6 +++---
 .../dubbo/reactive/ServerTripleReactorPublisher.java     | 2 +-
 .../dubbo/reactive/ServerTripleReactorSubscriber.java    | 2 +-
 .../apache/dubbo/reactive/calls/ReactorClientCalls.java  | 2 +-
 .../apache/dubbo/reactive/calls/ReactorServerCalls.java  | 2 +-
 .../dubbo/reactive/handler/ManyToManyMethodHandler.java  | 2 +-
 .../dubbo/reactive/handler/ManyToOneMethodHandler.java   | 2 +-
 .../org/apache/dubbo/reactive/CreateObserverAdapter.java | 8 ++++----
 .../dubbo/rpc/protocol/tri/CancelableStreamObserver.java | 6 +++---
 .../dubbo/rpc/protocol/tri/ClientStreamObserver.java     | 9 +++++++--
 .../dubbo/rpc/protocol/tri/ServerStreamObserver.java     | 5 +++--
 .../tri/h12/http2/Http2ServerStreamObserver.java         | 3 ++-
 .../tri/observer/ClientCallToObserverAdapter.java        | 4 +++-
 18 files changed, 47 insertions(+), 32 deletions(-)
 rename {dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer => dubbo-common/src/main/java/org/apache/dubbo/common/stream}/CallStreamObserver.java (94%)
 create mode 100644 dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java
 create mode 100644 dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java

diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java
similarity index 94%
rename from dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java
rename to dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java
index 46da513406c0..74101082576d 100644
--- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/CallStreamObserver.java
+++ b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java
@@ -14,10 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.dubbo.rpc.protocol.tri.observer;
-
-import org.apache.dubbo.common.stream.StreamObserver;
-import org.apache.dubbo.rpc.protocol.tri.compressor.Compressor;
+package org.apache.dubbo.common.stream;
 
 /**
  * An extension of {@link StreamObserver} that provides additional functionality for flow control
@@ -84,8 +81,6 @@ public interface CallStreamObserver extends StreamObserver {
      * 

* For stream set compression needs to determine whether the metadata has been sent, and carry * on corresponding processing - * - * @param compression {@link Compressor} */ void setCompression(String compression); diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java new file mode 100644 index 000000000000..b0ab8a3e29d6 --- /dev/null +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java @@ -0,0 +1,8 @@ +package org.apache.dubbo.common.stream; + +public interface ClientCallStreamObserver extends CallStreamObserver { + + + void disableAutoRequestWithInitial(int request); + +} diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java new file mode 100644 index 000000000000..ca18b08aecfd --- /dev/null +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java @@ -0,0 +1,3 @@ +package org.apache.dubbo.common.stream; + +public interface ServerCallStreamObserver extends CallStreamObserver {} diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorPublisher.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorPublisher.java index 70d7028a006e..34a55f36cd8a 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorPublisher.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorPublisher.java @@ -17,7 +17,7 @@ package org.apache.dubbo.reactive; import org.apache.dubbo.rpc.protocol.tri.CancelableStreamObserver; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; @@ -27,7 +27,7 @@ import org.reactivestreams.Subscription; /** - * The middle layer between {@link org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver} and Reactive API.

+ * The middle layer between {@link CallStreamObserver} and Reactive API.

* 1. passing the data received by CallStreamObserver to Reactive consumer
* 2. passing the request of Reactive API to CallStreamObserver */ diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorSubscriber.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorSubscriber.java index b3d6fa058e72..c911f903f35f 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorSubscriber.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorSubscriber.java @@ -16,7 +16,7 @@ */ package org.apache.dubbo.reactive; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.concurrent.atomic.AtomicBoolean; @@ -26,7 +26,7 @@ import reactor.util.annotation.NonNull; /** - * The middle layer between {@link org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver} and Reactive API.
+ * The middle layer between {@link CallStreamObserver} and Reactive API.
* Passing the data from Reactive producer to CallStreamObserver. */ public abstract class AbstractTripleReactorSubscriber implements Subscriber, CoreSubscriber { diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ClientTripleReactorPublisher.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ClientTripleReactorPublisher.java index 2ae90228043a..4caba1c79445 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ClientTripleReactorPublisher.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ClientTripleReactorPublisher.java @@ -16,8 +16,8 @@ */ package org.apache.dubbo.reactive; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; -import org.apache.dubbo.rpc.protocol.tri.observer.ClientCallToObserverAdapter; +import org.apache.dubbo.common.stream.CallStreamObserver; +import org.apache.dubbo.common.stream.ClientCallStreamObserver; import java.util.function.Consumer; @@ -36,7 +36,7 @@ public ClientTripleReactorPublisher(Consumer> onSubscribe, } @Override - public void beforeStart(ClientCallToObserverAdapter clientCallToObserverAdapter) { + public void beforeStart(ClientCallStreamObserver clientCallToObserverAdapter) { super.onSubscribe(clientCallToObserverAdapter); } } diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ServerTripleReactorPublisher.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ServerTripleReactorPublisher.java index c5b933674163..7d21174f3829 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ServerTripleReactorPublisher.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ServerTripleReactorPublisher.java @@ -16,7 +16,7 @@ */ package org.apache.dubbo.reactive; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; /** * Used in ManyToOne and ManyToMany in server.
diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ServerTripleReactorSubscriber.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ServerTripleReactorSubscriber.java index 3a4a9729a009..a1a992715c50 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ServerTripleReactorSubscriber.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ServerTripleReactorSubscriber.java @@ -18,7 +18,7 @@ import org.apache.dubbo.rpc.CancellationContext; import org.apache.dubbo.rpc.protocol.tri.CancelableStreamObserver; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.ArrayList; import java.util.List; diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorClientCalls.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorClientCalls.java index dfe55003f390..14d27991af6e 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorClientCalls.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorClientCalls.java @@ -21,7 +21,7 @@ import org.apache.dubbo.reactive.ClientTripleReactorSubscriber; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.model.StubMethodDescriptor; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.stub.StubInvocationUtil; import reactor.core.publisher.Flux; diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorServerCalls.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorServerCalls.java index 73613f3d8c68..c2b829e51d0a 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorServerCalls.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorServerCalls.java @@ -21,7 +21,7 @@ import org.apache.dubbo.reactive.ServerTripleReactorSubscriber; import org.apache.dubbo.rpc.StatusRpcException; import org.apache.dubbo.rpc.TriRpcStatus; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.List; import java.util.concurrent.CompletableFuture; diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToManyMethodHandler.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToManyMethodHandler.java index 6c14436249ba..58a60d57c58a 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToManyMethodHandler.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToManyMethodHandler.java @@ -18,7 +18,7 @@ import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.reactive.calls.ReactorServerCalls; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.stub.StubMethodHandler; import java.util.concurrent.CompletableFuture; diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToOneMethodHandler.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToOneMethodHandler.java index f82a6a614bd9..a5ff39a9e290 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToOneMethodHandler.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToOneMethodHandler.java @@ -18,7 +18,7 @@ import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.reactive.calls.ReactorServerCalls; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.stub.StubMethodHandler; import java.util.concurrent.CompletableFuture; diff --git a/dubbo-plugin/dubbo-reactive/src/test/java/org/apache/dubbo/reactive/CreateObserverAdapter.java b/dubbo-plugin/dubbo-reactive/src/test/java/org/apache/dubbo/reactive/CreateObserverAdapter.java index 3f303b5f3f53..fd6a76a01789 100644 --- a/dubbo-plugin/dubbo-reactive/src/test/java/org/apache/dubbo/reactive/CreateObserverAdapter.java +++ b/dubbo-plugin/dubbo-reactive/src/test/java/org/apache/dubbo/reactive/CreateObserverAdapter.java @@ -16,7 +16,7 @@ */ package org.apache.dubbo.reactive; -import org.apache.dubbo.rpc.protocol.tri.ServerStreamObserver; +import org.apache.dubbo.common.stream.ServerCallStreamObserver; import java.util.concurrent.atomic.AtomicInteger; @@ -28,7 +28,7 @@ public class CreateObserverAdapter { - private ServerStreamObserver responseObserver; + private ServerCallStreamObserver responseObserver; private AtomicInteger nextCounter; private AtomicInteger completeCounter; private AtomicInteger errorCounter; @@ -39,7 +39,7 @@ public class CreateObserverAdapter { completeCounter = new AtomicInteger(); errorCounter = new AtomicInteger(); - responseObserver = Mockito.mock(ServerStreamObserver.class); + responseObserver = Mockito.mock(ServerCallStreamObserver.class); doAnswer(o -> nextCounter.incrementAndGet()).when(responseObserver).onNext(anyString()); doAnswer(o -> completeCounter.incrementAndGet()).when(responseObserver).onCompleted(); doAnswer(o -> errorCounter.incrementAndGet()).when(responseObserver).onError(any(Throwable.class)); @@ -57,7 +57,7 @@ public AtomicInteger getErrorCounter() { return errorCounter; } - public ServerStreamObserver getResponseObserver() { + public ServerCallStreamObserver getResponseObserver() { return this.responseObserver; } } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/CancelableStreamObserver.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/CancelableStreamObserver.java index 6b1548b6e8a4..1ace07da9ec2 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/CancelableStreamObserver.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/CancelableStreamObserver.java @@ -16,11 +16,11 @@ */ package org.apache.dubbo.rpc.protocol.tri; +import org.apache.dubbo.common.stream.ClientCallStreamObserver; import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.rpc.CancellationContext; -import org.apache.dubbo.rpc.protocol.tri.observer.ClientCallToObserverAdapter; -public abstract class CancelableStreamObserver implements StreamObserver { +public abstract class CancelableStreamObserver implements StreamObserver { private CancellationContext cancellationContext; @@ -36,7 +36,7 @@ public void cancel(Throwable throwable) { cancellationContext.cancel(throwable); } - public void beforeStart(ClientCallToObserverAdapter clientCallToObserverAdapter) { + public void beforeStart(ClientCallStreamObserver clientCallStreamObserver) { // do nothing } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ClientStreamObserver.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ClientStreamObserver.java index 180fc9bf9a44..1c3a8a3aeec8 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ClientStreamObserver.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ClientStreamObserver.java @@ -16,10 +16,15 @@ */ package org.apache.dubbo.rpc.protocol.tri; +import org.apache.dubbo.common.stream.ClientCallStreamObserver; import org.apache.dubbo.common.stream.StreamObserver; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; -public interface ClientStreamObserver extends CallStreamObserver { +/** + * @param + * @deprecated use {@link ClientCallStreamObserver} + */ +@Deprecated +public interface ClientStreamObserver extends ClientCallStreamObserver { /** * Swaps to manual flow control where no message will be delivered to {@link diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ServerStreamObserver.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ServerStreamObserver.java index 6dad6aaec490..c0ce69b0f72c 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ServerStreamObserver.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ServerStreamObserver.java @@ -16,6 +16,7 @@ */ package org.apache.dubbo.rpc.protocol.tri; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.ServerCallStreamObserver; -public interface ServerStreamObserver extends CallStreamObserver {} +@Deprecated +public interface ServerStreamObserver extends ServerCallStreamObserver {} diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/Http2ServerStreamObserver.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/Http2ServerStreamObserver.java index ea959f26346f..4894dff07a9f 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/Http2ServerStreamObserver.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/h12/http2/Http2ServerStreamObserver.java @@ -16,6 +16,7 @@ */ package org.apache.dubbo.rpc.protocol.tri.h12.http2; +import org.apache.dubbo.common.stream.ServerCallStreamObserver; import org.apache.dubbo.remoting.http12.HttpHeaders; import org.apache.dubbo.remoting.http12.HttpMetadata; import org.apache.dubbo.remoting.http12.h2.H2StreamChannel; @@ -31,7 +32,7 @@ import java.util.Map; public class Http2ServerStreamObserver extends Http2ServerChannelObserver - implements ServerStreamObserver, AttachmentHolder { + implements ServerStreamObserver, ServerCallStreamObserver, AttachmentHolder { private final FrameworkModel frameworkModel; diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java index f832677ecc36..6dc692d1f34d 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/observer/ClientCallToObserverAdapter.java @@ -16,11 +16,13 @@ */ package org.apache.dubbo.rpc.protocol.tri.observer; +import org.apache.dubbo.common.stream.ClientCallStreamObserver; import org.apache.dubbo.rpc.protocol.tri.CancelableStreamObserver; import org.apache.dubbo.rpc.protocol.tri.ClientStreamObserver; import org.apache.dubbo.rpc.protocol.tri.call.ClientCall; -public class ClientCallToObserverAdapter extends CancelableStreamObserver implements ClientStreamObserver { +public class ClientCallToObserverAdapter extends CancelableStreamObserver + implements ClientStreamObserver, ClientCallStreamObserver { private final ClientCall call; private final boolean streamingResponse; From edf213234df0a0052c484e0432bf941f652c3750 Mon Sep 17 00:00:00 2001 From: earthchen Date: Mon, 5 Jan 2026 22:24:44 +0800 Subject: [PATCH 20/30] add license --- .../common/stream/ClientCallStreamObserver.java | 16 ++++++++++++++++ .../common/stream/ServerCallStreamObserver.java | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java index b0ab8a3e29d6..7385b5d2dbac 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java @@ -1,3 +1,19 @@ +/* + * 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. + */ package org.apache.dubbo.common.stream; public interface ClientCallStreamObserver extends CallStreamObserver { diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java index ca18b08aecfd..ffedad2ac05c 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java @@ -1,3 +1,19 @@ +/* + * 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. + */ package org.apache.dubbo.common.stream; public interface ServerCallStreamObserver extends CallStreamObserver {} From b213009262d0630a93fad4ee770e169bef000f98 Mon Sep 17 00:00:00 2001 From: earthchen Date: Mon, 5 Jan 2026 22:28:49 +0800 Subject: [PATCH 21/30] mvn spotless:apply --- .../apache/dubbo/common/stream/ClientCallStreamObserver.java | 2 -- .../apache/dubbo/reactive/AbstractTripleReactorPublisher.java | 2 +- .../apache/dubbo/reactive/ServerTripleReactorSubscriber.java | 2 +- .../org/apache/dubbo/reactive/calls/ReactorClientCalls.java | 2 +- .../org/apache/dubbo/reactive/calls/ReactorServerCalls.java | 2 +- .../dubbo/reactive/handler/ManyToManyMethodHandler.java | 2 +- .../apache/dubbo/reactive/handler/ManyToOneMethodHandler.java | 2 +- .../apache/dubbo/rpc/protocol/tri/ServerStreamObserver.java | 4 ++++ 8 files changed, 10 insertions(+), 8 deletions(-) diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java index 7385b5d2dbac..859f67495708 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java @@ -18,7 +18,5 @@ public interface ClientCallStreamObserver extends CallStreamObserver { - void disableAutoRequestWithInitial(int request); - } diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorPublisher.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorPublisher.java index 34a55f36cd8a..dfe0d140c603 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorPublisher.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorPublisher.java @@ -16,8 +16,8 @@ */ package org.apache.dubbo.reactive; -import org.apache.dubbo.rpc.protocol.tri.CancelableStreamObserver; import org.apache.dubbo.common.stream.CallStreamObserver; +import org.apache.dubbo.rpc.protocol.tri.CancelableStreamObserver; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ServerTripleReactorSubscriber.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ServerTripleReactorSubscriber.java index a1a992715c50..fdb8945bfea6 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ServerTripleReactorSubscriber.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ServerTripleReactorSubscriber.java @@ -16,9 +16,9 @@ */ package org.apache.dubbo.reactive; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.CancellationContext; import org.apache.dubbo.rpc.protocol.tri.CancelableStreamObserver; -import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.ArrayList; import java.util.List; diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorClientCalls.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorClientCalls.java index 14d27991af6e..243adc63ff5b 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorClientCalls.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorClientCalls.java @@ -16,12 +16,12 @@ */ package org.apache.dubbo.reactive.calls; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.reactive.ClientTripleReactorPublisher; import org.apache.dubbo.reactive.ClientTripleReactorSubscriber; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.model.StubMethodDescriptor; -import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.stub.StubInvocationUtil; import reactor.core.publisher.Flux; diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorServerCalls.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorServerCalls.java index c2b829e51d0a..eb65e40f5dff 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorServerCalls.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/calls/ReactorServerCalls.java @@ -16,12 +16,12 @@ */ package org.apache.dubbo.reactive.calls; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.reactive.ServerTripleReactorPublisher; import org.apache.dubbo.reactive.ServerTripleReactorSubscriber; import org.apache.dubbo.rpc.StatusRpcException; import org.apache.dubbo.rpc.TriRpcStatus; -import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.List; import java.util.concurrent.CompletableFuture; diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToManyMethodHandler.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToManyMethodHandler.java index 58a60d57c58a..87fe23fb5cd6 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToManyMethodHandler.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToManyMethodHandler.java @@ -16,9 +16,9 @@ */ package org.apache.dubbo.reactive.handler; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.reactive.calls.ReactorServerCalls; -import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.stub.StubMethodHandler; import java.util.concurrent.CompletableFuture; diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToOneMethodHandler.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToOneMethodHandler.java index a5ff39a9e290..441717d13856 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToOneMethodHandler.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/handler/ManyToOneMethodHandler.java @@ -16,9 +16,9 @@ */ package org.apache.dubbo.reactive.handler; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.reactive.calls.ReactorServerCalls; -import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.stub.StubMethodHandler; import java.util.concurrent.CompletableFuture; diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ServerStreamObserver.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ServerStreamObserver.java index c0ce69b0f72c..7911bbbb8cf9 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ServerStreamObserver.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/ServerStreamObserver.java @@ -18,5 +18,9 @@ import org.apache.dubbo.common.stream.ServerCallStreamObserver; +/** + * @deprecated use {@link ServerCallStreamObserver} instead + * @param + */ @Deprecated public interface ServerStreamObserver extends ServerCallStreamObserver {} From 52f8316f52481a75b5161a71365c3d1fcfe8fda3 Mon Sep 17 00:00:00 2001 From: earthchen Date: Mon, 5 Jan 2026 22:40:41 +0800 Subject: [PATCH 22/30] fix dubbo plugin --- .../apache/dubbo/mutiny/AbstractTripleMutinyPublisher.java | 4 ++-- .../apache/dubbo/mutiny/AbstractTripleMutinySubscriber.java | 2 +- .../org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java | 2 +- .../org/apache/dubbo/mutiny/ServerTripleMutinyPublisher.java | 2 +- .../org/apache/dubbo/mutiny/ServerTripleMutinySubscriber.java | 2 +- .../java/org/apache/dubbo/mutiny/calls/MutinyClientCalls.java | 2 +- .../java/org/apache/dubbo/mutiny/calls/MutinyServerCalls.java | 2 +- .../apache/dubbo/mutiny/handler/ManyToManyMethodHandler.java | 2 +- .../apache/dubbo/mutiny/handler/ManyToOneMethodHandler.java | 2 +- .../java/org/apache/dubbo/mutiny/MutinyClientCallsTest.java | 2 +- .../java/org/apache/dubbo/mutiny/MutinyServerCallsTest.java | 2 +- .../org/apache/dubbo/mutiny/TripleMutinyPublisherTest.java | 2 +- .../org/apache/dubbo/mutiny/TripleMutinySubscriberTest.java | 2 +- .../dubbo/rpc/protocol/tri/stream/TripleClientStreamTest.java | 2 ++ 14 files changed, 16 insertions(+), 14 deletions(-) diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/AbstractTripleMutinyPublisher.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/AbstractTripleMutinyPublisher.java index 5ca8676a1b3f..c2a7ab95654f 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/AbstractTripleMutinyPublisher.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/AbstractTripleMutinyPublisher.java @@ -17,14 +17,14 @@ package org.apache.dubbo.mutiny; import org.apache.dubbo.rpc.protocol.tri.CancelableStreamObserver; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; /** - * The middle layer between {@link org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver} and Mutiny API.

+ * The middle layer between {@link org.apache.dubbo.common.stream.CallStreamObserver} and Mutiny API.

* 1. passing the data received by CallStreamObserver to Mutiny consumer
* 2. passing the request of Mutiny API to CallStreamObserver */ diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/AbstractTripleMutinySubscriber.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/AbstractTripleMutinySubscriber.java index 643d84e32bcb..c19ee45d6e2b 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/AbstractTripleMutinySubscriber.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/AbstractTripleMutinySubscriber.java @@ -16,7 +16,7 @@ */ package org.apache.dubbo.mutiny; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicBoolean; diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java index 20fbe4aac76b..c8788ddc2a05 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java @@ -16,7 +16,7 @@ */ package org.apache.dubbo.mutiny; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.protocol.tri.observer.ClientCallToObserverAdapter; import java.util.function.Consumer; diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ServerTripleMutinyPublisher.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ServerTripleMutinyPublisher.java index eb126492fff2..180f193e66cd 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ServerTripleMutinyPublisher.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ServerTripleMutinyPublisher.java @@ -16,7 +16,7 @@ */ package org.apache.dubbo.mutiny; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; /** * Used in ManyToOne and ManyToMany in server.
diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ServerTripleMutinySubscriber.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ServerTripleMutinySubscriber.java index 50963db3a181..94a6f71197d9 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ServerTripleMutinySubscriber.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ServerTripleMutinySubscriber.java @@ -18,7 +18,7 @@ import org.apache.dubbo.rpc.CancellationContext; import org.apache.dubbo.rpc.protocol.tri.CancelableStreamObserver; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.ArrayList; import java.util.List; diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyClientCalls.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyClientCalls.java index 296017ebc9f9..b15fda16ea03 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyClientCalls.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyClientCalls.java @@ -21,7 +21,7 @@ import org.apache.dubbo.mutiny.ClientTripleMutinySubscriber; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.model.StubMethodDescriptor; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.stub.StubInvocationUtil; import io.smallrye.mutiny.Multi; diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyServerCalls.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyServerCalls.java index 5364b26c3f5b..f93971e0334a 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyServerCalls.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyServerCalls.java @@ -21,7 +21,7 @@ import org.apache.dubbo.mutiny.ServerTripleMutinySubscriber; import org.apache.dubbo.rpc.StatusRpcException; import org.apache.dubbo.rpc.TriRpcStatus; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.List; import java.util.concurrent.CompletableFuture; diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToManyMethodHandler.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToManyMethodHandler.java index 8915c2509259..5dafb4e60c76 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToManyMethodHandler.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToManyMethodHandler.java @@ -18,7 +18,7 @@ import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.mutiny.calls.MutinyServerCalls; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.stub.StubMethodHandler; import java.util.concurrent.CompletableFuture; diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToOneMethodHandler.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToOneMethodHandler.java index 634f47ccfbee..88c8f0f44992 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToOneMethodHandler.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToOneMethodHandler.java @@ -18,7 +18,7 @@ import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.mutiny.calls.MutinyServerCalls; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.stub.StubMethodHandler; import java.util.concurrent.CompletableFuture; diff --git a/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyClientCallsTest.java b/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyClientCallsTest.java index e4e840790a0f..5f44cdce5f8e 100644 --- a/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyClientCallsTest.java +++ b/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyClientCallsTest.java @@ -20,7 +20,7 @@ import org.apache.dubbo.mutiny.calls.MutinyClientCalls; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.model.StubMethodDescriptor; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.stub.StubInvocationUtil; import java.time.Duration; diff --git a/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyServerCallsTest.java b/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyServerCallsTest.java index 58bb7b115c25..3125b6f9e620 100644 --- a/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyServerCallsTest.java +++ b/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyServerCallsTest.java @@ -18,7 +18,7 @@ import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.mutiny.calls.MutinyServerCalls; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.List; import java.util.concurrent.CompletableFuture; diff --git a/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/TripleMutinyPublisherTest.java b/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/TripleMutinyPublisherTest.java index d74726b7201c..8d35d71e9387 100644 --- a/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/TripleMutinyPublisherTest.java +++ b/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/TripleMutinyPublisherTest.java @@ -16,7 +16,7 @@ */ package org.apache.dubbo.mutiny; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.ArrayList; import java.util.List; diff --git a/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/TripleMutinySubscriberTest.java b/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/TripleMutinySubscriberTest.java index 76402ad9d9b2..1c5cf3093c3c 100644 --- a/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/TripleMutinySubscriberTest.java +++ b/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/TripleMutinySubscriberTest.java @@ -16,7 +16,7 @@ */ package org.apache.dubbo.mutiny; -import org.apache.dubbo.rpc.protocol.tri.observer.CallStreamObserver; +import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.concurrent.Flow; diff --git a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/stream/TripleClientStreamTest.java b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/stream/TripleClientStreamTest.java index fa71ca1fa9bc..d60cd68f34a9 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/stream/TripleClientStreamTest.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/stream/TripleClientStreamTest.java @@ -32,6 +32,7 @@ import org.apache.dubbo.rpc.protocol.tri.command.DataQueueCommand; import org.apache.dubbo.rpc.protocol.tri.command.EndStreamQueueCommand; import org.apache.dubbo.rpc.protocol.tri.command.HeaderQueueCommand; +import org.apache.dubbo.rpc.protocol.tri.command.InitOnReadyQueueCommand; import org.apache.dubbo.rpc.protocol.tri.command.QueuedCommand; import org.apache.dubbo.rpc.protocol.tri.compressor.Compressor; import org.apache.dubbo.rpc.protocol.tri.h12.http2.Http2TripleClientStream; @@ -88,6 +89,7 @@ void progress() { listener, http2StreamChannel); verify(writeQueue).enqueue(any(CreateStreamQueueCommand.class)); + verify(writeQueue).enqueue(any(InitOnReadyQueueCommand.class)); final RequestMetadata requestMetadata = new RequestMetadata(); requestMetadata.method = methodDescriptor; From 686e5be20b26bf64d7551d89833783278f08a1ef Mon Sep 17 00:00:00 2001 From: earthchen Date: Mon, 5 Jan 2026 22:49:16 +0800 Subject: [PATCH 23/30] fix dubbo plugin spotless --- .../org/apache/dubbo/mutiny/AbstractTripleMutinyPublisher.java | 2 +- .../org/apache/dubbo/mutiny/ServerTripleMutinySubscriber.java | 2 +- .../java/org/apache/dubbo/mutiny/calls/MutinyClientCalls.java | 2 +- .../java/org/apache/dubbo/mutiny/calls/MutinyServerCalls.java | 2 +- .../apache/dubbo/mutiny/handler/ManyToManyMethodHandler.java | 2 +- .../org/apache/dubbo/mutiny/handler/ManyToOneMethodHandler.java | 2 +- .../java/org/apache/dubbo/mutiny/MutinyClientCallsTest.java | 2 +- .../java/org/apache/dubbo/mutiny/MutinyServerCallsTest.java | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/AbstractTripleMutinyPublisher.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/AbstractTripleMutinyPublisher.java index c2a7ab95654f..c3465bc9a82e 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/AbstractTripleMutinyPublisher.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/AbstractTripleMutinyPublisher.java @@ -16,8 +16,8 @@ */ package org.apache.dubbo.mutiny; -import org.apache.dubbo.rpc.protocol.tri.CancelableStreamObserver; import org.apache.dubbo.common.stream.CallStreamObserver; +import org.apache.dubbo.rpc.protocol.tri.CancelableStreamObserver; import java.util.concurrent.Flow; import java.util.concurrent.atomic.AtomicBoolean; diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ServerTripleMutinySubscriber.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ServerTripleMutinySubscriber.java index 94a6f71197d9..7e0f63eda281 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ServerTripleMutinySubscriber.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ServerTripleMutinySubscriber.java @@ -16,9 +16,9 @@ */ package org.apache.dubbo.mutiny; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.CancellationContext; import org.apache.dubbo.rpc.protocol.tri.CancelableStreamObserver; -import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.ArrayList; import java.util.List; diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyClientCalls.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyClientCalls.java index b15fda16ea03..c0da4124fbc9 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyClientCalls.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyClientCalls.java @@ -16,12 +16,12 @@ */ package org.apache.dubbo.mutiny.calls; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.mutiny.ClientTripleMutinyPublisher; import org.apache.dubbo.mutiny.ClientTripleMutinySubscriber; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.model.StubMethodDescriptor; -import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.stub.StubInvocationUtil; import io.smallrye.mutiny.Multi; diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyServerCalls.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyServerCalls.java index f93971e0334a..a64349a8bfc1 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyServerCalls.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/calls/MutinyServerCalls.java @@ -16,12 +16,12 @@ */ package org.apache.dubbo.mutiny.calls; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.mutiny.ServerTripleMutinyPublisher; import org.apache.dubbo.mutiny.ServerTripleMutinySubscriber; import org.apache.dubbo.rpc.StatusRpcException; import org.apache.dubbo.rpc.TriRpcStatus; -import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.List; import java.util.concurrent.CompletableFuture; diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToManyMethodHandler.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToManyMethodHandler.java index 5dafb4e60c76..a355cf762cb0 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToManyMethodHandler.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToManyMethodHandler.java @@ -16,9 +16,9 @@ */ package org.apache.dubbo.mutiny.handler; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.mutiny.calls.MutinyServerCalls; -import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.stub.StubMethodHandler; import java.util.concurrent.CompletableFuture; diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToOneMethodHandler.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToOneMethodHandler.java index 88c8f0f44992..33fd72c76eec 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToOneMethodHandler.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/handler/ManyToOneMethodHandler.java @@ -16,9 +16,9 @@ */ package org.apache.dubbo.mutiny.handler; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.mutiny.calls.MutinyServerCalls; -import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.stub.StubMethodHandler; import java.util.concurrent.CompletableFuture; diff --git a/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyClientCallsTest.java b/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyClientCallsTest.java index 5f44cdce5f8e..041bd1cb7c68 100644 --- a/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyClientCallsTest.java +++ b/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyClientCallsTest.java @@ -16,11 +16,11 @@ */ package org.apache.dubbo.mutiny; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.mutiny.calls.MutinyClientCalls; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.model.StubMethodDescriptor; -import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.rpc.stub.StubInvocationUtil; import java.time.Duration; diff --git a/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyServerCallsTest.java b/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyServerCallsTest.java index 3125b6f9e620..1bfb0de8dd54 100644 --- a/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyServerCallsTest.java +++ b/dubbo-plugin/dubbo-mutiny/src/test/java/org/apache/dubbo/mutiny/MutinyServerCallsTest.java @@ -16,9 +16,9 @@ */ package org.apache.dubbo.mutiny; +import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.mutiny.calls.MutinyServerCalls; -import org.apache.dubbo.common.stream.CallStreamObserver; import java.util.List; import java.util.concurrent.CompletableFuture; From b4bd5d6471abe6a6daae4bd9f50d38050e3b58d3 Mon Sep 17 00:00:00 2001 From: earthchen Date: Mon, 5 Jan 2026 22:55:47 +0800 Subject: [PATCH 24/30] fix --- .../org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java index c8788ddc2a05..bd765521eb73 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java @@ -17,7 +17,7 @@ package org.apache.dubbo.mutiny; import org.apache.dubbo.common.stream.CallStreamObserver; -import org.apache.dubbo.rpc.protocol.tri.observer.ClientCallToObserverAdapter; +import org.apache.dubbo.common.stream.ClientCallStreamObserver; import java.util.function.Consumer; @@ -36,7 +36,7 @@ public ClientTripleMutinyPublisher(Consumer> onSubscribe, } @Override - public void beforeStart(ClientCallToObserverAdapter clientCallToObserverAdapter) { + public void beforeStart(ClientCallStreamObserver clientCallToObserverAdapter) { super.onSubscribe(clientCallToObserverAdapter); } } From d2ae97dee0837de517fd56ae5ae962ebf0a39ac8 Mon Sep 17 00:00:00 2001 From: earthchen Date: Tue, 6 Jan 2026 10:55:52 +0800 Subject: [PATCH 25/30] fix ut --- .../rpc/protocol/tri/stream/TripleClientStreamTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/stream/TripleClientStreamTest.java b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/stream/TripleClientStreamTest.java index d60cd68f34a9..c8722e4072ec 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/stream/TripleClientStreamTest.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/test/java/org/apache/dubbo/rpc/protocol/tri/stream/TripleClientStreamTest.java @@ -102,11 +102,13 @@ void progress() { requestMetadata.version = url.getVersion(); stream.sendHeader(requestMetadata.toHeaders()); verify(writeQueue).enqueueFuture(any(HeaderQueueCommand.class), any(Executor.class)); - // no other commands - verify(writeQueue).enqueue(any(QueuedCommand.class)); + // enqueue should have been called twice: CreateStreamQueueCommand and InitOnReadyQueueCommand + verify(writeQueue, times(2)).enqueue(any(QueuedCommand.class)); stream.sendMessage(new byte[0], 0); verify(writeQueue).enqueueFuture(any(DataQueueCommand.class), any(Executor.class)); verify(writeQueue, times(2)).enqueueFuture(any(QueuedCommand.class), any(Executor.class)); + // After sendHeader and sendMessage, enqueue should have been called twice: + // once for CreateStreamQueueCommand and once for InitOnReadyQueueCommand stream.halfClose(); verify(writeQueue).enqueueFuture(any(EndStreamQueueCommand.class), any(Executor.class)); verify(writeQueue, times(3)).enqueueFuture(any(QueuedCommand.class), any(Executor.class)); From fe3831604c2c7e4ec518d7ce704cdd1171499cae Mon Sep 17 00:00:00 2001 From: earthchen Date: Tue, 6 Jan 2026 12:56:28 +0800 Subject: [PATCH 26/30] Add gRPC-compatible APIs and fix the integration tests --- .../common/stream/CallStreamObserver.java | 52 +++++++++-- .../stream/ClientCallStreamObserver.java | 79 +++++++++++++++- .../common/stream/ClientResponseObserver.java | 91 +++++++++++++++++++ .../stream/ServerCallStreamObserver.java | 63 ++++++++++++- .../AbstractTripleReactorPublisher.java | 17 +++- .../ClientTripleReactorPublisher.java | 19 +++- .../http12/h2/Http2ServerChannelObserver.java | 9 +- .../tri/CancelableStreamObserver.java | 6 +- .../dubbo/rpc/protocol/tri/TripleInvoker.java | 28 +++++- 9 files changed, 343 insertions(+), 21 deletions(-) create mode 100644 dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientResponseObserver.java diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java index 74101082576d..48b8bcad17db 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java @@ -18,17 +18,53 @@ /** * An extension of {@link StreamObserver} that provides additional functionality for flow control - * and backpressure. This interface mirrors gRPC's {@code CallStreamObserver} for compatibility. + * and backpressure. This interface mirrors gRPC's {@code io.grpc.stub.CallStreamObserver} for compatibility. * - *

Key features: + *

This is the base interface for both client-side ({@link ClientCallStreamObserver}) and + * server-side ({@link ServerCallStreamObserver}) flow control observers. It provides two types + * of flow control: + * + *

Send-Side Backpressure (Outbound Flow Control)

+ *

Controls the rate at which data is sent to avoid overwhelming the receiver: + *

    + *
  • {@link #isReady()} - Check if the stream can accept more data without blocking
  • + *
  • {@link #setOnReadyHandler(Runnable)} - Register a callback for when the stream becomes writable
  • + *
+ * + *

Receive-Side Backpressure (Inbound Flow Control)

+ *

Controls the rate at which data is received to avoid being overwhelmed: *

    - *
  • {@link #isReady()} - Check if the stream is ready to accept more messages
  • - *
  • {@link #setOnReadyHandler(Runnable)} - Set a callback for when the stream becomes ready
  • - *
  • {@link #request(int)} - Request more messages from the peer (for inbound flow control)
  • - *
  • {@link #disableAutoFlowControl()} - Switch to manual flow control mode
  • + *
  • {@link #disableAutoFlowControl()} - Switch from automatic to manual message requesting
  • + *
  • {@link #request(int)} - Explicitly request a specific number of messages from the sender
  • *
* + *

Typical Usage Pattern

+ *
{@code
+ * // Send-side backpressure example
+ * callStreamObserver.setOnReadyHandler(() -> {
+ *     while (callStreamObserver.isReady() && hasMoreData()) {
+ *         callStreamObserver.onNext(getNextData());
+ *     }
+ *     if (!hasMoreData()) {
+ *         callStreamObserver.onCompleted();
+ *     }
+ * });
+ *
+ * // Receive-side backpressure example (in beforeStart or similar)
+ * callStreamObserver.disableAutoFlowControl();
+ * callStreamObserver.request(10); // Request initial batch
+ *
+ * // Then in onNext()
+ * public void onNext(T value) {
+ *     process(value);
+ *     callStreamObserver.request(1); // Request next message after processing
+ * }
+ * }
+ * * @param the type of value passed to the stream + * @see ClientCallStreamObserver + * @see ServerCallStreamObserver + * @see ClientResponseObserver */ public interface CallStreamObserver extends StreamObserver { @@ -64,7 +100,7 @@ public interface CallStreamObserver extends StreamObserver { * * @param onReadyHandler the handler to invoke when the stream becomes ready */ - void setOnReadyHandler(Runnable onReadyHandler); + default void setOnReadyHandler(Runnable onReadyHandler) {} /** * Requests the peer to produce {@code count} more messages to be delivered to the 'inbound' @@ -82,7 +118,7 @@ public interface CallStreamObserver extends StreamObserver { * For stream set compression needs to determine whether the metadata has been sent, and carry * on corresponding processing */ - void setCompression(String compression); + default void setCompression(String compression) {} /** * Swaps to manual flow control where no message will be delivered to {@link diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java index 859f67495708..b19dff44a137 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientCallStreamObserver.java @@ -16,7 +16,84 @@ */ package org.apache.dubbo.common.stream; -public interface ClientCallStreamObserver extends CallStreamObserver { +/** + * A client-side extension of {@link CallStreamObserver} that provides additional functionality + * for controlling the outbound request stream. This interface mirrors gRPC's + * {@code io.grpc.stub.ClientCallStreamObserver} for compatibility. + * + *

This interface is typically obtained through {@link ClientResponseObserver#beforeStart(ClientCallStreamObserver)} + * and provides methods for: + *

    + *
  • Controlling send-side backpressure via {@link #isReady()} and {@link #setOnReadyHandler(Runnable)}
  • + *
  • Controlling receive-side backpressure via {@link #disableAutoRequestWithInitial(int)} and {@link #request(int)}
  • + *
+ * + *

Send-Side Backpressure (Controlling Outgoing Data Rate)

+ *
{@code
+ * @Override
+ * public void beforeStart(ClientCallStreamObserver requestStream) {
+ *     requestStream.disableAutoFlowControl();
+ *     requestStream.setOnReadyHandler(() -> {
+ *         while (requestStream.isReady() && hasMoreData()) {
+ *             requestStream.onNext(getNextRequest());
+ *         }
+ *     });
+ * }
+ * }
+ * + *

Receive-Side Backpressure (Controlling Incoming Data Rate)

+ *
{@code
+ * @Override
+ * public void beforeStart(ClientCallStreamObserver requestStream) {
+ *     // Request only 10 messages initially
+ *     requestStream.disableAutoRequestWithInitial(10);
+ * }
+ *
+ * @Override
+ * public void onNext(Response response) {
+ *     process(response);
+ *     // Request more after processing
+ *     requestStream.request(1);
+ * }
+ * }
+ * + * @param the type of messages sent to the server (request type) + * @see CallStreamObserver + * @see ClientResponseObserver + */ +public interface ClientCallStreamObserver extends CallStreamObserver { + /** + * Disables automatic inbound flow control and sets the initial number of messages + * to request from the server. + * + *

By default, the runtime automatically requests messages from the server as they + * are consumed. Calling this method switches to manual flow control mode, where the + * client must explicitly call {@link #request(int)} to receive more messages. + * + *

This method must be called within + * {@link ClientResponseObserver#beforeStart(ClientCallStreamObserver)} before the + * stream starts, otherwise it has no effect. + * + *

Usage: + *

{@code
+     * @Override
+     * public void beforeStart(ClientCallStreamObserver requestStream) {
+     *     // Start with 5 messages, then request more in onNext()
+     *     requestStream.disableAutoRequestWithInitial(5);
+     * }
+     *
+     * @Override
+     * public void onNext(Response response) {
+     *     process(response);
+     *     requestStream.request(1); // Request one more message
+     * }
+     * }
+ * + * @param request the initial number of messages to request from the server. + * A value of 0 means no messages will be delivered until {@link #request(int)} is called. + * @see #request(int) + * @see #disableAutoFlowControl() + */ void disableAutoRequestWithInitial(int request); } diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientResponseObserver.java b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientResponseObserver.java new file mode 100644 index 000000000000..1c6c3e7d376b --- /dev/null +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ClientResponseObserver.java @@ -0,0 +1,91 @@ +/* + * 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. + */ +package org.apache.dubbo.common.stream; + +/** + * A client-side {@link StreamObserver} that provides a callback to receive a reference to the + * outbound request stream observer before the call starts. This interface mirrors gRPC's + * {@code io.grpc.stub.ClientResponseObserver} for compatibility. + * + *

This interface is used for advanced flow control scenarios where the client needs to: + *

    + *
  • Configure flow control settings before the stream starts
  • + *
  • Set up an {@link CallStreamObserver#setOnReadyHandler(Runnable) onReadyHandler} for send-side backpressure
  • + *
  • Control the rate of receiving messages using {@link ClientCallStreamObserver#disableAutoRequestWithInitial(int)}
  • + *
+ * + *

Example Usage

+ *
{@code
+ * // Client streaming with backpressure
+ * ClientResponseObserver responseObserver =
+ *         new ClientResponseObserver() {
+ *     @Override
+ *     public void beforeStart(ClientCallStreamObserver requestStream) {
+ *         // Disable auto flow control for manual send control
+ *         requestStream.disableAutoFlowControl();
+ *
+ *         // Set up onReadyHandler for send-side backpressure
+ *         requestStream.setOnReadyHandler(() -> {
+ *             while (requestStream.isReady() && hasMoreData()) {
+ *                 requestStream.onNext(getNextChunk());
+ *             }
+ *         });
+ *     }
+ *
+ *     @Override
+ *     public void onNext(Response response) { ... }
+ *
+ *     @Override
+ *     public void onError(Throwable t) { ... }
+ *
+ *     @Override
+ *     public void onCompleted() { ... }
+ * };
+ *
+ * service.clientStream(responseObserver);
+ * }
+ * + * @param the type of messages sent to the server (request type) + * @param the type of messages received from the server (response type) + * @see ClientCallStreamObserver + * @see CallStreamObserver + */ +public interface ClientResponseObserver extends StreamObserver { + + /** + * Called by the runtime prior to the start of a call to provide a reference to the + * {@link ClientCallStreamObserver} for the outbound request stream. + * + *

This callback is invoked before the underlying stream is created, + * allowing the client to configure flow control settings that take effect from the + * beginning of the call. + * + *

Allowed operations in this callback: + *

    + *
  • {@link ClientCallStreamObserver#setOnReadyHandler(Runnable)} - Set handler for send-side backpressure
  • + *
  • {@link ClientCallStreamObserver#disableAutoRequestWithInitial(int)} - Configure receive-side backpressure
  • + *
  • {@link CallStreamObserver#disableAutoFlowControl()} - Disable automatic flow control
  • + *
+ * + *

Note: Do not call {@link StreamObserver#onNext(Object)} or + * {@link StreamObserver#onCompleted()} within this callback. Data should only be sent + * after the stream is ready (via the {@code onReadyHandler}). + * + * @param requestStream the {@link ClientCallStreamObserver} for sending requests to the server + */ + void beforeStart(final ClientCallStreamObserver requestStream); +} diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java index ffedad2ac05c..9e2f7321ed3a 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java @@ -16,4 +16,65 @@ */ package org.apache.dubbo.common.stream; -public interface ServerCallStreamObserver extends CallStreamObserver {} +/** + * A server-side extension of {@link CallStreamObserver} that provides flow control capabilities + * for outbound response streams. This interface mirrors gRPC's + * {@code io.grpc.stub.ServerCallStreamObserver} for compatibility. + * + *

On the server side, this interface is obtained by casting the {@link StreamObserver} + * parameter passed to streaming RPC methods. It allows the server to: + *

    + *
  • Check if the response stream is ready using {@link #isReady()}
  • + *
  • Set a callback for when the stream becomes ready using {@link #setOnReadyHandler(Runnable)}
  • + *
  • Control inbound flow from the client using {@link #request(int)} and {@link #disableAutoFlowControl()}
  • + *
+ * + *

Server-Side Send Backpressure Example

+ *
{@code
+ * @Override
+ * public void serverStream(Request request, StreamObserver responseObserver) {
+ *     ServerCallStreamObserver serverObserver =
+ *             (ServerCallStreamObserver) responseObserver;
+ *
+ *     AtomicInteger sent = new AtomicInteger(0);
+ *     int totalCount = 100;
+ *
+ *     serverObserver.setOnReadyHandler(() -> {
+ *         while (serverObserver.isReady() && sent.get() < totalCount) {
+ *             int seq = sent.getAndIncrement();
+ *             serverObserver.onNext(createResponse(seq));
+ *         }
+ *         if (sent.get() >= totalCount) {
+ *             serverObserver.onCompleted();
+ *         }
+ *     });
+ * }
+ * }
+ * + *

Server-Side Receive Backpressure Example (for Client/Bidi Streaming)

+ *
{@code
+ * @Override
+ * public StreamObserver clientStream(StreamObserver responseObserver) {
+ *     ServerCallStreamObserver serverObserver =
+ *             (ServerCallStreamObserver) responseObserver;
+ *
+ *     // Control how many messages we receive from the client
+ *     serverObserver.disableAutoFlowControl();
+ *     serverObserver.request(5); // Start with 5 messages
+ *
+ *     return new StreamObserver() {
+ *         @Override
+ *         public void onNext(Request request) {
+ *             process(request);
+ *             serverObserver.request(1); // Request one more
+ *         }
+ *         // ... onError, onCompleted
+ *     };
+ * }
+ * }
+ * + * @param the type of messages sent to the client (response type) + * @see CallStreamObserver + * @see StreamObserver + */ +public interface ServerCallStreamObserver extends CallStreamObserver {} diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorPublisher.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorPublisher.java index dfe0d140c603..3940f7d05cef 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorPublisher.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/AbstractTripleReactorPublisher.java @@ -71,8 +71,20 @@ protected void onSubscribe(final CallStreamObserver subscription) { if (subscription != null && this.subscription == null && HAS_SUBSCRIPTION.compareAndSet(false, true)) { this.subscription = subscription; subscription.disableAutoFlowControl(); + + // Set up onReadyHandler to trigger onSubscribe callback when stream becomes ready. + // This is called AFTER call.start() via InitOnReadyQueueCommand, ensuring the stream + // is created before any data is sent + // is triggered by onReady, not by onStart (which requires server headers). if (onSubscribe != null) { - onSubscribe.accept(subscription); + subscription.setOnReadyHandler(() -> { + // Only execute the callback once (on first onReady) + Consumer> callback = onSubscribe; + if (callback != null && subscription.isReady()) { + onSubscribe = null; // Clear to prevent re-execution + callback.accept(subscription); + } + }); } return; } @@ -148,6 +160,9 @@ public void startRequest() { synchronized (this) { if (!canRequest) { canRequest = true; + // Request buffered messages from the server. + // Note: onSubscribe callback is now triggered by onReadyHandler (set in onSubscribe()), + // not here, because onReady is triggered earlier than onStart. long count = requested; subscription.request(count >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) count); } diff --git a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ClientTripleReactorPublisher.java b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ClientTripleReactorPublisher.java index 4caba1c79445..dffcb16a1fc6 100644 --- a/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ClientTripleReactorPublisher.java +++ b/dubbo-plugin/dubbo-reactive/src/main/java/org/apache/dubbo/reactive/ClientTripleReactorPublisher.java @@ -18,6 +18,7 @@ import org.apache.dubbo.common.stream.CallStreamObserver; import org.apache.dubbo.common.stream.ClientCallStreamObserver; +import org.apache.dubbo.common.stream.ClientResponseObserver; import java.util.function.Consumer; @@ -26,8 +27,13 @@ * It is a Publisher for user subscriber to subscribe.
* It is a StreamObserver for responseStream.
* It is a Subscription for user subscriber to request and pass request to requestStream. + *

+ * Implements {@link ClientResponseObserver} following gRPC's pattern where + * {@link #beforeStart(ClientCallStreamObserver)} is called before the stream starts, + * allowing configuration of flow control before any messages are sent. */ -public class ClientTripleReactorPublisher extends AbstractTripleReactorPublisher { +public class ClientTripleReactorPublisher extends AbstractTripleReactorPublisher + implements ClientResponseObserver { public ClientTripleReactorPublisher() {} @@ -35,8 +41,15 @@ public ClientTripleReactorPublisher(Consumer> onSubscribe, super(onSubscribe, shutdownHook); } + /** + * Called by the runtime prior to the start of a call to provide a reference to the + * {@link ClientCallStreamObserver} for the outbound stream. + *

+ * Following gRPC's pattern, this method is called BEFORE {@code call.start()}, + * allowing configuration of onReadyHandler and flow control settings. + */ @Override - public void beforeStart(ClientCallStreamObserver clientCallToObserverAdapter) { - super.onSubscribe(clientCallToObserverAdapter); + public void beforeStart(ClientCallStreamObserver requestStream) { + super.onSubscribe(requestStream); } } diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ServerChannelObserver.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ServerChannelObserver.java index fb82a6c2e610..37aa34f49b51 100644 --- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ServerChannelObserver.java +++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ServerChannelObserver.java @@ -16,6 +16,7 @@ */ package org.apache.dubbo.remoting.http12.h2; +import org.apache.dubbo.common.stream.ServerCallStreamObserver; import org.apache.dubbo.remoting.http12.AbstractServerHttpChannelObserver; import org.apache.dubbo.remoting.http12.ErrorCodeHolder; import org.apache.dubbo.remoting.http12.FlowControlStreamObserver; @@ -29,8 +30,14 @@ import io.netty.handler.codec.http2.DefaultHttp2Headers; +/** + * HTTP/2 server-side stream observer with flow control and backpressure support. + * Implements {@link ServerCallStreamObserver} following gRPC's pattern for backpressure. + */ public class Http2ServerChannelObserver extends AbstractServerHttpChannelObserver - implements FlowControlStreamObserver, Http2CancelableStreamObserver { + implements FlowControlStreamObserver, + Http2CancelableStreamObserver, + ServerCallStreamObserver { private CancellationContext cancellationContext; diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/CancelableStreamObserver.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/CancelableStreamObserver.java index 1ace07da9ec2..6b1548b6e8a4 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/CancelableStreamObserver.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/CancelableStreamObserver.java @@ -16,11 +16,11 @@ */ package org.apache.dubbo.rpc.protocol.tri; -import org.apache.dubbo.common.stream.ClientCallStreamObserver; import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.rpc.CancellationContext; +import org.apache.dubbo.rpc.protocol.tri.observer.ClientCallToObserverAdapter; -public abstract class CancelableStreamObserver implements StreamObserver { +public abstract class CancelableStreamObserver implements StreamObserver { private CancellationContext cancellationContext; @@ -36,7 +36,7 @@ public void cancel(Throwable throwable) { cancellationContext.cancel(throwable); } - public void beforeStart(ClientCallStreamObserver clientCallStreamObserver) { + public void beforeStart(ClientCallToObserverAdapter clientCallToObserverAdapter) { // do nothing } diff --git a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java index 0a3fe14b2ad0..fa29728af7d2 100644 --- a/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java +++ b/dubbo-rpc/dubbo-rpc-triple/src/main/java/org/apache/dubbo/rpc/protocol/tri/TripleInvoker.java @@ -22,6 +22,7 @@ import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.common.logger.ErrorTypeAwareLogger; import org.apache.dubbo.common.logger.LoggerFactory; +import org.apache.dubbo.common.stream.ClientResponseObserver; import org.apache.dubbo.common.stream.StreamObserver; import org.apache.dubbo.common.threadpool.ThreadlessExecutor; import org.apache.dubbo.common.utils.ConcurrentHashMapUtils; @@ -248,10 +249,19 @@ AsyncRpcResult invokeBiOrClientStream(MethodDescriptor methodDescriptor, Invocat * The call sequence is: *
      * 1. Create adapter and listener
-     * 2. Call beforeStart() if observer is CancelableStreamObserver (allows configuring onReadyHandler)
+     * 2. Call beforeStart() if observer is ClientResponseObserver or CancelableStreamObserver
+     *   (allows configuring onReadyHandler before stream starts)
      * 3. Start the call (creates stream, may trigger initial onReady)
      * 
+ * + *

The two interfaces serve different purposes: + *

    + *
  • {@link ClientResponseObserver} - gRPC-compatible interface with just beforeStart() + *
  • {@link CancelableStreamObserver} - Dubbo's extended interface with cancellation and startRequest() + *
+ * An observer can implement both interfaces (e.g., ClientTripleReactorPublisher). */ + @SuppressWarnings("unchecked") StreamObserver streamCall( ClientCall call, RequestMetadata metadata, StreamObserver responseObserver) { ClientCallToObserverAdapter adapter = new ClientCallToObserverAdapter<>(call, true); @@ -260,16 +270,28 @@ StreamObserver streamCall( ObserverToClientCallListenerAdapter listener = new ObserverToClientCallListenerAdapter(responseObserver); listener.setRequestAdapter(adapter); + // Handle CancelableStreamObserver first (for cancellation context and startRequest) + // This must be done regardless of whether it also implements ClientResponseObserver if (responseObserver instanceof CancelableStreamObserver) { CancelableStreamObserver cancelableObserver = (CancelableStreamObserver) responseObserver; // Set up cancellation context CancellationContext context = new CancellationContext(); cancelableObserver.setCancellationContext(context); context.addListener(ctx -> call.cancelByLocal(new IllegalStateException("Canceled by app"))); + // Set up startRequest to be called when stream is established (onStart) listener.setOnStartConsumer(dummy -> cancelableObserver.startRequest()); + } - // Call beforeStart BEFORE starting the call - // This allows users to configure onReadyHandler before the stream starts + // Now call beforeStart() - use ClientResponseObserver if available (gRPC-compatible), + // otherwise fall back to CancelableStreamObserver.beforeStart() + if (responseObserver instanceof ClientResponseObserver) { + // gRPC-compatible interface - beforeStart takes ClientCallStreamObserver + ClientResponseObserver clientResponseObserver = + (ClientResponseObserver) responseObserver; + clientResponseObserver.beforeStart(adapter); + } else if (responseObserver instanceof CancelableStreamObserver) { + // Legacy Dubbo interface - beforeStart takes ClientCallToObserverAdapter + CancelableStreamObserver cancelableObserver = (CancelableStreamObserver) responseObserver; cancelableObserver.beforeStart(adapter); } From 348858ee7fe9600a15fc9b21043dc368a48bdb0f Mon Sep 17 00:00:00 2001 From: earthchen Date: Tue, 6 Jan 2026 13:09:41 +0800 Subject: [PATCH 27/30] fix --- .../dubbo/common/stream/ServerCallStreamObserver.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java index 9e2f7321ed3a..a80b38cf58ed 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/ServerCallStreamObserver.java @@ -77,4 +77,9 @@ * @see CallStreamObserver * @see StreamObserver */ -public interface ServerCallStreamObserver extends CallStreamObserver {} +public interface ServerCallStreamObserver extends CallStreamObserver { + + default void disableAutoRequest() { + disableAutoFlowControl(); + } +} From 0ef863127e93cef440eba52a5814c7ee75c9f140 Mon Sep 17 00:00:00 2001 From: earthchen Date: Tue, 6 Jan 2026 13:24:27 +0800 Subject: [PATCH 28/30] fix --- .../org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java index bd765521eb73..c8788ddc2a05 100644 --- a/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java +++ b/dubbo-plugin/dubbo-mutiny/src/main/java/org/apache/dubbo/mutiny/ClientTripleMutinyPublisher.java @@ -17,7 +17,7 @@ package org.apache.dubbo.mutiny; import org.apache.dubbo.common.stream.CallStreamObserver; -import org.apache.dubbo.common.stream.ClientCallStreamObserver; +import org.apache.dubbo.rpc.protocol.tri.observer.ClientCallToObserverAdapter; import java.util.function.Consumer; @@ -36,7 +36,7 @@ public ClientTripleMutinyPublisher(Consumer> onSubscribe, } @Override - public void beforeStart(ClientCallStreamObserver clientCallToObserverAdapter) { + public void beforeStart(ClientCallToObserverAdapter clientCallToObserverAdapter) { super.onSubscribe(clientCallToObserverAdapter); } } From 2331ffc749d537b0c453e1b054a542ff84ea4a34 Mon Sep 17 00:00:00 2001 From: earthchen Date: Thu, 8 Jan 2026 16:47:29 +0800 Subject: [PATCH 29/30] remove default --- .../java/org/apache/dubbo/common/stream/CallStreamObserver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java index 48b8bcad17db..5d12ce800184 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java @@ -100,7 +100,7 @@ public interface CallStreamObserver extends StreamObserver { * * @param onReadyHandler the handler to invoke when the stream becomes ready */ - default void setOnReadyHandler(Runnable onReadyHandler) {} + void setOnReadyHandler(Runnable onReadyHandler); /** * Requests the peer to produce {@code count} more messages to be delivered to the 'inbound' From c7e27b8e67a9f7266d4b9de0af14c2200804fef5 Mon Sep 17 00:00:00 2001 From: earthchen Date: Thu, 8 Jan 2026 16:58:53 +0800 Subject: [PATCH 30/30] remove default --- .../org/apache/dubbo/common/stream/CallStreamObserver.java | 2 +- .../dubbo/remoting/http12/h2/Http2ServerChannelObserver.java | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java index 5d12ce800184..a08dd4c4b2a4 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/stream/CallStreamObserver.java @@ -118,7 +118,7 @@ public interface CallStreamObserver extends StreamObserver { * For stream set compression needs to determine whether the metadata has been sent, and carry * on corresponding processing */ - default void setCompression(String compression) {} + void setCompression(String compression); /** * Swaps to manual flow control where no message will be delivered to {@link diff --git a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ServerChannelObserver.java b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ServerChannelObserver.java index 37aa34f49b51..0fbf9bab0c82 100644 --- a/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ServerChannelObserver.java +++ b/dubbo-remoting/dubbo-remoting-http12/src/main/java/org/apache/dubbo/remoting/http12/h2/Http2ServerChannelObserver.java @@ -125,6 +125,11 @@ public void request(int count) { streamingDecoder.request(count); } + @Override + public void setCompression(String compression) { + // not supported yet + } + @Override public void disableAutoFlowControl() { autoRequestN = false;