From ad50eb0311577d10dc3d841ecda5fcb08c3b98e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kraus?= Date: Tue, 28 Feb 2023 16:39:43 +0100 Subject: [PATCH 01/17] HTTP2 Server Flow-control - inbound MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Kraus --- .../grpc/webserver/GrpcProtocolHandler.java | 6 +- .../GrpcProtocolHandlerNotFound.java | 2 +- .../io/helidon/nima/http2/FlowControl.java | 232 +++++++++++---- .../helidon/nima/http2/FlowControlImpl.java | 178 +++++++---- .../helidon/nima/http2/FlowControlNoop.java | 74 +++++ .../nima/http2/Http2ConnectionWriter.java | 12 +- .../io/helidon/nima/http2/Http2Stream.java | 14 +- .../helidon/nima/http2/Http2StreamWriter.java | 8 +- .../io/helidon/nima/http2/WindowSize.java | 121 ++++---- .../io/helidon/nima/http2/WindowSizeImpl.java | 277 ++++++++++++++++++ .../helidon/nima/http2/Http2HeadersTest.java | 25 +- .../nima/http2/webserver/Http2Config.java | 40 ++- .../nima/http2/webserver/Http2Connection.java | 149 ++++++++-- .../http2/webserver/Http2ServerResponse.java | 8 +- .../nima/http2/webserver/Http2Stream.java | 60 ++-- .../http2/webserver/ConnectionConfigTest.java | 40 ++- .../src/test/resources/application.yaml | 4 + 17 files changed, 1000 insertions(+), 250 deletions(-) create mode 100644 nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java create mode 100644 nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java diff --git a/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandler.java b/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandler.java index 4f455c49cd8..8f90468427a 100644 --- a/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandler.java +++ b/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandler.java @@ -151,7 +151,7 @@ public void sendHeaders(Metadata headers) { streamWriter.writeHeaders(http2Headers, streamId, Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS), - FlowControl.NOOP); + FlowControl.Outbound.NOOP); } @Override @@ -175,7 +175,7 @@ public void sendMessage(RES message) { Http2Flag.DataFlags.create(0), streamId); - streamWriter.write(new Http2FrameData(header, bufferData), FlowControl.NOOP); + streamWriter.write(new Http2FrameData(header, bufferData), FlowControl.Outbound.NOOP); } @Override @@ -187,7 +187,7 @@ public void close(Status status, Metadata trailers) { streamWriter.writeHeaders(http2Headers, streamId, Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS | Http2Flag.END_OF_STREAM), - FlowControl.NOOP); + FlowControl.Outbound.NOOP); currentStreamState = Http2StreamState.HALF_CLOSED_LOCAL; } diff --git a/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandlerNotFound.java b/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandlerNotFound.java index da8f7315660..19e49811926 100644 --- a/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandlerNotFound.java +++ b/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandlerNotFound.java @@ -50,7 +50,7 @@ public void init() { streamWriter.writeHeaders(http2Headers, streamId, Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS | Http2Flag.END_OF_STREAM), - FlowControl.NOOP); + FlowControl.Outbound.NOOP); currentStreamState = Http2StreamState.HALF_CLOSED_LOCAL; } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java index 774fb70921b..21f25a3a437 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,57 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.nima.http2; +import java.util.function.Consumer; + /** * Flow control used by HTTP/2 for backpressure. */ public interface FlowControl { - /** - * No-op flow control, used for connection related frames. - */ - FlowControl NOOP = new FlowControl() { - @Override - public void resetStreamWindowSize(long increment) { - } - - @Override - public void decrementWindowSize(int decrement) { - } - - @Override - public boolean incrementStreamWindowSize(int increment) { - return false; - } - - @Override - public int getRemainingWindowSize() { - return Integer.MAX_VALUE; - } - - @Override - public Http2FrameData[] split(Http2FrameData frame) { - return new Http2FrameData[] {frame}; - } - - @Override - public boolean blockTillUpdate() { - return false; - } - }; /** - * Create a flow control for a stream. + * Decrement window size. * - * @param streamId stream id - * @param streamInitialWindowSize initial window size for stream - * @param connectionWindowSize connection window size - * @return a new flow control + * @param decrement decrement in bytes */ - static FlowControl create(int streamId, int streamInitialWindowSize, WindowSize connectionWindowSize) { - return new FlowControlImpl(streamId, streamInitialWindowSize, connectionWindowSize); - } + void decrementWindowSize(int decrement); /** * Reset stream window size. @@ -73,39 +37,185 @@ static FlowControl create(int streamId, int streamInitialWindowSize, WindowSize void resetStreamWindowSize(long increment); /** - * Decrement window size. + * Remaining window size in bytes. * - * @param decrement decrement in bytes + * @return remaining size */ - void decrementWindowSize(int decrement); + int getRemainingWindowSize(); /** - * Increment stream window size. + * Create inbound flow control builder for a stream. * - * @param increment increment in bytes - * @return {@code true} if succeeded, {@code false} if timed out + * @return a new inbound flow control builder */ - boolean incrementStreamWindowSize(int increment); + static FlowControl.Inbound.Builder builderInbound() { + return new FlowControl.Inbound.Builder(); + } /** - * Remaining window size in bytes. + * Create outbound flow control for a stream. * - * @return remaining size + * @param streamId stream id + * @param streamInitialWindowSize initial window size for stream + * @param connectionWindowSize connection window size + * @return a new flow control */ - int getRemainingWindowSize(); + static FlowControl.Outbound createOutbound(int streamId, + int streamInitialWindowSize, + WindowSize.Outbound connectionWindowSize) { + return new FlowControlImpl.Outbound(streamId, + streamInitialWindowSize, + connectionWindowSize); + } /** - * Split frame into frames that can be sent. - * - * @param frame frame to split - * @return result + * Inbound flow control used by HTTP/2 for backpressure. */ - Http2FrameData[] split(Http2FrameData frame); + interface Inbound extends FlowControl { + + /** + * Increment window size. + * + * @param increment increment in bytes + */ + void incrementWindowSize(int increment); + + /** + * Inbound flow control builder. + */ + class Builder implements io.helidon.common.Builder { + + private int streamId; + private int streamWindowSize; + private int streamMaxFrameSize; + private WindowSize.Inbound connectionWindowSize; + private Consumer windowUpdateStreamWriter; + private boolean noop; + + private Builder() { + this.streamId = 0; + this.streamWindowSize = 0; + this.streamMaxFrameSize = 0; + this.connectionWindowSize = null; + this.windowUpdateStreamWriter = null; + this.noop = false; + } + + @Override + public FlowControl.Inbound build() { + return noop + ? new FlowControlNoop.Inbound(connectionWindowSize, + windowUpdateStreamWriter) + : new FlowControlImpl.Inbound(streamId, + streamWindowSize, + streamMaxFrameSize, + connectionWindowSize, + windowUpdateStreamWriter); + } + + /** + * Trigger build of NOOP flow control (flow control turned off). + * NOOP flow control will be returned regardless of other setting when this method is called. + * + * @return this builder + */ + public Builder noop() { + noop = true; + return this; + } + + /** + * Set HTTP/2 stream ID. + * + * @param streamId HTTP/2 stream ID + * @return this builder + */ + public Builder streamId(int streamId) { + this.streamId = streamId; + return this; + } + + /** + * Set HTTP/2 connection window size. + * + * @param windowSize HTTP/2 connection window size + * @return this builder + */ + public Builder connectionWindowsize(WindowSize.Inbound windowSize) { + this.connectionWindowSize = windowSize; + return this; + } + + /** + * Set HTTP/2 stream window size. + * + * @param windowSize HTTP/2 stream window size + * @return this builder + */ + public Builder streamWindowsize(int windowSize) { + this.streamWindowSize = windowSize; + return this; + } + + /** + * Set HTTP/2 stream window size. + * + * @param maxFrameSize HTTP/2 stream maximum frame size size + * @return this builder + */ + public Builder streamMaxFrameSize(int maxFrameSize) { + this.streamMaxFrameSize = maxFrameSize; + return this; + } + + /** + * Set writer method for current HTTP/2 stream WINDOW_UPDATE frame. + * + * @param windowUpdateWriter WINDOW_UPDATE frame writer for current HTTP/2 stream + * @return this builder + */ + public Builder windowUpdateStreamWriter(Consumer windowUpdateWriter) { + this.windowUpdateStreamWriter = windowUpdateWriter; + return this; + } + + } + + } /** - * Block until a window size update happens. - * - * @return {@code true} if window update happened, {@code false} in case of timeout + * Outbound flow control used by HTTP/2 for backpressure. */ - boolean blockTillUpdate(); + interface Outbound extends FlowControl { + + /** + * No-op outbound flow control, used for connection related frames. + */ + Outbound NOOP = new FlowControlNoop.Outbound(); + + /** + * Increment stream window size. + * + * @param increment increment in bytes + * @return {@code true} if succeeded, {@code false} if timed out + */ + boolean incrementStreamWindowSize(int increment); + + /** + * Split frame into frames that can be sent. + * + * @param frame frame to split + * @return result + */ + Http2FrameData[] split(Http2FrameData frame); + + /** + * Block until a window size update happens. + * + * @return {@code true} if window update happened, {@code false} in case of timeout + */ + boolean blockTillUpdate(); + + } + } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java index d5f204e5514..c118993004c 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,96 +13,164 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.nima.http2; +import java.util.Objects; +import java.util.function.Consumer; + import io.helidon.common.buffers.BufferData; -class FlowControlImpl implements FlowControl { +abstract class FlowControlImpl implements FlowControl { private final int streamId; - private final WindowSize connectionWindowSize; - private final WindowSize streamWindowSize; - FlowControlImpl(int streamId, int streamInitialWindowSize, WindowSize connectionWindowSize) { + FlowControlImpl(int streamId) { this.streamId = streamId; - this.connectionWindowSize = connectionWindowSize; - this.streamWindowSize = new WindowSize(streamInitialWindowSize); } - @Override - public void resetStreamWindowSize(long increment) { - streamWindowSize.resetWindowSize(increment); - } + abstract WindowSize connectionWindowSize(); + abstract WindowSize streamWindowSize(); - @Override public void decrementWindowSize(int decrement) { - connectionWindowSize.decrementWindowSize(decrement); - streamWindowSize.decrementWindowSize(decrement); + connectionWindowSize().decrementWindowSize(decrement); + streamWindowSize().decrementWindowSize(decrement); } @Override - public boolean incrementStreamWindowSize(int increment) { - boolean overflow = streamWindowSize.incrementWindowSize(increment); - connectionWindowSize.triggerUpdate(); - return overflow; + public void resetStreamWindowSize(long increment) { + streamWindowSize().resetWindowSize(increment); } @Override public int getRemainingWindowSize() { - return Integer.min(connectionWindowSize.getRemainingWindowSize(), streamWindowSize.getRemainingWindowSize()); - } - - @Override - public Http2FrameData[] split(Http2FrameData frame) { - return split(getRemainingWindowSize(), frame); - } - - @Override - public boolean blockTillUpdate() { - return connectionWindowSize.blockTillUpdate(); + return Integer.min( + connectionWindowSize().getRemainingWindowSize(), + streamWindowSize().getRemainingWindowSize()); } @Override public String toString() { return "FlowControlImpl{" + "streamId=" + streamId - + ", connectionWindowSize=" + connectionWindowSize - + ", streamWindowSize=" + streamWindowSize + + ", connectionWindowSize=" + connectionWindowSize() + + ", streamWindowSize=" + streamWindowSize() + '}'; } - private Http2FrameData[] split(int size, Http2FrameData frame) { - int length = frame.header().length(); - if (length <= size || length == 0) { - return new Http2FrameData[] {frame}; + static class Inbound extends FlowControlImpl implements FlowControl.Inbound { + + private final WindowSize.Inbound connectionWindowSize; + private final WindowSize.Inbound streamWindowSize; + + Inbound(int streamId, + int streamInitialWindowSize, + int streamMaxFrameSize, + WindowSize.Inbound connectionWindowSize, + Consumer windowUpdateStreamWriter) { + super(streamId); + if (streamInitialWindowSize == 0) { + throw new IllegalArgumentException("Window size in bytes for stream-level flow control was not set."); + } + Objects.requireNonNull(connectionWindowSize, "Window size in bytes for connection-level flow control was not set."); + Objects.requireNonNull(windowUpdateStreamWriter, "Stream-level window update writer was not set."); + this.connectionWindowSize = connectionWindowSize; + this.streamWindowSize = WindowSize.createInbound(streamInitialWindowSize, + streamMaxFrameSize, + windowUpdateStreamWriter); + } + + @Override + WindowSize connectionWindowSize() { + return connectionWindowSize; + } + + @Override + WindowSize streamWindowSize() { + return streamWindowSize; + } + + @Override + public void incrementWindowSize(int increment) { + streamWindowSize.incrementWindowSize(increment); + connectionWindowSize.incrementWindowSize(increment); + } + + } + + static class Outbound extends FlowControlImpl implements FlowControl.Outbound { + + private final WindowSize.Outbound connectionWindowSize; + private final WindowSize.Outbound streamWindowSize; + + Outbound(int streamId, + int streamInitialWindowSize, + WindowSize.Outbound connectionWindowSize) { + super(streamId); + this.connectionWindowSize = connectionWindowSize; + this.streamWindowSize = WindowSize.createOutbound(streamInitialWindowSize); } - if (size == 0) { - return new Http2FrameData[0]; + @Override + WindowSize connectionWindowSize() { + return connectionWindowSize; } - byte[] data1 = new byte[size]; - byte[] data2 = new byte[length - size]; + @Override + WindowSize streamWindowSize() { + return streamWindowSize; + } - frame.data().read(data1); - frame.data().read(data2); + @Override + public boolean incrementStreamWindowSize(int increment) { + boolean result = streamWindowSize.incrementWindowSize(increment); + connectionWindowSize.triggerUpdate(); + return result; + } - BufferData bufferData1 = BufferData.create(data1); - BufferData bufferData2 = BufferData.create(data2); + @Override + public Http2FrameData[] split(Http2FrameData frame) { + return split(getRemainingWindowSize(), frame); + } - Http2FrameData frameData1 = new Http2FrameData(Http2FrameHeader.create(bufferData1.available(), - Http2FrameTypes.DATA, - Http2Flag.DataFlags.create(0), - frame.header().streamId()), - bufferData1); + @Override + public boolean blockTillUpdate() { + return connectionWindowSize.blockTillUpdate(); + } - Http2FrameData frameData2 = new Http2FrameData(Http2FrameHeader.create(bufferData2.available(), - Http2FrameTypes.DATA, - Http2Flag.DataFlags.create(0), - frame.header().streamId()), - bufferData2); + private Http2FrameData[] split(int size, Http2FrameData frame) { + int length = frame.header().length(); + if (length <= size || length == 0) { + return new Http2FrameData[]{frame}; + } + + if (size == 0) { + return new Http2FrameData[0]; + } + + byte[] data1 = new byte[size]; + byte[] data2 = new byte[length - size]; + + frame.data().read(data1); + frame.data().read(data2); + + BufferData bufferData1 = BufferData.create(data1); + BufferData bufferData2 = BufferData.create(data2); + + Http2FrameData frameData1 = new Http2FrameData(Http2FrameHeader.create(bufferData1.available(), + Http2FrameTypes.DATA, + Http2Flag.DataFlags.create(0), + frame.header().streamId()), + bufferData1); + + Http2FrameData frameData2 = new Http2FrameData(Http2FrameHeader.create(bufferData2.available(), + Http2FrameTypes.DATA, + Http2Flag.DataFlags.create(0), + frame.header().streamId()), + bufferData2); + + return new Http2FrameData[]{frameData1, frameData2}; + } - return new Http2FrameData[] {frameData1, frameData2}; } + } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java new file mode 100644 index 00000000000..155787917a8 --- /dev/null +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.http2; + +import java.util.function.Consumer; + +class FlowControlNoop implements FlowControl { + + @Override + public void decrementWindowSize(int decrement) { + } + + @Override + public void resetStreamWindowSize(long increment) { + } + + @Override + public int getRemainingWindowSize() { + return Integer.MAX_VALUE; + } + + + // Even NOOP sends WINDOW_UPDATE frames + static class Inbound extends FlowControlNoop implements FlowControl.Inbound { + + private final WindowSize.Inbound connectionWindowSize; + private final WindowSize.Inbound streamWindowSize; + + Inbound(WindowSize.Inbound connectionWindowSize, Consumer windowUpdateStreamWriter) { + this.connectionWindowSize = connectionWindowSize; + this.streamWindowSize = WindowSize.createInboundNoop(windowUpdateStreamWriter); + } + + @Override + public void incrementWindowSize(int increment) { + streamWindowSize.incrementWindowSize(increment); + connectionWindowSize.incrementWindowSize(increment); + } + + } + + static class Outbound extends FlowControlNoop implements FlowControl.Outbound { + + @Override + public boolean incrementStreamWindowSize(int increment) { + return false; + } + + @Override + public Http2FrameData[] split(Http2FrameData frame) { + return new Http2FrameData[] {frame}; + } + + @Override + public boolean blockTillUpdate() { + return false; + } + + } + +} diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java index 74075aa7efa..886ed07c752 100755 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -57,7 +57,7 @@ public Http2ConnectionWriter(SocketContext ctx, DataWriter writer, List { noLockWrite(flowControl, frame); return null; @@ -65,7 +65,7 @@ public void write(Http2FrameData frame, FlowControl flowControl) { } @Override - public int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlags flags, FlowControl flowControl) { + public int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlags flags, FlowControl.Outbound flowControl) { // this is executing in the thread of the stream // we must enforce parallelism of exactly 1, to make sure the dynamic table is updated // and then immediately written @@ -92,7 +92,7 @@ public int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlags flags, Http2FrameData dataFrame, - FlowControl flowControl) { + FlowControl.Outbound flowControl) { // this is executing in the thread of the stream // we must enforce parallelism of exactly 1, to make sure the dynamic table is updated // and then immediately written @@ -145,7 +145,7 @@ private T withStreamLock(Callable callable) { } } - private void noLockWrite(FlowControl flowControl, Http2FrameData frame) { + private void noLockWrite(FlowControl.Outbound flowControl, Http2FrameData frame) { if (frame.header().type() == Http2FrameTypes.DATA.type()) { splitAndWrite(frame, flowControl); } else { @@ -153,7 +153,7 @@ private void noLockWrite(FlowControl flowControl, Http2FrameData frame) { } } - private void splitAndWrite(Http2FrameData frame, FlowControl flowControl) { + private void splitAndWrite(Http2FrameData frame, FlowControl.Outbound flowControl) { Http2FrameData[] splitFrames = flowControl.split(frame); if (splitFrames.length == 1) { // windows are wide enough diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Stream.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Stream.java index cd6ca8709bd..608f1a5f6c7 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Stream.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Stream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,9 +75,17 @@ public interface Http2Stream { Http2StreamState streamState(); /** - * Flow control of this stream. + * Outbound flow control of this stream. * * @return flow control */ - FlowControl flowControl(); + FlowControl.Outbound outboundFlowControl(); + + /** + * Inbound flow control of this stream. + * + * @return flow control + */ + FlowControl.Inbound inboundFlowControl(); + } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamWriter.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamWriter.java index 042d74db22a..4d17e210c3f 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamWriter.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamWriter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ public interface Http2StreamWriter { * @param frame frame to write * @param flowControl flow control */ - void write(Http2FrameData frame, FlowControl flowControl); + void write(Http2FrameData frame, FlowControl.Outbound flowControl); /** * Write headers with no (or streaming) entity. @@ -37,7 +37,7 @@ public interface Http2StreamWriter { * @param flowControl flow control * @return number of bytes written */ - int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlags flags, FlowControl flowControl); + int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlags flags, FlowControl.Outbound flowControl); /** * Write headers and entity. @@ -53,5 +53,5 @@ int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlags flags, Http2FrameData dataFrame, - FlowControl flowControl); + FlowControl.Outbound flowControl); } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java index ebbbdbb628b..9c722d16440 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,110 +13,121 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.helidon.nima.http2; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; /** * Window size container, used with {@link io.helidon.nima.http2.FlowControl}. */ -public class WindowSize { +public interface WindowSize { + /** * Default window size. */ - public static final int DEFAULT_WIN_SIZE = 65_535; + int DEFAULT_WIN_SIZE = 65_535; + /** * Maximal window size. */ - public static final int MAX_WIN_SIZE = Integer.MAX_VALUE; + int MAX_WIN_SIZE = Integer.MAX_VALUE; - private final AtomicInteger remainingWindowSize; + /** + * Create inbound window size container with initial window size set. + * + * @param initialWindowSize initial window size + * @param maxFrameSize maximal frame size + * @param windowUpdateWriter writer method for HTTP/2 WINDOW_UPDATE frame + * @return a new window size container + */ + static WindowSize.Inbound createInbound(int initialWindowSize, + int maxFrameSize, + Consumer windowUpdateWriter) { + return new WindowSizeImpl.Inbound(initialWindowSize, maxFrameSize, windowUpdateWriter); + } - private final AtomicReference> updated = new AtomicReference<>(new CompletableFuture<>()); + /** + * Create outbound window size container with default initial size. + * + * @return a new window size container + */ + static WindowSize.Outbound createOutbound() { + return new WindowSizeImpl.Outbound(WindowSize.DEFAULT_WIN_SIZE); + } - WindowSize(int initialWindowSize) { - remainingWindowSize = new AtomicInteger(initialWindowSize); + /** + * Create outbound window size container with initial window size set. + * + * @param initialWindowSize initial window size + * @return a new window size container + */ + static WindowSize.Outbound createOutbound(int initialWindowSize) { + return new WindowSizeImpl.Outbound(initialWindowSize); } /** - * Window size with default initial size. + * Create inbound window size container with flow control turned off. + * + * @param windowUpdateWriter WINDOW_UPDATE frame writer + * @return a new window size container */ - public WindowSize() { - remainingWindowSize = new AtomicInteger(DEFAULT_WIN_SIZE); + static WindowSize.Inbound createInboundNoop(Consumer windowUpdateWriter) { + return new WindowSizeImpl.InboundNoop(windowUpdateWriter); } /** * Reset window size. * - * @param n window size + * @param size window size */ - public void resetWindowSize(long n) { - // When the value of SETTINGS_INITIAL_WINDOW_SIZE changes, - // a receiver MUST adjust the size of all stream flow-control windows that - // it maintains by the difference between the new value and the old value - remainingWindowSize.updateAndGet(o -> (int) n - o); - } + void resetWindowSize(long size); /** * Increment window size. * - * @param n increment + * @param increment increment * @return whether the increment succeeded */ - public boolean incrementWindowSize(int n) { - int old = remainingWindowSize.getAndUpdate(r -> MAX_WIN_SIZE - r > n ? n + r : MAX_WIN_SIZE); - triggerUpdate(); - return MAX_WIN_SIZE - old <= n; - } + boolean incrementWindowSize(int increment); /** * Decrement window size. * * @param decrement decrement */ - public void decrementWindowSize(int decrement) { - remainingWindowSize.updateAndGet(operand -> operand - decrement); - } + void decrementWindowSize(int decrement); /** * Remaining window size. * * @return remaining sze */ - public int getRemainingWindowSize() { - return remainingWindowSize.get(); - } + int getRemainingWindowSize(); + // Does not add anything new but having a separate name makes code more human-readable. /** - * Block until window size update. - * - * @return whether update happened before timeout + * Inbound window size container. */ - public boolean blockTillUpdate() { - try { - //TODO configurable timeout - updated.get().get(10, TimeUnit.SECONDS); - return false; - } catch (InterruptedException | ExecutionException | TimeoutException e) { - return true; - } + interface Inbound extends WindowSize { } /** - * Trigger update of window size. + * Outbound window size container. */ - public void triggerUpdate() { - updated.getAndSet(new CompletableFuture<>()).complete(null); - } + interface Outbound extends WindowSize { + + /** + * Trigger update of window size. + */ + void triggerUpdate(); + + /** + * Block until window size update. + * + * @return whether update happened before timeout + */ + boolean blockTillUpdate(); - @Override - public String toString() { - return String.valueOf(remainingWindowSize.get()); } + } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java new file mode 100644 index 00000000000..5ed50ea4a00 --- /dev/null +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.http2; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +/** + * Window size container, used with {@link io.helidon.nima.http2.FlowControl}. + */ +abstract class WindowSizeImpl implements WindowSize { + + private final AtomicInteger remainingWindowSize; + + private WindowSizeImpl(int initialWindowSize) { + remainingWindowSize = new AtomicInteger(initialWindowSize); + } + + @Override + public void resetWindowSize(long size) { + // When the value of SETTINGS_INITIAL_WINDOW_SIZE changes, + // a receiver MUST adjust the size of all stream flow-control windows that + // it maintains by the difference between the new value and the old value + remainingWindowSize.updateAndGet(o -> (int) size - o); + } + + @Override + public boolean incrementWindowSize(int increment) { + int remaining = remainingWindowSize.getAndUpdate(r -> MAX_WIN_SIZE - r > increment ? increment + r : MAX_WIN_SIZE); + return MAX_WIN_SIZE - remaining <= increment; + } + + @Override + public void decrementWindowSize(int decrement) { + remainingWindowSize.updateAndGet(operand -> operand - decrement); + } + + @Override + public int getRemainingWindowSize() { + return remainingWindowSize.get(); + } + + @Override + public String toString() { + return String.valueOf(remainingWindowSize.get()); + } + + /** + * Inbound window size container. + */ + static final class Inbound extends WindowSizeImpl implements WindowSize.Inbound { + + private final Strategy strategy; + + Inbound(int initialWindowSize, int maxFrameSize, Consumer windowUpdateWriter) { + super(initialWindowSize); + // Strategy selection based on initialWindowSize and maxFrameSize + this.strategy = Strategy.create(new Strategy.Context(maxFrameSize, initialWindowSize), + windowUpdateWriter); + } + + @Override + public boolean incrementWindowSize(int increment) { + boolean result = super.incrementWindowSize(increment); + strategy.windowUpdate(increment); + return result; + } + + } + + /** + * Outbound window size container. + */ + static final class Outbound extends WindowSizeImpl implements WindowSize.Outbound { + + private final AtomicReference> updated = new AtomicReference<>(new CompletableFuture<>()); + + Outbound(int initialWindowSize) { + super(initialWindowSize); + } + + @Override + public boolean incrementWindowSize(int increment) { + boolean result = super.incrementWindowSize(increment); + triggerUpdate(); + return result; + } + + @Override + public void triggerUpdate() { + updated.getAndSet(new CompletableFuture<>()).complete(null); + } + + @Override + public boolean blockTillUpdate() { + try { + //TODO configurable timeout + updated.get().get(10, TimeUnit.SECONDS); + return false; + } catch (InterruptedException | ExecutionException | TimeoutException e) { + return true; + } + } + + } + + /** + * Inbound window size container with flow control turned off. + */ + public static final class InboundNoop implements WindowSize.Inbound { + + private static final int WIN_SIZE_WATERMARK = MAX_WIN_SIZE / 2; + private final Consumer windowUpdateWriter; + private int delayedIncrement; + + InboundNoop(Consumer windowUpdateWriter) { + this.windowUpdateWriter = windowUpdateWriter; + this.delayedIncrement = 0; + } + + @Override + public boolean incrementWindowSize(int increment) { + // Send WINDOW_UPDATE frame joined for at least 1/2 of the maximum space + delayedIncrement += increment; + if (delayedIncrement > WIN_SIZE_WATERMARK) { + windowUpdateWriter.accept(new Http2WindowUpdate(delayedIncrement)); + delayedIncrement = 0; + } + return true; + } + + @Override + public void resetWindowSize(long size) { + } + + @Override + public void decrementWindowSize(int decrement) { + } + + @Override + public int getRemainingWindowSize() { + return MAX_WIN_SIZE; + } + + @Override + public String toString() { + return String.valueOf(MAX_WIN_SIZE); + } + + } + + abstract static class Strategy { + + private final Context context; + private final Consumer windowUpdateWriter; + + private Strategy(Context context, Consumer windowUpdateWriter) { + this.context = context; + this.windowUpdateWriter = windowUpdateWriter; + } + + abstract void windowUpdate(int increment); + + Context context() { + return context; + } + + Consumer + windowUpdateWriter() { + return windowUpdateWriter; + } + + private interface StrategyConstructor { + Strategy create(Context context, Consumer windowUpdateWriter); + } + + // Strategy Type to instance mapping array + private static final StrategyConstructor[] CREATORS = new StrategyConstructor[] { + Simple::new, + Bisection::new + }; + + // Strategy implementation factory + private static Strategy create(Context context, Consumer windowUpdateWriter) { + return CREATORS[Type.select(context).ordinal()] + .create(context, windowUpdateWriter); + } + + private enum Type { + /** + * Simple WINDOW_UPDATE strategy. + * Sends update frames as soon as buffer space is restored. + */ + SIMPLE, + /** + * Buffer space bisection strategy. + * Sends update frames when at least half of the buffer space is consumed. + */ + BISECTION; + + private static Type select(Context context) { + // Bisection strategy requires at least 4 frames to be placed inside window + return context.maxFrameSize * 4 < context.maxWindowsize ? BISECTION : SIMPLE; + } + + } + + private record Context( + int maxFrameSize, + int maxWindowsize) { + } + + /** + * Simple update strategy. + * Sends update frames as soon as buffer space is restored. + */ + private static final class Simple extends Strategy { + + private Simple(Context context, Consumer windowUpdateWriter) { + super(context, windowUpdateWriter); + } + + @Override + void windowUpdate(int increment) { + windowUpdateWriter().accept(new Http2WindowUpdate(increment)); + } + + } + + /** + * Buffer space bisection strategy. + * Sends update frames when at least half of the buffer space is consumed. + */ + private static final class Bisection extends Strategy { + + private int delayedIncrement; + + private final int watermark; + + private Bisection(Context context, Consumer windowUpdateWriter) { + super(context, windowUpdateWriter); + this.delayedIncrement = 0; + this.watermark = context().maxWindowsize() / 2; + } + + @Override + void windowUpdate(int increment) { + delayedIncrement += increment; + if (delayedIncrement > watermark) { + windowUpdateWriter().accept(new Http2WindowUpdate(delayedIncrement)); + delayedIncrement = 0; + } + } + + } + + } + +} diff --git a/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java b/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java index de51441e3e8..f7fd81b460a 100644 --- a/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java +++ b/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -163,7 +163,7 @@ void testC_3() { @Test void testC_4_toBytes() { DynamicTable dynamicTable = DynamicTable.create(Http2Settings.create()); - WritableHeaders headers = WritableHeaders.create(); + WritableHeaders headers = WritableHeaders.create(); Http2Headers http2Headers = Http2Headers.create(headers); http2Headers.method(Http.Method.GET); http2Headers.scheme("http"); @@ -264,6 +264,12 @@ private Http2Headers headers(String hexEncoded, DynamicTable dynamicTable) { private Http2Stream stream() { return new Http2Stream() { + private static final FlowControl.Inbound FC_IN_NOOP = FlowControl.builderInbound() + .noop() + .connectionWindowsize(WindowSize.createInboundNoop(http2WindowUpdate -> {})) + .windowUpdateStreamWriter(http2WindowUpdate -> {}) + .build(); + @Override public void rstStream(Http2RstStream rstStream) { @@ -300,8 +306,13 @@ public Http2StreamState streamState() { } @Override - public FlowControl flowControl() { - return FlowControl.NOOP; + public FlowControl.Outbound outboundFlowControl() { + return FlowControl.Outbound.NOOP; + } + + @Override + public FlowControl.Inbound inboundFlowControl() { + return FC_IN_NOOP; } }; } @@ -309,11 +320,11 @@ public FlowControl flowControl() { private static class DevNullWriter implements Http2StreamWriter { @Override - public void write(Http2FrameData frame, FlowControl flowControl) { + public void write(Http2FrameData frame, FlowControl.Outbound flowControl) { } @Override - public int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlags flags, FlowControl flowControl) { + public int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlags flags, FlowControl.Outbound flowControl) { return 0; } @@ -322,7 +333,7 @@ public int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlags flags, Http2FrameData dataFrame, - FlowControl flowControl) { + FlowControl.Outbound flowControl) { return 0; } } diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java index a7464b8a4bf..147e8e18652 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java @@ -29,12 +29,13 @@ public interface Http2Config { /** * The size of the largest frame payload that the sender is willing to receive in bytes. + * Default value is {@code 16384} and maximum value is 224-1 = 16777215 bytes. * See RFC 9113 section 6.5.2 for details. * * @return maximal frame size */ - @ConfiguredOption("16_384") - long maxFrameSize(); + @ConfiguredOption("16384") + int maxFrameSize(); /** * The maximum field section size that the sender is prepared to accept in bytes. @@ -66,6 +67,41 @@ public interface Http2Config { @ConfiguredOption("8192") long maxConcurrentStreams(); + /** + * This setting indicates whether flow control is turned on or off. Value of {@code true} turns flow control on + * and value of {@code false} turns flow control off. + * Default value is {@code true}. + * + * @return whether flow control is enabled + */ + @ConfiguredOption("true") + boolean flowControlEnabled(); + + /** + * This setting indicates the sender's maximum window size in bytes for connection-level flow control. + * Default and maximum value is 231-1 = 2147483647 bytes. This setting affects the window size + * of HTTP/2 connection. + * Any value greater than 2147483647 causes an error. Any value smaller than initial window size causes an error. + * See RFC 9113 section 6.9.1 for details. + * + * @return maximum window size in bytes + */ + @ConfiguredOption("2147483647") + int maxWindowSize(); + + /** + * This setting indicates the sender's maximum window size in bytes for stream-level flow control. + * Value of {@code 0} is reserved to use the same value as connection-level value. + * Default value is {@code 0}. This setting affects the window size of all streams. + * Any value greater than 2147483647 causes an error. Any value greater than {@code 0} and smaller than initial + * window size causes an error. + * See RFC 9113 section 6.9.1 for details. + * + * @return maximum stream-level window size in bytes + */ + @ConfiguredOption("0") + int maxStreamWindowSize(); + /** * Whether to send error message over HTTP to client. * Defaults to {@code false}, as exception message may contain internal information that could be used as an diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java index 5a9e3edb8c4..6f89f7db724 100755 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java @@ -86,7 +86,6 @@ public class Http2Connection implements ServerConnection, InterruptableTask subProviders; - private final WindowSize connectionWindowSize = new WindowSize(); private final DataReader reader; private final Http2Settings serverSettings; @@ -106,7 +105,12 @@ public class Http2Connection implements ServerConnection, InterruptableTask subProviders) { this.ctx = ctx; @@ -115,15 +119,36 @@ public class Http2Connection implements ServerConnection, InterruptableTask settingsUpdate(http2Config, builder)) .add(Http2Setting.ENABLE_PUSH, false) .build(); - this.connectionWriter = new Http2ConnectionWriter(ctx, ctx.dataWriter(), List.of(new Http2LoggingFrameListener("send"))); + this.connectionWriter = new Http2ConnectionWriter(ctx, + ctx.dataWriter(), + List.of(new Http2LoggingFrameListener("send"))); this.subProviders = subProviders; - this.requestDynamicTable = Http2Headers.DynamicTable.create(serverSettings.value(Http2Setting.HEADER_TABLE_SIZE)); + this.requestDynamicTable = Http2Headers.DynamicTable.create( + serverSettings.value(Http2Setting.HEADER_TABLE_SIZE)); this.requestHuffman = new Http2HuffmanDecoder(); this.routing = ctx.router().routing(HttpRouting.class, HttpRouting.empty()); this.reader = ctx.dataReader(); this.sendErrorDetails = http2Config.sendErrorDetails(); this.maxClientFrameSize = http2Config.maxClientFrameSize(); this.maxClientConcurrentStreams = http2Config.maxConcurrentStreams(); + + // Outbound flow control is initialized by RFC 9113 default values + this.outboundWindowSize = WindowSize.createOutbound(); + this.outboundInitialWindowSize = WindowSize.DEFAULT_WIN_SIZE; + + // Inbound flow control is initialized from config + if (http2Config.flowControlEnabled()) { + this.inboundWindowSize = WindowSize.createInbound(http2Config.maxWindowSize(), + http2Config.maxFrameSize(), + this::writeWindowUpdateFrame); + this.inboundInitialWindowSize = http2Config.maxStreamWindowSize() > 0 + ? http2Config.maxStreamWindowSize() + : http2Config.maxWindowSize(); + } else { + // Pass NOOP when flow control is turned off (but we still have to send WINDOW_UPDATE frames) + this.inboundWindowSize = WindowSize.createInboundNoop(this::writeWindowUpdateFrame); + this.inboundInitialWindowSize = WindowSize.MAX_WIN_SIZE; + } } @Override @@ -144,7 +169,7 @@ public void handle() throws InterruptedException { Http2GoAway frame = new Http2GoAway(0, e.code(), sendErrorDetails ? e.getMessage() : ""); - connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.NOOP); + connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); state = State.FINISHED; } catch (CloseConnectionException | InterruptedException e) { throw e; @@ -156,7 +181,7 @@ public void handle() throws InterruptedException { Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.INTERNAL, sendErrorDetails ? e.getClass().getName() + ": " + e.getMessage() : ""); - connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.NOOP); + connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); state = State.FINISHED; throw e; } @@ -216,6 +241,7 @@ private static void settingsUpdate(Http2Config config, Http2Settings.Builder bui applySetting(builder, config.maxFrameSize(), Http2Setting.MAX_FRAME_SIZE); applySetting(builder, config.maxHeaderListSize(), Http2Setting.MAX_HEADER_LIST_SIZE); applySetting(builder, config.maxConcurrentStreams(), Http2Setting.MAX_CONCURRENT_STREAMS); + applySetting(builder, config.maxWindowSize(), Http2Setting.INITIAL_WINDOW_SIZE); } // Add value to the builder only when differs from default @@ -240,11 +266,40 @@ private void doHandle() throws InterruptedException { // no data to read -> connection is closed throw new CloseConnectionException("Connection closed by client", e); } + dispatchHandler(); + + // Flow-control: frame processing is done, free space reserved for frame in window + int length = frameHeader.length(); + if (length > 0) { + int streamId = frameHeader.streamId(); + if (streamId > 0 && frameHeader.type() != Http2FrameType.HEADERS) { + // Stream ID > 0: update conenction and stream + FlowControl.Inbound inboundFlowControl = stream(streamId) + .stream() + .inboundFlowControl(); + inboundFlowControl.incrementWindowSize(length); + LOGGER.log(System.Logger.Level.INFO, () -> String.format( + "SRV IFC: Full increment %d-> %d", length, + inboundFlowControl.getRemainingWindowSize())); + } else { + // Stream ID == 0: update connection only + inboundWindowSize.incrementWindowSize(length); + LOGGER.log(System.Logger.Level.INFO, () -> String.format( + "SRV IFC: Conn increment %d -> %d", + length, inboundWindowSize.getRemainingWindowSize())); + } + } + } else { + dispatchHandler(); } - switch (state) { + } + } + + private void dispatchHandler() { + switch (state) { case CONTINUATION -> doContinuation(); case WRITE_SERVER_SETTINGS -> writeServerSettings(); - case WINDOW_UPDATE -> windowUpdateFrame(); + case WINDOW_UPDATE -> readWindowUpdateFrame(); case SETTINGS -> doSettings(); case ACK_SETTINGS -> ackSettings(); case DATA -> dataFrame(); @@ -258,7 +313,6 @@ private void doHandle() throws InterruptedException { goAwayFrame(); case RST_STREAM -> rstStream(); default -> unknownFrame(); - } } } @@ -274,6 +328,7 @@ private void readPreface() { } private void readFrame() { + BufferData frameHeaderBuffer = reader.readBuffer(FRAME_HEADER_LENGTH); receiveFrameListener.frameHeader(ctx, frameHeaderBuffer); @@ -291,6 +346,28 @@ private void readFrame() { if (frameHeader.length() == 0) { frameInProgress = BufferData.empty(); } else { + // Flow-control: reading frameHeader.length() bytes from HTTP2 socket for known stream ID. + int length = frameHeader.length(); + if (length > 0) { + int streamId = frameHeader.streamId(); + if (streamId > 0 && frameHeader.type() != Http2FrameType.HEADERS) { + // Stream ID > 0: update conenction and stream + FlowControl.Inbound inboundFlowControl = stream(streamId) + .stream() + .inboundFlowControl(); + inboundFlowControl.decrementWindowSize(length); + LOGGER.log(System.Logger.Level.INFO, () -> String.format( + "SRV IFC: Full decrement %d-> %d", length, + inboundFlowControl.getRemainingWindowSize())); + } else { + // Stream ID == 0: update connection only + inboundWindowSize.decrementWindowSize(length); + LOGGER.log(System.Logger.Level.INFO, () -> String.format( + "SRV IFC: Conn decrement %d -> %d", + length, inboundWindowSize.getRemainingWindowSize())); + } + } + frameInProgress = reader.readBuffer(frameHeader.length()); } @@ -342,12 +419,12 @@ private void doContinuation() { } private void writeServerSettings() { - connectionWriter.write(serverSettings.toFrameData(serverSettings, 0, Http2Flag.SettingsFlags.create(0)), - FlowControl.NOOP); + connectionWriter.write(serverSettings + .toFrameData(serverSettings, 0, Http2Flag.SettingsFlags.create(0)), FlowControl.Outbound.NOOP); state = State.READ_FRAME; } - private void windowUpdateFrame() { + private void readWindowUpdateFrame() { Http2WindowUpdate windowUpdate = Http2WindowUpdate.create(inProgressFrame()); receiveFrameListener.frame(ctx, windowUpdate); state = State.READ_FRAME; @@ -359,12 +436,14 @@ private void windowUpdateFrame() { // todo implement if (windowUpdate.windowSizeIncrement() == 0) { Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.PROTOCOL, "Window size 0"); - connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.NOOP); + connectionWriter.write(frame + .toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); } - overflow = connectionWindowSize.incrementWindowSize(windowUpdate.windowSizeIncrement()); + overflow = outboundWindowSize.incrementWindowSize(windowUpdate.windowSizeIncrement()); if (overflow) { Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.FLOW_CONTROL, "Window size too big. Max: "); - connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.NOOP); + connectionWriter.write(frame + .toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); } } else { try { @@ -376,6 +455,13 @@ private void windowUpdateFrame() { } } + // Used in inbound flow control instance to write WINDOW_UPDATE frame. + private void writeWindowUpdateFrame(Http2WindowUpdate windowUpdateFrame) { + connectionWriter.write(windowUpdateFrame + .toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); + LOGGER.log(System.Logger.Level.INFO, () -> String.format("SRV IFC: Connection WINDOW_UPDATE %s", windowUpdateFrame)); + } + private void doSettings() { if (frameHeader.streamId() != 0) { throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Settings must use stream ID 0, but use " + frameHeader.streamId()); @@ -401,11 +487,12 @@ private void doSettings() { //6.9.2/3 - legal range for the increment to the flow-control window is 1 to 2^31-1 (2,147,483,647) octets. if (it > WindowSize.MAX_WIN_SIZE) { Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.FLOW_CONTROL, "Window " + it + " size too large"); - connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.NOOP); + connectionWriter.write(frame + .toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); } //6.9.1/1 - changing the flow-control window for streams that are not yet active - streamInitialWindowSize = (int) it; + outboundInitialWindowSize = (int) it; //6.9.2/1 - SETTINGS frame can alter the initial flow-control // window size for streams with active flow-control windows (that is, @@ -413,12 +500,12 @@ private void doSettings() { for (StreamContext sctx : streams.values()) { Http2StreamState streamState = sctx.stream.streamState(); if (streamState == Http2StreamState.OPEN || streamState == Http2StreamState.HALF_CLOSED_REMOTE) { - sctx.stream.flowControl().resetStreamWindowSize(it); + sctx.stream.outboundFlowControl().resetStreamWindowSize(it); } } // Unblock frames waiting for update - this.connectionWindowSize.triggerUpdate(); + this.outboundWindowSize.triggerUpdate(); } this.clientSettings.presentValue(Http2Setting.MAX_FRAME_SIZE) @@ -442,7 +529,7 @@ private void doSettings() { + " exceeded hard limit value " + http2Config.maxConcurrentStreams()); connectionWriter.write( frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), - FlowControl.NOOP); + FlowControl.Outbound.NOOP); } }); @@ -457,7 +544,7 @@ private void doSettings() { private void ackSettings() { Http2Flag.SettingsFlags flags = Http2Flag.SettingsFlags.create(Http2Flag.ACK); Http2FrameHeader header = Http2FrameHeader.create(0, Http2FrameTypes.SETTINGS, flags, 0); - connectionWriter.write(new Http2FrameData(header, BufferData.empty()), FlowControl.NOOP); + connectionWriter.write(new Http2FrameData(header, BufferData.empty()), FlowControl.Outbound.NOOP); state = State.READ_FRAME; if (upgradeHeaders != null) { @@ -623,7 +710,7 @@ private void writePingAck() { Http2Flag.PingFlags.create(Http2Flag.ACK), 0); ping = null; - connectionWriter.write(new Http2FrameData(header, frame), FlowControl.NOOP); + connectionWriter.write(new Http2FrameData(header, frame), FlowControl.Outbound.NOOP); state = State.READ_FRAME; } @@ -686,6 +773,17 @@ private StreamContext stream(int streamId) { throw new Http2Exception(Http2ErrorCode.REFUSED_STREAM, "Maximum concurrent streams limit " + maxClientConcurrentStreams + " exceeded"); } + // Pass NOOP when flow control is turned off + FlowControl.Inbound.Builder inboundFlowControlBuilder = http2Config.flowControlEnabled() + ? FlowControl.builderInbound() + .streamId(streamId) + .connectionWindowsize(inboundWindowSize) + .streamWindowsize(inboundInitialWindowSize) + .streamMaxFrameSize(http2Config.maxFrameSize()) + // Pass NOOP when flow control is turned off (but we still have to send WINDOW_UPDATE frames) + : FlowControl.builderInbound() + .connectionWindowsize(inboundWindowSize) + .noop(); streamContext = new StreamContext(streamId, new Http2Stream(ctx, routing, @@ -695,9 +793,10 @@ private StreamContext stream(int streamId) { serverSettings, clientSettings, connectionWriter, - FlowControl.create(streamId, - streamInitialWindowSize, - connectionWindowSize))); + inboundFlowControlBuilder, + FlowControl.createOutbound(streamId, + outboundInitialWindowSize, + outboundWindowSize))); streams.put(streamId, streamContext); } diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ServerResponse.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ServerResponse.java index 366919c83a6..776eeca936a 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ServerResponse.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ServerResponse.java @@ -44,7 +44,7 @@ class Http2ServerResponse extends ServerResponseBase { private final Http2StreamWriter writer; private final int streamId; private final ServerResponseHeaders headers; - private final FlowControl flowControl; + private final FlowControl.Outbound flowControl; private boolean isSent; private boolean streamingEntity; @@ -55,7 +55,7 @@ class Http2ServerResponse extends ServerResponseBase { Http2ServerRequest request, Http2StreamWriter writer, int streamId, - FlowControl flowControl) { + FlowControl.Outbound flowControl) { super(ctx, request); this.ctx = ctx; this.writer = writer; @@ -177,7 +177,7 @@ private static class BlockingOutputStream extends OutputStream { private final ServerResponseHeaders headers; private final Http2StreamWriter writer; private final int streamId; - private final FlowControl flowControl; + private final FlowControl.Outbound flowControl; private final Http.Status status; private final Runnable responseCloseRunnable; @@ -189,7 +189,7 @@ private static class BlockingOutputStream extends OutputStream { private BlockingOutputStream(ServerResponseHeaders headers, Http2StreamWriter writer, int streamId, - FlowControl flowControl, + FlowControl.Outbound flowControl, Http.Status status, Runnable responseCloseRunnable) { diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java index fe4c698b2e1..599d7875b0a 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java @@ -61,7 +61,6 @@ public class Http2Stream implements Runnable, io.helidon.nima.http2.Http2Stream 0), BufferData.empty()); private static final System.Logger LOGGER = System.getLogger(Http2Stream.class.getName()); - private final FlowControl flowControl; private final ConnectionContext ctx; private final Http2Config http2Config; private final List subProviders; @@ -80,19 +79,22 @@ public class Http2Stream implements Runnable, io.helidon.nima.http2.Http2Stream private long expectedLength = -1; private HttpRouting routing; private HttpPrologue prologue; + private final FlowControl.Inbound inboundFlowControl; + private final FlowControl.Outbound outboundFlowControl; /** * A new HTTP/2 server stream. * - * @param ctx connection context - * @param routing HTTP routing - * @param http2Config HTTP/2 configuration + * @param ctx connection context + * @param routing HTTP routing + * @param http2Config HTTP/2 configuration * @param subProviders - * @param streamId stream id - * @param serverSettings server settings - * @param clientSettings client settings - * @param writer writer - * @param flowControl flow control + * @param streamId stream id + * @param serverSettings server settings + * @param clientSettings client settings + * @param writer writer + * @param inboundFlowControlBuilder inbound flow control builder + * @param outboundFlowControl outbound flow control */ public Http2Stream(ConnectionContext ctx, HttpRouting routing, @@ -102,7 +104,8 @@ public Http2Stream(ConnectionContext ctx, Http2Settings serverSettings, Http2Settings clientSettings, Http2StreamWriter writer, - FlowControl flowControl) { + FlowControl.Inbound.Builder inboundFlowControlBuilder, + FlowControl.Outbound outboundFlowControl) { this.ctx = ctx; this.routing = routing; this.http2Config = http2Config; @@ -112,7 +115,10 @@ public Http2Stream(ConnectionContext ctx, this.clientSettings = clientSettings; this.writer = writer; this.router = ctx.router(); - this.flowControl = flowControl; + this.inboundFlowControl = inboundFlowControlBuilder + .windowUpdateStreamWriter(this::writeWindowUpdate) + .build(); + this.outboundFlowControl = outboundFlowControl; } /** @@ -185,15 +191,22 @@ public void windowUpdate(Http2WindowUpdate windowUpdate) { //6.9/2 if (windowUpdate.windowSizeIncrement() == 0) { Http2RstStream frame = new Http2RstStream(Http2ErrorCode.PROTOCOL); - writer.write(frame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()), FlowControl.NOOP); + writer.write(frame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); } //6.9.1/3 - if (flowControl.incrementStreamWindowSize(windowUpdate.windowSizeIncrement())) { + if (outboundFlowControl.incrementStreamWindowSize(windowUpdate.windowSizeIncrement())) { Http2RstStream frame = new Http2RstStream(Http2ErrorCode.FLOW_CONTROL); - writer.write(frame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()), FlowControl.NOOP); + writer.write(frame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); } } + // Used in inbound flow control instance to write WINDOW_UPDATE frame. + void writeWindowUpdate(Http2WindowUpdate windowUpdate) { + writer.write(windowUpdate.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); + LOGGER.log(System.Logger.Level.INFO, () -> String.format("SRV IFC: Stream WINDOW_UPDATE %s", windowUpdate)); + } + + // this method is called from connection thread and start the // thread o this stream @Override @@ -216,7 +229,7 @@ public void data(Http2FrameHeader header, BufferData data) { if (expectedLength != -1 && expectedLength < header.length()) { state = Http2StreamState.CLOSED; Http2RstStream rst = new Http2RstStream(Http2ErrorCode.PROTOCOL); - writer.write(rst.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()), flowControl); + writer.write(rst.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()), outboundFlowControl); return; } if (expectedLength != -1) { @@ -249,8 +262,13 @@ public Http2StreamState streamState() { } @Override - public FlowControl flowControl() { - return flowControl; + public FlowControl.Outbound outboundFlowControl() { + return outboundFlowControl; + } + + @Override + public FlowControl.Inbound inboundFlowControl() { + return inboundFlowControl; } @Override @@ -262,7 +280,7 @@ public void run() { handle(); } catch (SocketWriterException | CloseConnectionException | UncheckedIOException e) { Http2RstStream rst = new Http2RstStream(Http2ErrorCode.STREAM_CLOSED); - writer.write(rst.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create()), flowControl); + writer.write(rst.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create()), outboundFlowControl); // no sense in throwing an exception, as this is invoked from an executor service directly } catch (RequestException e) { DirectHandler handler = ctx.listenerContext() @@ -284,7 +302,7 @@ public void run() { writer.writeHeaders(http2Headers, streamId, Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS | Http2Flag.END_OF_STREAM), - flowControl); + outboundFlowControl); } else { Http2FrameHeader dataHeader = Http2FrameHeader.create(message.length, Http2FrameTypes.DATA, @@ -294,7 +312,7 @@ public void run() { streamId, Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS), new Http2FrameData(dataHeader, BufferData.create(message)), - flowControl); + outboundFlowControl); } } finally { headers = null; @@ -374,7 +392,7 @@ private void handle() { decoder, streamId, this::readEntityFromPipeline); - Http2ServerResponse response = new Http2ServerResponse(ctx, request, writer, streamId, flowControl); + Http2ServerResponse response = new Http2ServerResponse(ctx, request, writer, streamId, outboundFlowControl); try { routing.route(ctx, request, response); } finally { diff --git a/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java b/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java index 0b88d3359b7..e029fc1296d 100644 --- a/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java +++ b/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java @@ -52,7 +52,7 @@ void testConnectionConfig() { WebServer.builder().addConnectionProvider(provider).build(); assertThat(provider.isConfig(), is(true)); Http2Config http2Config = provider.config(); - assertThat(http2Config.maxFrameSize(), is(8192L)); + assertThat(http2Config.maxFrameSize(), is(8192)); assertThat(http2Config.maxHeaderListSize(), is(4096L)); } @@ -62,7 +62,7 @@ void testProviderConfigBuilder() { Http2ConnectionSelector provider = (Http2ConnectionSelector) Http2ConnectionProvider.builder() .http2Config(DefaultHttp2Config.builder() - .maxFrameSize(4096L) + .maxFrameSize(4096) .maxHeaderListSize(2048L) .build()) .build() @@ -70,7 +70,7 @@ void testProviderConfigBuilder() { Http2Connection conn = (Http2Connection) provider.connection(mockContext()); // Verify values to be updated from configuration file - assertThat(conn.config().maxFrameSize(), is(4096L)); + assertThat(conn.config().maxFrameSize(), is(4096)); assertThat(conn.config().maxHeaderListSize(), is(2048L)); // Verify Http2Settings values to be updated from configuration file assertThat(conn.serverSettings().value(Http2Setting.MAX_FRAME_SIZE), is(4096L)); @@ -99,6 +99,40 @@ void testConfigValidatePath() { assertThat(http2Config.validatePath(), is(false)); } + // Verify that HTTP/2 flow control enabled is properly configured from configuration file + @Test + void testFlowControlEnabled() { + // This will pick up application.yaml from the classpath as default configuration file + TestProvider provider = new TestProvider(); + WebServer.builder().addConnectionProvider(provider).build(); + assertThat(provider.isConfig(), is(true)); + Http2Config http2Config = provider.config(); + assertThat(http2Config.flowControlEnabled(), is(false)); + } + + // Verify that HTTP/2 maximum connection-level window size is properly configured from configuration file + @Test + void testMaxWindowSize() { + // This will pick up application.yaml from the classpath as default configuration file + TestProvider provider = new TestProvider(); + WebServer.builder().addConnectionProvider(provider).build(); + assertThat(provider.isConfig(), is(true)); + Http2Config http2Config = provider.config(); + assertThat(http2Config.maxWindowSize(), is(32767)); + } + + // Verify that HTTP/2 maximum stream-level window size is properly configured from configuration file + @Test + void testMaxStreamWindowSize() { + // This will pick up application.yaml from the classpath as default configuration file + TestProvider provider = new TestProvider(); + WebServer.builder().addConnectionProvider(provider).build(); + assertThat(provider.isConfig(), is(true)); + Http2Config http2Config = provider.config(); + assertThat(http2Config.maxStreamWindowSize(), is(16383)); + } + + private static class TestProvider implements ServerConnectionProvider { private Http2Config http2Config = null; diff --git a/nima/http2/webserver/src/test/resources/application.yaml b/nima/http2/webserver/src/test/resources/application.yaml index 08918614969..8fc07bcf13d 100644 --- a/nima/http2/webserver/src/test/resources/application.yaml +++ b/nima/http2/webserver/src/test/resources/application.yaml @@ -23,4 +23,8 @@ server: max-frame-size: 8192 max-header-list-size: 4096 max-concurrent-streams: 16384 + flow-control-enabled: false + initial-window-size: 8192 + max-window-size: 32767 + max-stream-window-size: 16383 validate-path: false From cd691a8a45927e04739bbb3064608d4fe0da212d Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Thu, 9 Mar 2023 18:31:38 +0100 Subject: [PATCH 02/17] HTTP/2 Client --- .../helidon/common/uri/UriQueryWriteable.java | 8 +- .../common/uri/UriQueryWriteableImpl.java | 21 +- .../io/helidon/nima/http2/FlowControl.java | 9 +- .../helidon/nima/http2/FlowControlImpl.java | 55 +--- .../helidon/nima/http2/FlowControlNoop.java | 5 +- .../nima/http2/Http2ConnectionWriter.java | 89 +++--- .../io/helidon/nima/http2/Http2FrameData.java | 100 +++++- .../io/helidon/nima/http2/Http2Headers.java | 5 +- .../io/helidon/nima/http2/WindowSize.java | 3 +- .../io/helidon/nima/http2/WindowSizeImpl.java | 30 +- .../helidon/nima/http2/Http2HeadersTest.java | 2 +- nima/http2/webclient/pom.xml | 16 + .../http2/webclient/ClientRequestImpl.java | 41 ++- .../nima/http2/webclient/ConnectionKey.java | 26 ++ .../webclient/Http2ClientConnection.java | 289 +++++++++++++++++- .../Http2ClientConnectionHandler.java | 11 +- .../http2/webclient/Http2ClientStream.java | 184 ++++++++++- .../webclient/LockingStreamIdSequence.java | 35 +++ .../webclient/UpgradeRedirectException.java | 28 ++ .../webclient/src/main/java/module-info.java | 3 +- .../nima/http2/webclient/FlowControlTest.java | 139 +++++++++ .../http2/webclient/Http2WebClientTest.java | 267 ++++++++++++++++ .../src/test/resources/certificate.p12 | Bin 0 -> 2557 bytes .../test/resources/logging-test.properties | 28 ++ .../nima/http2/webserver/Http2Connection.java | 65 ++-- .../nima/http2/webserver/Http2Stream.java | 3 +- .../http2/client/Http2ClientTest.java | 8 +- .../integration/http2/client/PostTest.java | 45 ++- .../test/resources/logging-test.properties | 21 ++ .../io/helidon/nima/webclient/UriHelper.java | 11 +- .../webclient/http1/ClientRequestImpl.java | 1 - .../webclient/{ => http1}/ConnectionKey.java | 7 +- .../http1/Http1ClientConnection.java | 1 - .../helidon/nima/webclient/UriHelperTest.java | 16 +- 34 files changed, 1319 insertions(+), 253 deletions(-) create mode 100644 nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionKey.java create mode 100644 nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/LockingStreamIdSequence.java create mode 100644 nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/UpgradeRedirectException.java create mode 100644 nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/FlowControlTest.java create mode 100644 nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/Http2WebClientTest.java create mode 100644 nima/http2/webclient/src/test/resources/certificate.p12 create mode 100644 nima/http2/webclient/src/test/resources/logging-test.properties create mode 100644 nima/tests/integration/http2/client/src/test/resources/logging-test.properties rename nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/{ => http1}/ConnectionKey.java (86%) diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java index a32aab05db3..b5551c83ccd 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,13 +42,13 @@ static UriQueryWriteable create() { UriQueryWriteable set(String name, String... value); /** - * Add a new query parameter or add a value to existing. + * Add a new query parameter or add values to existing. * * @param name name of the parameter - * @param value additional value of the parameter + * @param value additional value(s) of the parameter * @return this instance */ - UriQueryWriteable add(String name, String value); + UriQueryWriteable add(String name, String... value); /** * Set a query parameter with values, if not already defined. diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java index 6ea4979ce5e..d7509a879c4 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -146,14 +146,21 @@ public UriQueryWriteable set(String name, String... values) { } @Override - public UriQueryWriteable add(String name, String value) { + public UriQueryWriteable add(String name, String... values) { String encodedName = UriEncoding.encodeUri(name); - String encodedValue = UriEncoding.encodeUri(value); - rawQueryParams.computeIfAbsent(encodedName, it -> new ArrayList<>(1)) - .add(encodedValue); - decodedQueryParams.computeIfAbsent(name, it -> new ArrayList<>(1)) - .add(value); + List decodedValues = new ArrayList<>(values.length); + List encodedValues = new ArrayList<>(values.length); + + for (String value : values) { + decodedValues.add(value); + encodedValues.add(UriEncoding.encodeUri(value)); + } + + rawQueryParams.computeIfAbsent(encodedName, it -> new ArrayList<>(values.length)) + .addAll(encodedValues); + decodedQueryParams.computeIfAbsent(name, it -> new ArrayList<>(values.length)) + .addAll(decodedValues); return this; } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java index 21f25a3a437..c3619f88cea 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java @@ -141,7 +141,7 @@ public Builder streamId(int streamId) { * @param windowSize HTTP/2 connection window size * @return this builder */ - public Builder connectionWindowsize(WindowSize.Inbound windowSize) { + public Builder connectionWindowSize(WindowSize.Inbound windowSize) { this.connectionWindowSize = windowSize; return this; } @@ -152,7 +152,7 @@ public Builder connectionWindowsize(WindowSize.Inbound windowSize) { * @param windowSize HTTP/2 stream window size * @return this builder */ - public Builder streamWindowsize(int windowSize) { + public Builder streamWindowSize(int windowSize) { this.streamWindowSize = windowSize; return this; } @@ -207,14 +207,13 @@ interface Outbound extends FlowControl { * @param frame frame to split * @return result */ - Http2FrameData[] split(Http2FrameData frame); + Http2FrameData[] cut(Http2FrameData frame); /** * Block until a window size update happens. * - * @return {@code true} if window update happened, {@code false} in case of timeout */ - boolean blockTillUpdate(); + void blockTillUpdate(); } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java index c118993004c..ca3d87edc79 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java @@ -18,8 +18,6 @@ import java.util.Objects; import java.util.function.Consumer; -import io.helidon.common.buffers.BufferData; - abstract class FlowControlImpl implements FlowControl { private final int streamId; @@ -43,9 +41,12 @@ public void resetStreamWindowSize(long increment) { @Override public int getRemainingWindowSize() { - return Integer.min( - connectionWindowSize().getRemainingWindowSize(), - streamWindowSize().getRemainingWindowSize()); + return Math.max(0, + Integer.min( + connectionWindowSize().getRemainingWindowSize(), + streamWindowSize().getRemainingWindowSize() + ) + ); } @Override @@ -128,49 +129,15 @@ public boolean incrementStreamWindowSize(int increment) { } @Override - public Http2FrameData[] split(Http2FrameData frame) { - return split(getRemainingWindowSize(), frame); + public Http2FrameData[] cut(Http2FrameData frame) { + return frame.cut(getRemainingWindowSize()); } @Override - public boolean blockTillUpdate() { - return connectionWindowSize.blockTillUpdate(); + public void blockTillUpdate() { + connectionWindowSize.blockTillUpdate(); + streamWindowSize.blockTillUpdate(); } - - private Http2FrameData[] split(int size, Http2FrameData frame) { - int length = frame.header().length(); - if (length <= size || length == 0) { - return new Http2FrameData[]{frame}; - } - - if (size == 0) { - return new Http2FrameData[0]; - } - - byte[] data1 = new byte[size]; - byte[] data2 = new byte[length - size]; - - frame.data().read(data1); - frame.data().read(data2); - - BufferData bufferData1 = BufferData.create(data1); - BufferData bufferData2 = BufferData.create(data2); - - Http2FrameData frameData1 = new Http2FrameData(Http2FrameHeader.create(bufferData1.available(), - Http2FrameTypes.DATA, - Http2Flag.DataFlags.create(0), - frame.header().streamId()), - bufferData1); - - Http2FrameData frameData2 = new Http2FrameData(Http2FrameHeader.create(bufferData2.available(), - Http2FrameTypes.DATA, - Http2Flag.DataFlags.create(0), - frame.header().streamId()), - bufferData2); - - return new Http2FrameData[]{frameData1, frameData2}; - } - } } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java index 155787917a8..6d3d74802a7 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java @@ -60,13 +60,12 @@ public boolean incrementStreamWindowSize(int increment) { } @Override - public Http2FrameData[] split(Http2FrameData frame) { + public Http2FrameData[] cut(Http2FrameData frame) { return new Http2FrameData[] {frame}; } @Override - public boolean blockTillUpdate() { - return false; + public void blockTillUpdate() { } } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java index 886ed07c752..1156272b02e 100755 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java @@ -35,7 +35,7 @@ public class Http2ConnectionWriter implements Http2StreamWriter { private final Lock streamLock = new ReentrantLock(true); private final SocketContext ctx; private final Http2FrameListener listener; - private final Http2Headers.DynamicTable responseDynamicTable; + private final Http2Headers.DynamicTable outboundDynamicTable; private final Http2HuffmanEncoder responseHuffman; private final BufferData headerBuffer = BufferData.growing(512); @@ -52,16 +52,17 @@ public Http2ConnectionWriter(SocketContext ctx, DataWriter writer, List { - noLockWrite(flowControl, frame); - return null; - }); + if (frame.header().type() == Http2FrameTypes.DATA.type()) { + splitAndWrite(frame, flowControl); + } else { + lockedWrite(frame); + } } @Override @@ -73,7 +74,7 @@ public int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlag return withStreamLock(() -> { int written = 0; headerBuffer.clear(); - headers.write(responseDynamicTable, responseHuffman, headerBuffer); + headers.write(outboundDynamicTable, responseHuffman, headerBuffer); Http2FrameHeader frameHeader = Http2FrameHeader.create(headerBuffer.available(), Http2FrameTypes.HEADERS, flags, @@ -81,7 +82,7 @@ public int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlag written += frameHeader.length(); written += Http2FrameHeader.LENGTH; - noLockWrite(flowControl, new Http2FrameData(frameHeader, headerBuffer)); + noLockWrite(new Http2FrameData(frameHeader, headerBuffer)); return written; }); @@ -101,7 +102,7 @@ public int writeHeaders(Http2Headers headers, int bytesWritten = 0; headerBuffer.clear(); - headers.write(responseDynamicTable, responseHuffman, headerBuffer); + headers.write(outboundDynamicTable, responseHuffman, headerBuffer); bytesWritten += headerBuffer.available(); Http2FrameHeader frameHeader = Http2FrameHeader.create(headerBuffer.available(), @@ -110,8 +111,8 @@ public int writeHeaders(Http2Headers headers, streamId); bytesWritten += Http2FrameHeader.LENGTH; - noLockWrite(flowControl, new Http2FrameData(frameHeader, headerBuffer)); - noLockWrite(flowControl, dataFrame); + noLockWrite(new Http2FrameData(frameHeader, headerBuffer)); + noLockWrite(dataFrame); bytesWritten += Http2FrameHeader.LENGTH; bytesWritten += dataFrame.header().length(); @@ -127,7 +128,14 @@ public int writeHeaders(Http2Headers headers, */ public void updateHeaderTableSize(long newSize) throws InterruptedException { withStreamLock(() -> { - responseDynamicTable.protocolMaxTableSize(newSize); + outboundDynamicTable.protocolMaxTableSize(newSize); + return null; + }); + } + + private void lockedWrite(Http2FrameData frame) { + withStreamLock(() -> { + noLockWrite(frame); return null; }); } @@ -145,40 +153,7 @@ private T withStreamLock(Callable callable) { } } - private void noLockWrite(FlowControl.Outbound flowControl, Http2FrameData frame) { - if (frame.header().type() == Http2FrameTypes.DATA.type()) { - splitAndWrite(frame, flowControl); - } else { - writeFrameInternal(frame); - } - } - - private void splitAndWrite(Http2FrameData frame, FlowControl.Outbound flowControl) { - Http2FrameData[] splitFrames = flowControl.split(frame); - if (splitFrames.length == 1) { - // windows are wide enough - writeFrameInternal(frame); - flowControl.decrementWindowSize(frame.header().length()); - } else if (splitFrames.length == 0) { - // block until window update - if (!flowControl.blockTillUpdate()) { - // no timeout - splitAndWrite(frame, flowControl); - } - } else if (splitFrames.length == 2) { - // write send-able part and block until window update with the rest - writeFrameInternal(splitFrames[0]); - flowControl.decrementWindowSize(frame.header().length()); - if (!flowControl.blockTillUpdate()) { - // no timeout - splitAndWrite(splitFrames[1], flowControl); - } else { - //TODO discarded frames after timeout - } - } - } - - private void writeFrameInternal(Http2FrameData frame) { + private void noLockWrite(Http2FrameData frame) { Http2FrameHeader frameHeader = frame.header(); listener.frameHeader(ctx, frameHeader); @@ -194,6 +169,28 @@ private void writeFrameInternal(Http2FrameData frame) { } } + private void splitAndWrite(Http2FrameData frame, FlowControl.Outbound flowControl) { + Http2FrameData currFrame = frame; + while (true) { + Http2FrameData[] splitFrames = flowControl.cut(currFrame); + if (splitFrames.length == 1) { + // windows are wide enough + lockedWrite(currFrame); + flowControl.decrementWindowSize(currFrame.header().length()); + break; + } else if (splitFrames.length == 0) { + // block until window update + flowControl.blockTillUpdate(); + } else if (splitFrames.length == 2) { + // write send-able part and block until window update with the rest + lockedWrite(splitFrames[0]); + flowControl.decrementWindowSize(currFrame.header().length()); + flowControl.blockTillUpdate(); + currFrame = splitFrames[1]; + } + } + } + // TODO use for fastpath // private void noLockWrite(Http2FrameData... frames) { // List toWrite = new LinkedList<>(); diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2FrameData.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2FrameData.java index fe8ec9140fc..1ff653c3738 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2FrameData.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2FrameData.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,4 +25,102 @@ * @param data frame data */ public record Http2FrameData(Http2FrameHeader header, BufferData data) { + + /** + * Split this frame to smaller frames of maximum frame size. + * + * @param size maximum frame size + * @return array of + */ + public Http2FrameData[] split(int size) { + int length = this.header().length(); + + // Already smaller than max size + if (length <= size || length == 0) { + return new Http2FrameData[] {this}; + } + + // Zero max size fast path + if (size == 0) { + return new Http2FrameData[0]; + } + + // End of stream flag is set only to the last frame in the array + boolean endOfStream = this.header().flags(Http2FrameTypes.DATA).endOfStream(); + + int lastFrameSize = length % size; + + // Avoid creating 0 length last frame + int allFrames = (length / size) + (lastFrameSize != 0 ? 1 : 0); + Http2FrameData[] splitFrames = new Http2FrameData[allFrames]; + + for (int i = 0; i < allFrames; i++) { + boolean lastFrame = allFrames == i + 1; + // only last frame can be smaller than max size + byte[] data = new byte[lastFrame ? lastFrameSize : size]; + this.data().read(data); + BufferData bufferData = BufferData.create(data); + splitFrames[i] = new Http2FrameData( + Http2FrameHeader.create(bufferData.available(), + Http2FrameTypes.DATA, + Http2Flag.DataFlags.create(endOfStream && lastFrame + ? Http2Flag.END_OF_STREAM + : 0), + this.header().streamId()), + bufferData); + } + return splitFrames; + } + + /** + * Cut the frame of given size from larger frame, + * returns two frames, first of given size, second with the rest of the data. + * + * @param size maximum frame size of the first frame + * @return array of 0,1 or 2 frames + */ + public Http2FrameData[] cut(int size) { + int length = this.header().length(); + + // Already smaller than max size + if (length <= size || length == 0) { + return new Http2FrameData[] {this}; + } + + // Zero max size fast path + if (size == 0) { + return new Http2FrameData[0]; + } + + // End of stream flag is set only to the last frame in the array + boolean endOfStream = this.header.flags(Http2FrameTypes.DATA).endOfStream(); + + byte[] data1 = new byte[size]; + byte[] data2 = new byte[length - size]; + + this.data().read(data1); + this.data().read(data2); + + BufferData bufferData1 = BufferData.create(data1); + BufferData bufferData2 = BufferData.create(data2); + + Http2FrameData frameData1 = + new Http2FrameData(Http2FrameHeader.create(bufferData1.available(), + Http2FrameTypes.DATA, + Http2Flag.DataFlags.create(0), + this.header().streamId()), + bufferData1); + + Http2FrameData frameData2 = + new Http2FrameData(Http2FrameHeader.create(bufferData2.available(), + Http2FrameTypes.DATA, + Http2Flag.DataFlags.create(endOfStream + ? Http2Flag.END_OF_STREAM + : 0), + this.header().streamId()), + bufferData2); + + return new Http2FrameData[] {frameData1, frameData2}; + } + } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Headers.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Headers.java index ae3c9d66010..e14cc43e9e0 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Headers.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Headers.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -782,9 +782,8 @@ enum StaticHeader implements IndexedHeaderRecord { if (predefinedHeader.hasValue()) { BY_NAME_VALUE.computeIfAbsent(predefinedHeader.headerName().lowerCase(), it -> new HashMap<>()) .put(predefinedHeader.value(), predefinedHeader); - } else { - BY_NAME_NO_VALUE.put(predefinedHeader.headerName().lowerCase(), predefinedHeader); } + BY_NAME_NO_VALUE.putIfAbsent(predefinedHeader.headerName().lowerCase(), predefinedHeader); } MAX_INDEX = maxIndex; diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java index 9c722d16440..f397422648e 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java @@ -124,9 +124,8 @@ interface Outbound extends WindowSize { /** * Block until window size update. * - * @return whether update happened before timeout */ - boolean blockTillUpdate(); + void blockTillUpdate(); } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java index 5ed50ea4a00..5b4fd2f9800 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java @@ -44,7 +44,10 @@ public void resetWindowSize(long size) { @Override public boolean incrementWindowSize(int increment) { - int remaining = remainingWindowSize.getAndUpdate(r -> MAX_WIN_SIZE - r > increment ? increment + r : MAX_WIN_SIZE); + int remaining = remainingWindowSize + .getAndUpdate(r -> r < 0 || MAX_WIN_SIZE - r > increment + ? increment + r + : MAX_WIN_SIZE); return MAX_WIN_SIZE - remaining <= increment; } @@ -80,7 +83,12 @@ static final class Inbound extends WindowSizeImpl implements WindowSize.Inbound @Override public boolean incrementWindowSize(int increment) { boolean result = super.incrementWindowSize(increment); - strategy.windowUpdate(increment); + // 6.9 + // A receiver MUST treat the receipt of a WINDOW_UPDATE frame + // with a flow-control window increment of 0 as a stream error + if (increment > 0) { + strategy.windowUpdate(increment); + } return result; } @@ -110,13 +118,13 @@ public void triggerUpdate() { } @Override - public boolean blockTillUpdate() { - try { - //TODO configurable timeout - updated.get().get(10, TimeUnit.SECONDS); - return false; - } catch (InterruptedException | ExecutionException | TimeoutException e) { - return true; + public void blockTillUpdate() { + while (getRemainingWindowSize() < 1){ + try { + //TODO configurable timeout + updated.get().get(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + } } } @@ -218,7 +226,9 @@ private enum Type { private static Type select(Context context) { // Bisection strategy requires at least 4 frames to be placed inside window - return context.maxFrameSize * 4 < context.maxWindowsize ? BISECTION : SIMPLE; + //FIXME: Find out why bisection strategy gets deadlocked +// return context.maxFrameSize * 4 < context.maxWindowsize ? BISECTION : SIMPLE; + return SIMPLE; } } diff --git a/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java b/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java index f7fd81b460a..14a0923d4b5 100644 --- a/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java +++ b/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java @@ -266,7 +266,7 @@ private Http2Stream stream() { return new Http2Stream() { private static final FlowControl.Inbound FC_IN_NOOP = FlowControl.builderInbound() .noop() - .connectionWindowsize(WindowSize.createInboundNoop(http2WindowUpdate -> {})) + .connectionWindowSize(WindowSize.createInboundNoop(http2WindowUpdate -> {})) .windowUpdateStreamWriter(http2WindowUpdate -> {}) .build(); diff --git a/nima/http2/webclient/pom.xml b/nima/http2/webclient/pom.xml index 6ff8dbca21e..1f544a7e635 100644 --- a/nima/http2/webclient/pom.xml +++ b/nima/http2/webclient/pom.xml @@ -60,6 +60,22 @@ hamcrest-all test + + io.helidon.nima.http2 + helidon-nima-http2-webserver + test + + + io.helidon.nima.testing.junit5 + + helidon-nima-testing-junit5-webserver + test + + + org.junit.jupiter + junit-jupiter-params + test + diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java index d1c4e456e47..594afa140f5 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java @@ -26,6 +26,7 @@ import java.util.function.Consumer; import java.util.function.Function; +import io.helidon.common.Version; import io.helidon.common.buffers.BufferData; import io.helidon.common.http.ClientRequestHeaders; import io.helidon.common.http.Http; @@ -38,10 +39,11 @@ import io.helidon.nima.http2.Http2Headers; import io.helidon.nima.webclient.ClientConnection; import io.helidon.nima.webclient.ClientRequest; -import io.helidon.nima.webclient.ConnectionKey; import io.helidon.nima.webclient.UriHelper; class ClientRequestImpl implements Http2ClientRequest { + static final HeaderValue USER_AGENT_HEADER = Header.create(Header.USER_AGENT, "Helidon Nima " + Version.VERSION); + //todo Gracefully close connections in channel cache private static final Map CHANNEL_CACHE = new ConcurrentHashMap<>(); private WritableHeaders explicitHeaders = WritableHeaders.create(); @@ -104,7 +106,8 @@ public Http2ClientRequest pathParam(String name, String value) { @Override public Http2ClientRequest queryParam(String name, String... values) { - throw new UnsupportedOperationException("Not implemented"); + query.add(name, values); + return this; } @Override @@ -127,6 +130,7 @@ public Http2ClientResponse submit(Object entity) { entityBytes = entityBytes(entity); } headers.set(Header.create(Header.CONTENT_LENGTH, entityBytes.length)); + headers.setIfAbsent(USER_AGENT_HEADER); Http2Headers http2Headers = prepareHeaders(headers); stream.write(http2Headers, entityBytes.length == 0); @@ -226,22 +230,33 @@ private Http2Headers prepareHeaders(WritableHeaders headers) { private Http2ClientStream reserveStream() { if (explicitConnection == null) { - ConnectionKey connectionKey = new ConnectionKey(uri.scheme(), - uri.host(), - uri.port(), - tls, - client.dnsResolver(), - client.dnsAddressLookup()); + return newStream(uri); + } else { + throw new UnsupportedOperationException("Explicit connection not (yet) supported for HTTP/2 client"); + } + } + + private Http2ClientStream newStream(UriHelper uri){ + try { + ConnectionKey connectionKey = new ConnectionKey(method, + uri.scheme(), + uri.host(), + uri.port(), + priorKnowledge, + tls, + client.dnsResolver(), + client.dnsAddressLookup()); // this statement locks all threads - must not do anything complicated (just create a new instance) return CHANNEL_CACHE.computeIfAbsent(connectionKey, - key -> new Http2ClientConnectionHandler(executor, - SocketOptions.builder().build(), - key)) + key -> new Http2ClientConnectionHandler(executor, + SocketOptions.builder().build(), + uri.path(), + key)) // this statement may block a single connection key .newStream(priorKnowledge, priority); - } else { - throw new UnsupportedOperationException("Explicit connection not (yet) supported for HTTP/2 client"); + } catch (UpgradeRedirectException e){ + return newStream(UriHelper.create(URI.create(e.redirectUri()), UriQueryWriteable.create())); } } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionKey.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionKey.java new file mode 100644 index 00000000000..9edcb1d4758 --- /dev/null +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionKey.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.http2.webclient; + +import io.helidon.common.http.Http; +import io.helidon.nima.common.tls.Tls; +import io.helidon.nima.webclient.DnsAddressLookup; +import io.helidon.nima.webclient.spi.DnsResolver; + +record ConnectionKey(Http.Method method, String scheme, String host, int port, boolean priorKnowledge, Tls tls, + DnsResolver dnsResolver, DnsAddressLookup dnsAddressLookup) { +} diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java index 68fa36f01d1..b7a9a07dc43 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,49 +22,106 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; +import java.nio.charset.StandardCharsets; import java.security.cert.Certificate; import java.security.cert.X509Certificate; +import java.util.ArrayDeque; +import java.util.Base64; +import java.util.HashMap; import java.util.HexFormat; import java.util.List; +import java.util.Map; +import java.util.Queue; import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocket; +import io.helidon.common.buffers.BufferData; +import io.helidon.common.buffers.DataReader; import io.helidon.common.buffers.DataWriter; +import io.helidon.common.http.Http; +import io.helidon.common.http.Http1HeadersParser; +import io.helidon.common.http.WritableHeaders; import io.helidon.common.socket.PlainSocket; import io.helidon.common.socket.SocketOptions; import io.helidon.common.socket.SocketWriter; import io.helidon.common.socket.TlsSocket; +import io.helidon.nima.http2.FlowControl; import io.helidon.nima.http2.Http2ConnectionWriter; -import io.helidon.nima.webclient.ConnectionKey; +import io.helidon.nima.http2.Http2ErrorCode; +import io.helidon.nima.http2.Http2Flag; +import io.helidon.nima.http2.Http2FrameData; +import io.helidon.nima.http2.Http2FrameHeader; +import io.helidon.nima.http2.Http2FrameListener; +import io.helidon.nima.http2.Http2GoAway; +import io.helidon.nima.http2.Http2Headers; +import io.helidon.nima.http2.Http2LoggingFrameListener; +import io.helidon.nima.http2.Http2Setting; +import io.helidon.nima.http2.Http2Settings; +import io.helidon.nima.http2.Http2WindowUpdate; +import io.helidon.nima.http2.WindowSize; import io.helidon.nima.webclient.spi.DnsResolver; import static java.lang.System.Logger.Level.DEBUG; import static java.lang.System.Logger.Level.TRACE; +import static java.lang.System.Logger.Level.WARNING; class Http2ClientConnection { private static final System.Logger LOGGER = System.getLogger(Http2ClientConnection.class.getName()); + private final Http2FrameListener sendListener = new Http2LoggingFrameListener("cl-send"); + private final Http2FrameListener recvListener = new Http2LoggingFrameListener("cl-recv"); + + private static final int FRAME_HEADER_LENGTH = 9; + + private static final String UPGRADE_REQ_MASK = """ + %s %s HTTP/1.1\r + Host: %s:%s\r + Connection: Upgrade, HTTP2-Settings\r + Upgrade: h2c\r + HTTP2-Settings: %s\r\n\r + """; + private static final byte[] PRIOR_KNOWLEDGE_PREFACE = + "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(StandardCharsets.UTF_8); + private final ExecutorService executor; private final SocketOptions socketOptions; private final ConnectionKey connectionKey; + private final String primaryPath; private final boolean priorKnowledge; + private final LockingStreamIdSequence streamIdSeq = new LockingStreamIdSequence(); + private final Map> buffer = new HashMap<>(); + private final Map streams = new HashMap<>(); + private final Lock connectionLock = new ReentrantLock(); private String channelId; private Socket socket; private PlainSocket helidonSocket; private Http2ConnectionWriter writer; private InputStream inputStream; + private DataReader reader; + private DataWriter dataWriter; + private Http2Headers.DynamicTable inboundDynamicTable = + Http2Headers.DynamicTable.create(Http2Setting.HEADER_TABLE_SIZE.defaultValue()); + private Future handleTask; + private int streamInitialWindowSize = WindowSize.DEFAULT_WIN_SIZE; + private final WindowSize.Outbound outboundConnectionWindowSize = WindowSize.createOutbound(); + private int maxFrameSize = 16_384; Http2ClientConnection(ExecutorService executor, SocketOptions socketOptions, ConnectionKey connectionKey, + String primaryPath, boolean priorKnowledge) { this.executor = executor; this.socketOptions = socketOptions; this.connectionKey = connectionKey; + this.primaryPath = primaryPath; this.priorKnowledge = priorKnowledge; } @@ -85,20 +142,123 @@ Http2ClientConnection connect() { return this; } + private Queue buffer(int streamId) { + return buffer.computeIfAbsent(streamId, i -> new ArrayDeque<>()); + } + + Http2FrameData readNextFrame(int streamId) { + try { + // Don't let streams to steal frame parts + // Always read whole frame(frameHeader+data) at once + connectionLock.lock(); + return buffer(streamId).poll(); + } finally { + connectionLock.unlock(); + } + } + + private void handle() { + this.reader.ensureAvailable(); + BufferData frameHeaderBuffer = this.reader.readBuffer(FRAME_HEADER_LENGTH); + Http2FrameHeader frameHeader = Http2FrameHeader.create(frameHeaderBuffer); + frameHeader.type().checkLength(frameHeader.length()); + BufferData data; + if (frameHeader.length() != 0) { + data = this.reader.readBuffer(frameHeader.length()); + } else { + data = BufferData.empty(); + } + switch (frameHeader.type()) { + case GO_AWAY: + Http2GoAway http2GoAway = Http2GoAway.create(data); + recvListener.frameHeader(helidonSocket, frameHeader); + recvListener.frame(helidonSocket, http2GoAway); + this.close(); + throw new IllegalStateException("Connection closed by the other side, error code: " + + http2GoAway.errorCode() + + " lastStreamId: " + http2GoAway.lastStreamId()); + + case SETTINGS: + Http2Settings http2Settings = Http2Settings.create(data); + recvListener.frameHeader(helidonSocket, frameHeader); + recvListener.frame(helidonSocket, http2Settings); + // §4.3.1 Endpoint communicates the size chosen by its HPACK decoder context + inboundDynamicTable.protocolMaxTableSize(http2Settings.value(Http2Setting.HEADER_TABLE_SIZE)); + //FIXME: MAX_FRAME_SIZE can be only int + if (http2Settings.hasValue(Http2Setting.MAX_FRAME_SIZE)) { + maxFrameSize = Math.toIntExact(http2Settings.value(Http2Setting.MAX_FRAME_SIZE)); + } + // §6.5.2 Update initial window size for new streams + if (http2Settings.hasValue(Http2Setting.INITIAL_WINDOW_SIZE)) { + Long initWinSize = http2Settings.value(Http2Setting.INITIAL_WINDOW_SIZE); + if (initWinSize > WindowSize.MAX_WIN_SIZE) { + goAway(frameHeader.streamId(), Http2ErrorCode.FLOW_CONTROL, "Window size too big. Max: "); + //FIXME: close connection? + return; + } + streamInitialWindowSize = Math.toIntExact(initWinSize); + } + // §6.5.3 Settings Synchronization + ackSettings(); + //FIXME: Other settings + return; + + case WINDOW_UPDATE: + Http2WindowUpdate http2WindowUpdate = Http2WindowUpdate.create(data); + recvListener.frameHeader(helidonSocket, frameHeader); + recvListener.frame(helidonSocket, http2WindowUpdate); + // Outbound flow-control window update + int increment = http2WindowUpdate.windowSizeIncrement(); + if (frameHeader.streamId() == 0) { + outboundConnectionWindowSize.incrementWindowSize(increment); + } else { + streams.get(frameHeader.streamId()) + .outboundFlowControl() + .incrementStreamWindowSize(increment); + } + return; + + default: + if (frameHeader.streamId() != 0) { + try { + // Don't let streams to steal frame parts + // Always read whole frame(frameHeader+data) at once + connectionLock.lock(); + buffer(frameHeader.streamId()).add(new Http2FrameData(frameHeader, data)); + } finally { + connectionLock.unlock(); + } + return; + } + + //FIXME: other frame types + LOGGER.log(WARNING, "Unsupported frame type!! " + frameHeader.type()); + } + + } + Http2ClientStream stream(int priority) { - return null; + //FIXME: priority + return new Http2ClientStream(this, + helidonSocket, + streamIdSeq); + } + + void addStream(int streamId, Http2ClientStream stream){ + this.streams.put(streamId, stream); } Http2ClientStream tryStream(int priority) { try { return stream(priority); - } catch (IllegalStateException e) { + } catch (IllegalStateException | UncheckedIOException e) { return null; } } void close() { try { + handleTask.cancel(true); socket.close(); } catch (IOException e) { e.printStackTrace(); @@ -106,9 +266,9 @@ void close() { } private void doConnect() throws IOException { - SSLSocket sslSocket = connectionKey.tls() == null - ? null - : connectionKey.tls().createSocket("h2"); + boolean useTls = "https".equals(connectionKey.scheme()) && connectionKey.tls() != null; + + SSLSocket sslSocket = useTls ? connectionKey.tls().createSocket("h2") : null; socket = sslSocket == null ? new Socket() : sslSocket; socketOptions.configureSocket(socket); @@ -126,9 +286,10 @@ private void doConnect() throws IOException { ? PlainSocket.client(socket, channelId) : TlsSocket.client(sslSocket, channelId); - DataWriter writer = SocketWriter.create(executor, helidonSocket, 32); + dataWriter = SocketWriter.create(executor, helidonSocket, 32); + this.reader = new DataReader(helidonSocket); inputStream = socket.getInputStream(); - this.writer = new Http2ConnectionWriter(helidonSocket, writer, List.of()); + this.writer = new Http2ConnectionWriter(helidonSocket, dataWriter, List.of()); if (sslSocket != null) { sslSocket.startHandshake(); @@ -141,6 +302,96 @@ private void doConnect() throws IOException { throw new IllegalStateException("Failed to negotiate h2 protocol. Protocol from socket: " + negotiatedProtocol); } } + + if (!priorKnowledge && !useTls) { + httpUpgrade(); + // Settings are part of the HTTP/1 upgrade request + sendPreface(false); + } else { + sendPreface(true); + } + + handleTask = java.util.concurrent.Executors.newSingleThreadExecutor().submit(() -> { +// handleTask = executor.submit(() -> { + while (!Thread.interrupted()) { + handle(); + } + System.out.println("Client listener interrupted!!!"); + }); + } + + private void ackSettings() { + Http2Flag.SettingsFlags flags = Http2Flag.SettingsFlags.create(Http2Flag.ACK); + Http2Settings http2Settings = Http2Settings.create(); + Http2FrameData frameData = http2Settings.toFrameData(null, 0, flags); + sendListener.frameHeader(helidonSocket, frameData.header()); + sendListener.frame(helidonSocket, http2Settings); + writer.write(frameData, FlowControl.Outbound.NOOP); + } + + private void goAway(int streamId, Http2ErrorCode errorCode, String msg) { + Http2Settings http2Settings = Http2Settings.create(); + Http2GoAway frame = new Http2GoAway(streamId, errorCode, msg); + writer.write(frame.toFrameData(http2Settings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); + } + + private void sendPreface(boolean sendSettings){ + dataWriter.writeNow(BufferData.create(PRIOR_KNOWLEDGE_PREFACE)); + if (sendSettings) { + // §3.5 Preface bytes must be followed by setting frame + Http2Settings http2Settings = Http2Settings.builder() + .add(Http2Setting.MAX_HEADER_LIST_SIZE, 8192L) + .add(Http2Setting.ENABLE_PUSH, false) + // .add(Http2Setting., false) + .build(); + Http2Flag.SettingsFlags flags = Http2Flag.SettingsFlags.create(0); + Http2FrameData frameData = http2Settings.toFrameData(null, 0, flags); + sendListener.frameHeader(helidonSocket, frameData.header()); + sendListener.frame(helidonSocket, http2Settings); + writer.write(frameData, FlowControl.Outbound.NOOP); + } + // todo win update it needed after prolog? + // win update + Http2WindowUpdate windowUpdate = new Http2WindowUpdate(10000); + Http2Flag.NoFlags flags = Http2Flag.NoFlags.create(); + Http2FrameData frameData = windowUpdate.toFrameData(null, 0, flags); + sendListener.frameHeader(helidonSocket, frameData.header()); + sendListener.frame(helidonSocket, windowUpdate); + writer.write(frameData, FlowControl.Outbound.NOOP); + } + + private void httpUpgrade() { + Http2FrameData settingsFrame = Http2Settings.create().toFrameData(null, 0, Http2Flag.SettingsFlags.create(0)); + BufferData upgradeRequest = createUpgradeRequest(settingsFrame); + sendListener.frame(helidonSocket, upgradeRequest); + dataWriter.writeNow(upgradeRequest); + reader.skip("HTTP/1.1 ".length()); + String status = reader.readAsciiString(3); + String message = reader.readLine(); + WritableHeaders headers = Http1HeadersParser.readHeaders(reader, 8192, false); + switch (status) { + case "101": + break; + case "301": + throw new UpgradeRedirectException(headers.get(Http.Header.LOCATION).value()); + default: + close(); + throw new IllegalStateException("Upgrade to HTTP/2 failed: " + message); + } + } + + private BufferData createUpgradeRequest(Http2FrameData settingsFrame) { + BufferData settingsData = settingsFrame.data(); + byte[] b = new byte[settingsData.available()]; + settingsData.write(b); + + return BufferData.create(UPGRADE_REQ_MASK.formatted( + connectionKey.method().text(), + this.primaryPath != null && !"".equals(this.primaryPath) ? primaryPath : "/", + connectionKey.host(), + connectionKey.port(), + Base64.getEncoder().encodeToString(b) + )); } private void debugTls(SSLSocket sslSocket) { @@ -192,4 +443,24 @@ private String certsToString(Certificate[] peerCertificates) { return String.join(", ", certs); } + + public Http2ConnectionWriter getWriter() { + return writer; + } + + public Http2Headers.DynamicTable getInboundDynamicTable() { + return this.inboundDynamicTable; + } + + int streamInitialWindowSize() { + return streamInitialWindowSize; + } + + WindowSize.Outbound outboundConnectionWindowSize() { + return outboundConnectionWindowSize; + } + + public int maxFrameSize() { + return maxFrameSize; + } } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnectionHandler.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnectionHandler.java index 8d7eed678b9..4a19e808596 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnectionHandler.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnectionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import java.util.concurrent.atomic.AtomicReference; import io.helidon.common.socket.SocketOptions; -import io.helidon.nima.webclient.ConnectionKey; // a representation of a single remote endpoint // this may use one or more connections (depending on parallel streams) @@ -33,16 +32,19 @@ class Http2ClientConnectionHandler { private final ExecutorService executor; private final SocketOptions socketOptions; + private String primaryPath; private final ConnectionKey connectionKey; private final AtomicReference activeConnection = new AtomicReference<>(); - // simple solutio for now + // simple solution for now private final Semaphore semaphore = new Semaphore(1); Http2ClientConnectionHandler(ExecutorService executor, SocketOptions socketOptions, + String primaryPath, ConnectionKey connectionKey) { this.executor = executor; this.socketOptions = socketOptions; + this.primaryPath = primaryPath; this.connectionKey = connectionKey; } @@ -74,7 +76,8 @@ public Http2ClientStream newStream(boolean priorKnowledge, int priority) { } private Http2ClientConnection createConnection(ConnectionKey connectionKey, boolean priorKnowledge) { - Http2ClientConnection conn = new Http2ClientConnection(executor, socketOptions, connectionKey, priorKnowledge); + Http2ClientConnection conn = + new Http2ClientConnection(executor, socketOptions, connectionKey, primaryPath, priorKnowledge); conn.connect(); activeConnection.set(conn); fullConnections.add(conn); diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java index 23844a52bc6..2235b4d4a19 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,14 @@ import java.io.OutputStream; import io.helidon.common.buffers.BufferData; +import io.helidon.common.http.ClientRequestHeaders; +import io.helidon.common.http.ClientResponseHeaders; +import io.helidon.common.http.WritableHeaders; import io.helidon.common.socket.SocketContext; +import io.helidon.nima.http.encoding.ContentDecoder; +import io.helidon.nima.http.media.MediaContext; import io.helidon.nima.http.media.ReadableEntityBase; +import io.helidon.nima.http2.FlowControl; import io.helidon.nima.http2.Http2ErrorCode; import io.helidon.nima.http2.Http2Flag; import io.helidon.nima.http2.Http2FrameData; @@ -30,25 +36,37 @@ import io.helidon.nima.http2.Http2FrameType; import io.helidon.nima.http2.Http2FrameTypes; import io.helidon.nima.http2.Http2Headers; +import io.helidon.nima.http2.Http2HuffmanDecoder; import io.helidon.nima.http2.Http2LoggingFrameListener; +import io.helidon.nima.http2.Http2Priority; import io.helidon.nima.http2.Http2RstStream; import io.helidon.nima.http2.Http2Settings; +import io.helidon.nima.http2.Http2Stream; import io.helidon.nima.http2.Http2StreamState; +import io.helidon.nima.http2.Http2WindowUpdate; +import io.helidon.nima.webclient.ClientResponseEntity; -class Http2ClientStream { - private final Http2ClientConnection myConnection; +class Http2ClientStream implements Http2Stream { + + private static final System.Logger LOGGER = System.getLogger(Http2ClientStream.class.getName()); + private final Http2ClientConnection connection; private final SocketContext ctx; - private final int streamId; + private final LockingStreamIdSequence streamIdSeq; + private FlowControl.Outbound outboundFlowControl; private final Http2FrameListener sendListener = new Http2LoggingFrameListener("cl-send"); private final Http2FrameListener recvListener = new Http2LoggingFrameListener("cl-recv"); // todo configure private final Http2Settings settings = Http2Settings.create(); private volatile Http2StreamState state = Http2StreamState.IDLE; + private Http2Headers currentHeaders; + private int streamId; - Http2ClientStream(Http2ClientConnection myConnection, SocketContext ctx, int streamId) { - this.myConnection = myConnection; + Http2ClientStream(Http2ClientConnection connection, + SocketContext ctx, + LockingStreamIdSequence streamIdSeq) { + this.connection = connection; this.ctx = ctx; - this.streamId = streamId; + this.streamIdSeq = streamIdSeq; } void cancel() { @@ -60,30 +78,120 @@ void cancel() { } ReadableEntityBase entity() { + return ClientResponseEntity.create( + ContentDecoder.NO_OP, + this::read, + this::close, + ClientRequestHeaders.create(WritableHeaders.create()), + ClientResponseHeaders.create(WritableHeaders.create()), + MediaContext.create() + ); + } + + void close() { + //todo cleanup + } + + Http2FrameData readOne() { + Http2FrameData frameData = connection.readNextFrame(streamId); + + if (frameData != null) { + + recvListener.frameHeader(ctx, frameData.header()); + recvListener.frame(ctx, frameData.data()); + + int flags = frameData.header().flags(); + if ((flags & Http2Flag.END_OF_STREAM) == Http2Flag.END_OF_STREAM) { + state = Http2StreamState.CLOSED; + } + + switch (frameData.header().type()) { + case DATA: + return frameData; + case HEADERS: + var requestHuffman = new Http2HuffmanDecoder(); + currentHeaders = Http2Headers.create(this, connection.getInboundDynamicTable(), requestHuffman, frameData); + break; + case RST_STREAM: + state = Http2StreamState.CLOSED; + //FIXME: Kill just the stream + throw new RuntimeException("Reset of " + streamId + " stream received!"); + default: + //FIXME: Settings, outbound flow control + LOGGER.log(System.Logger.Level.DEBUG, "Dropping frame " + frameData.header() + " expected header or data."); + } + } return null; } + BufferData read(int i) { + while (state == Http2StreamState.HALF_CLOSED_LOCAL) { + Http2FrameData frameData = readOne(); + if (frameData != null) { + return frameData.data(); + } + } + return BufferData.empty(); + } + void write(Http2Headers http2Headers, boolean endOfStream) { this.state = Http2StreamState.checkAndGetState(this.state, Http2FrameType.HEADERS, true, endOfStream, true); - //myConnection.writeHeaders(streamId, sendListener, http2Headers, endOfStream); + Http2Flag.HeaderFlags flags; + if (endOfStream) { + flags = Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS | Http2Flag.END_OF_STREAM); + } else { + flags = Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS); + } + + sendListener.headers(ctx, http2Headers); + try { + // Keep ascending streamId order among concurrent streams + // §5.1.1 - The identifier of a newly established stream MUST be numerically + // greater than all streams that the initiating endpoint has opened or reserved. + this.streamId = streamIdSeq.lockAndNext(); + outboundFlowControl = FlowControl.createOutbound(streamId, + connection.streamInitialWindowSize(), + connection.outboundConnectionWindowSize()); + this.connection.addStream(streamId, this); + // First call to the server-starting stream, needs to be increasing sequence of odd numbers + connection.getWriter().writeHeaders(http2Headers, streamId, flags, outboundFlowControl); + } finally { + streamIdSeq.unlock(); + } } void writeData(BufferData entityBytes, boolean endOfStream) { - // todo split to frames if bigger than max frame size - // todo handle flow control Http2FrameHeader frameHeader = Http2FrameHeader.create(entityBytes.available(), Http2FrameTypes.DATA, Http2Flag.DataFlags.create(endOfStream ? Http2Flag.END_OF_STREAM : 0), streamId); - sendListener.frameHeader(ctx, frameHeader); - sendListener.frame(ctx, entityBytes); - write(new Http2FrameData(frameHeader, entityBytes), endOfStream); + Http2FrameData frameData = new Http2FrameData(frameHeader, entityBytes); + splitAndWrite(frameData, endOfStream); + } + + void splitAndWrite(Http2FrameData frameData, boolean endOfStream) { + // todo handle flow control + int maxFrameSize = connection.maxFrameSize(); + + var frm = frameData; + + // Split to frames if bigger than max frame size + Http2FrameData[] frames = frm.split(maxFrameSize); + for (Http2FrameData frame : frames) { + write(frame, frame.header().flags(Http2FrameTypes.DATA).endOfStream()); + } } Http2Headers readHeaders() { - return null; + while (currentHeaders == null) { + Http2FrameData frameData = readOne(); + if (frameData != null) { + throw new IllegalStateException("Unexpected frame type " + frameData.header() + ", HEADERS are expected."); + } + } + return currentHeaders; } ClientOutputStream outputStream() { @@ -96,6 +204,54 @@ private void write(Http2FrameData frameData, boolean endOfStream) { true, endOfStream, false); + connection.getWriter().write(frameData, outboundFlowControl()); + } + + @Override + public void rstStream(Http2RstStream rstStream) { + //FIXME: reset stream + } + + @Override + public void windowUpdate(Http2WindowUpdate windowUpdate) { + //FIXME: win update + } + + @Override + public void headers(Http2Headers headers, boolean endOfStream) { + throw new UnsupportedOperationException("Not applicable on client."); + } + + @Override + public void data(Http2FrameHeader header, BufferData data) { + throw new UnsupportedOperationException("Not applicable on client."); + } + + @Override + public void priority(Http2Priority http2Priority) { + //FIXME: priority + } + + @Override + public int streamId() { + return streamId; + } + + @Override + public Http2StreamState streamState() { + //FIXME: State check + throw new UnsupportedOperationException("Not implemented yet!"); + } + + @Override + public FlowControl.Outbound outboundFlowControl() { + return outboundFlowControl; + } + + @Override + public FlowControl.Inbound inboundFlowControl() { + //FIXME: inbound flow control + return null; } class ClientOutputStream extends OutputStream { diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/LockingStreamIdSequence.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/LockingStreamIdSequence.java new file mode 100644 index 00000000000..32bb2d50725 --- /dev/null +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/LockingStreamIdSequence.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.http2.webclient; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +class LockingStreamIdSequence { + + private final AtomicInteger streamIdSeq = new AtomicInteger(0); + private final Lock lock = new ReentrantLock(); + + int lockAndNext() { + lock.lock(); + return streamIdSeq.updateAndGet(o -> o % 2 == 0 ? o + 1 : o + 2); + } + + void unlock(){ + lock.unlock(); + } +} diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/UpgradeRedirectException.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/UpgradeRedirectException.java new file mode 100644 index 00000000000..6fef921112b --- /dev/null +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/UpgradeRedirectException.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.http2.webclient; + +class UpgradeRedirectException extends RuntimeException { + private final String redirectUri; + + UpgradeRedirectException(String redirectUri) { + this.redirectUri = redirectUri; + } + + String redirectUri() { + return redirectUri; + } +} diff --git a/nima/http2/webclient/src/main/java/module-info.java b/nima/http2/webclient/src/main/java/module-info.java index d8a06403a8c..282ef887e82 100644 --- a/nima/http2/webclient/src/main/java/module-info.java +++ b/nima/http2/webclient/src/main/java/module-info.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ requires transitive io.helidon.nima.http2; requires transitive io.helidon.nima.webclient; + requires transitive io.helidon.common.pki; exports io.helidon.nima.http2.webclient; } diff --git a/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/FlowControlTest.java b/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/FlowControlTest.java new file mode 100644 index 00000000000..b57ace433d1 --- /dev/null +++ b/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/FlowControlTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.http2.webclient; + +import java.io.InputStream; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; + +import io.helidon.nima.http2.webserver.Http2Route; +import io.helidon.nima.testing.junit5.webserver.ServerTest; +import io.helidon.nima.testing.junit5.webserver.SetUpServer; +import io.helidon.nima.webserver.WebServer; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.http.Http.Method.PUT; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@ServerTest +public class FlowControlTest { + + private static volatile CompletableFuture flowControlServerLatch = new CompletableFuture<>(); + private static volatile CompletableFuture flowControlClientLatch = new CompletableFuture<>(); + private static final ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor(); + private static final int TIMEOUT_SEC = 15; + private final WebServer server; + + @SetUpServer + static void setUpServer(WebServer.Builder serverBuilder) { + serverBuilder + .defaultSocket(builder -> builder.port(-1) + .host("localhost") + ) + .routing(router -> router + .route(Http2Route.route(PUT, "/flow-control", (req, res) -> { + StringBuilder sb = new StringBuilder(); + AtomicLong cnt = new AtomicLong(); + InputStream is = req.content().inputStream(); + for (byte[] b = is.readNBytes(5_000); + b.length != 0; + b = is.readNBytes(5_000)) { + int lastLength = b.length; + long receivedData = cnt.updateAndGet(o -> o + lastLength); + if (receivedData > 0) { + // Unblock client to assert sent data + flowControlClientLatch.complete(null); + // Block server, give client time to assert + flowControlServerLatch.join(); + } + sb.append(new String(b)); + } + is.close(); + res.send(sb.toString()); + })) + ); + } + + FlowControlTest(WebServer server) { + this.server = server; + } + + @Test + void flowControl() throws ExecutionException, InterruptedException, TimeoutException { + flowControlServerLatch = new CompletableFuture<>(); + flowControlClientLatch = new CompletableFuture<>(); + AtomicLong sentData = new AtomicLong(); + + var client = Http2Client.builder() + .priorKnowledge(true) + .baseUri("http://localhost:" + server.port()) + .build(); + + String data10k = "Helidon!!!".repeat(1_000); + + var req = client.method(PUT) + .path("/flow-control"); + + CompletableFuture responded = new CompletableFuture<>(); + + exec.submit(() -> { + try (var res = req + .outputStream( + out -> { + for (int i = 0; i < 5; i++) { + byte[] bytes = data10k.getBytes(); + out.write(bytes); + sentData.updateAndGet(o -> o + bytes.length); + } + for (int i = 0; i < 5; i++) { + byte[] bytes = data10k.toUpperCase().getBytes(); + out.write(bytes); + sentData.updateAndGet(o -> o + bytes.length); + } + out.close(); + } + )) { + responded.complete(res.as(String.class)); + } + }); + + flowControlClientLatch.get(TIMEOUT_SEC, TimeUnit.SECONDS); + // Now client can't send more, because server didn't ask for it (Window update) + Thread.sleep(150); + assertThat(sentData.get(), is(70_000L)); + // Let server ask for the rest of the data + flowControlServerLatch.complete(null); + String response = responded.get(TIMEOUT_SEC, TimeUnit.SECONDS); + assertThat(sentData.get(), is(100_000L)); + assertThat(response, is(data10k.repeat(5) + data10k.toUpperCase().repeat(5))); + } + + @AfterAll + static void afterAll() throws InterruptedException { + exec.shutdown(); + if (!exec.awaitTermination(TIMEOUT_SEC, TimeUnit.SECONDS)) { + exec.shutdownNow(); + } + } +} diff --git a/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/Http2WebClientTest.java b/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/Http2WebClientTest.java new file mode 100644 index 00000000000..34e6512178b --- /dev/null +++ b/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/Http2WebClientTest.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.http2.webclient; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import io.helidon.common.LazyValue; +import io.helidon.common.configurable.Resource; +import io.helidon.common.http.Http; +import io.helidon.common.pki.KeyConfig; +import io.helidon.nima.common.tls.Tls; +import io.helidon.nima.http2.webserver.DefaultHttp2Config; +import io.helidon.nima.http2.webserver.Http2ConnectionProvider; +import io.helidon.nima.http2.webserver.Http2Route; +import io.helidon.nima.testing.junit5.webserver.ServerTest; +import io.helidon.nima.testing.junit5.webserver.SetUpServer; +import io.helidon.nima.webserver.WebServer; +import io.helidon.nima.webserver.http1.Http1Route; + +import static io.helidon.common.http.Http.Header.USER_AGENT; +import static io.helidon.common.http.Http.Method.GET; +import static io.helidon.common.http.Http.Method.POST; +import static io.helidon.common.http.Http.Method.PUT; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +@ServerTest +class Http2WebClientTest { + + private static final Http.HeaderName CLIENT_CUSTOM_HEADER_NAME = Http.Header.create("client-custom-header"); + private static final Http.HeaderName SERVER_CUSTOM_HEADER_NAME = Http.Header.create("server-custom-header"); + private static final Http.HeaderName SERVER_HEADER_FROM_PARAM_NAME = Http.Header.create("header-from-param"); + private static final Http.HeaderName CLIENT_USER_AGENT_HEADER_NAME = Http.Header.create("client-user-agent"); + private static ExecutorService executorService; + private static int plainPort; + private static int tlsPort; + + Http2WebClientTest(WebServer server) { + this.plainPort = server.port(); + this.tlsPort = server.port("https"); + } + + @SetUpServer + static void setUpServer(WebServer.Builder serverBuilder) { + executorService = Executors.newFixedThreadPool(5); + + KeyConfig privateKeyConfig = + KeyConfig.keystoreBuilder() + .keystore(Resource.create("certificate.p12")) + .keystorePassphrase("helidon") + .build(); + + Tls tls = Tls.builder() + .privateKey(privateKeyConfig.privateKey().get()) + .privateKeyCertChain(privateKeyConfig.certChain()) + .build(); + + serverBuilder + .defaultSocket(builder -> builder.port(-1) + .host("localhost") + ) + .addConnectionProvider(Http2ConnectionProvider.builder() + .http2Config(DefaultHttp2Config.builder() + .flowControlEnabled(true) + .maxStreamWindowSize(10)) + .build()) + .socket("https", + builder -> builder.port(-1) + .host("localhost") + .tls(tls) + .receiveBufferSize(4096) + .backlog(8192) + ) + .routing(router -> router + .get("/", (req, res) -> res.send("Hello world!")) + .route(Http1Route.route(GET, "/versionspecific", (req, res) -> res.send("HTTP/1.1 route"))) + .route(Http2Route.route(GET, "/versionspecific", (req, res) -> { + res.header(CLIENT_USER_AGENT_HEADER_NAME, req.headers().get(USER_AGENT).value()); + res.header(SERVER_HEADER_FROM_PARAM_NAME, req.query().value("custQueryParam")); + res.send("HTTP/2 route"); + })) + .route(Http2Route.route(PUT, "/versionspecific", (req, res) -> { + res.header(SERVER_CUSTOM_HEADER_NAME, req.headers().get(CLIENT_CUSTOM_HEADER_NAME).value()); + res.header(CLIENT_USER_AGENT_HEADER_NAME, req.headers().get(USER_AGENT).value()); + res.header(SERVER_HEADER_FROM_PARAM_NAME, req.query().value("custQueryParam")); + res.send("PUT " + req.content().as(String.class)); + })) + .route(Http2Route.route(POST, "/versionspecific", (req, res) -> { + res.header(SERVER_CUSTOM_HEADER_NAME, req.headers().get(CLIENT_CUSTOM_HEADER_NAME).value()); + res.header(CLIENT_USER_AGENT_HEADER_NAME, req.headers().get(USER_AGENT).value()); + res.header(SERVER_HEADER_FROM_PARAM_NAME, req.query().value("custQueryParam")); + res.send("POST " + req.content().as(String.class)); + })) + .route(Http2Route.route(GET, "/versionspecific/h2streaming", (req, res) -> { + res.status(Http.Status.OK_200); + String execId = req.query().value("execId"); + try (OutputStream os = res.outputStream()) { + for (int i = 0; i < 5; i++) { + os.write(String.format(execId + "BAF%03d", i).getBytes()); + Thread.sleep(10); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + })) + ); + } + + private static final LazyValue priorKnowledgeClient = LazyValue.create(() -> Http2Client.builder() + .priorKnowledge(true) + .baseUri("http://localhost:" + plainPort + "/versionspecific") + .build()); + private static final LazyValue upgradeClient = LazyValue.create(() -> Http2Client.builder() + .baseUri("http://localhost:" + plainPort + "/versionspecific") + .build()); + private static final LazyValue tlsClient = LazyValue.create(() -> Http2Client.builder() + .baseUri("https://localhost:" + tlsPort + "/versionspecific") + .tls(Tls.builder() + .enabled(true) + .trustAll(true) + .endpointIdentificationAlgorithm(Tls.ENDPOINT_IDENTIFICATION_NONE) + .build()) + .build()); + + private static Stream clientTypes() { + return Stream.of( + Arguments.of("priorKnowledge", priorKnowledgeClient), + Arguments.of("upgrade", upgradeClient), + Arguments.of("tls", tlsClient) + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("clientTypes") + void clientGet(String name, LazyValue client) { + try (Http2ClientResponse response = client.get() + .get() + .queryParam("custQueryParam", "test-get") + .request()) { + + assertThat(response.status(), is(Http.Status.OK_200)); + assertThat(response.as(String.class), is("HTTP/2 route")); + assertThat(response.headers().get(CLIENT_USER_AGENT_HEADER_NAME).value(), + is(ClientRequestImpl.USER_AGENT_HEADER.value())); + assertThat(response.headers().get(SERVER_HEADER_FROM_PARAM_NAME).value(), + is("test-get")); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("clientTypes") + void clientPut(String clientType, LazyValue client) { + + String payload = clientType + " payload"; + String custHeaderValue = clientType + " header value"; + + try (Http2ClientResponse response = client.get() + .method(PUT) + .queryParam("custQueryParam", "test-put") + .header(CLIENT_CUSTOM_HEADER_NAME, custHeaderValue) + .submit(payload)) { + + assertThat(response.status(), is(Http.Status.OK_200)); + assertThat(response.as(String.class), is("PUT " + payload)); + assertThat(response.headers().get(CLIENT_USER_AGENT_HEADER_NAME).value(), + is(ClientRequestImpl.USER_AGENT_HEADER.value())); + assertThat(response.headers().get(SERVER_CUSTOM_HEADER_NAME).value(), + is(custHeaderValue)); + assertThat(response.headers().get(SERVER_HEADER_FROM_PARAM_NAME).value(), + is("test-put")); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("clientTypes") + void clientPost(String clientType, LazyValue client) { + + String payload = clientType + " payload"; + String custHeaderValue = clientType + " header value"; + + try (Http2ClientResponse response = client.get() + .method(POST) + .queryParam("custQueryParam", "test-post") + .header(CLIENT_CUSTOM_HEADER_NAME, custHeaderValue) + .submit(payload)) { + + assertThat(response.status(), is(Http.Status.OK_200)); + assertThat(response.as(String.class), is("POST " + payload)); + assertThat(response.headers().get(CLIENT_USER_AGENT_HEADER_NAME).value(), + is(ClientRequestImpl.USER_AGENT_HEADER.value())); + assertThat(response.headers().get(SERVER_CUSTOM_HEADER_NAME).value(), + is(custHeaderValue)); + assertThat(response.headers().get(SERVER_HEADER_FROM_PARAM_NAME).value(), + is("test-post")); + } + } + + @ParameterizedTest(name = "{0}") + @MethodSource("clientTypes") + void multiplexParallelStreamsGet(String clientType, LazyValue client) + throws ExecutionException, InterruptedException, TimeoutException { + Consumer callable = id -> { + try (Http2ClientResponse response = client.get() + .get("/h2streaming") + .queryParam("execId", id.toString()) + .request() + ) { + + InputStream is = response.inputStream(); + for (int i = 0; ; i++) { + byte[] bytes = is.readNBytes("0BAF000".getBytes().length); + if (bytes.length == 0) break; + String message = new String(bytes); + assertThat(message, is(String.format(id + "BAF%03d", i))); + } + + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + + CompletableFuture.allOf( + CompletableFuture.runAsync(() -> callable.accept(1), executorService) + , CompletableFuture.runAsync(() -> callable.accept(2), executorService) + , CompletableFuture.runAsync(() -> callable.accept(3), executorService) + , CompletableFuture.runAsync(() -> callable.accept(4), executorService) + ).get(5, TimeUnit.MINUTES); + } + + @AfterAll + static void afterAll() throws InterruptedException { + executorService.shutdown(); + if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + } + } +} diff --git a/nima/http2/webclient/src/test/resources/certificate.p12 b/nima/http2/webclient/src/test/resources/certificate.p12 new file mode 100644 index 0000000000000000000000000000000000000000..b2cb83427d181db6657cca1e2a70fa49bdb40724 GIT binary patch literal 2557 zcmY+^XEYm(8V7JmlMo{{TD7XS(rOdCqP1$YQbmkLgBGnF^QB0TP=wG}txZtlidCDw z)Sgw;=+!}s+9TAauJ@dK?|biu=RD{9|Ic|o{@^6&J`ivQPJ*^Tpt2DL5xX1!W8EiKoCLD{yMpA9K*pyL3J732UAlia0Jso@_1_=Z0T4J0#L{lBIEeZUWB`FI zOrazYpZuRyYsv6BeHC3~j`v3#2BuL54sF9g33lb7P}vO z^L@D&e_lq+R~&%SWhKN}ToPOtb0wP*>tMq!MR%LPxC|K2vexVZw7r)tDy)%eaTb(e zymOPI@|`_zsjBfH>KrDoY|{2>*SPVb9)3${`ioIt&lD^n1>aYJX@9jkH^NI+awU6I z5}VJ{XIl=ZcR5fqD1=dIRnSa2hXA4mPHb`**VNljMuahqhyWgC(dV1W_O{Xesk6?G|Hm} z%Ymzi5}iSx=2=rL!=M>3gKhYPO`Z(?mHSQ4hN;QSTNpTQ@_TZ?aEG2+(ZXTfydo-W6D);8#bFnW=!`VB|wJ~Lq;7IJt7k3!}g=a*lC4oPuToq z&>)ev0`D1~><0p6rOuR)8)meX01i{$W%pZFO!9`EsMYX?*za9eE-fy+0R?mEbZ?^V zzFc+fAgPl9G2L#jNiKc|5(PfYLm!q8+{>?(!{wqB78A|~5{jft$eF3V$U}(5yXB*` z<%0Ke9m{xQZUtG6@h7lib5ZWupm{+>{kp+Nqc$=+`C-ga3sm7%RHDVv=7|t;mF_7cPQllGD~yp+gV@JWi%@eQ^%O^yJEK0h!-ub zGR|wLJT*gDg2F}~2LIYcd`2&YyZ`VNZd$(dWaKE!5!GN?0{?THI@7p|Bci`PWPdhQ zJW--7*qsX}fwlezk{l9P9z+6PJB?*eM+(CBA68faz*B$LPT^VlKW)zZrHx7dM`xnW zz39KR0g*sGsZ9x^4W$E^JyaaJHlxD|v*I(~%85iIo_qP~!SHk8RNFkAUW<41n}+0r z#G78z*XPpj9C=;rI>Jg@J^Ojv#XJk}`0<_l6|}towM8=4Da!sjMYma}Q%f@;W=XKNb}azh zM@r2j*j32(O*x3HOxA=&L zYqyUN0s=%oMLLL=24t=}e@ypZ9?=*>sW<~`b4gMs$_mZU1BI?mZ;j$M!X5dGc$G&_ z%TlAZAq3I+6)Tx&m_mC`6b>b^*b%I|z5nK#!x z4=+8DC9#xuoyD7=UfF_f2dSUY69uw{LG3@ojC#@hRpxBNYLm2j=%5+-GB>9DT5-mo zlGWbm?0JQ#H|clJi^+Vk(uco~=mDF>G7TyGa@Q zwbh%8M?s6PG?>J{ot?-rkbBPMa9aW1@caj=?))-nN2G42~VPe?;x^u`J>Z__NtPR;PKNmTxJRZsRjHmS_HQgytm6aEgg`HWhYDmA#*m6f-}X9eY7|M|2=H|)_IgcA0W-c$R{g^9VCk`@mH{y$X547S z!rUXh(4yZNfl5o1iCga@P#q`H-ymx~c19ICpiQ>;g%+##uN9o0i|B&NxZTSg@o#1J z%F=B#*OwHWkJs;-UTYZl6(ouzu5m@wHIAB7{}qHrh#dh@-oPiJ!{8N_1W OY0p~c!p8BJlK%o#WVn_9 literal 0 HcmV?d00001 diff --git a/nima/http2/webclient/src/test/resources/logging-test.properties b/nima/http2/webclient/src/test/resources/logging-test.properties new file mode 100644 index 00000000000..253bb2c1bc2 --- /dev/null +++ b/nima/http2/webclient/src/test/resources/logging-test.properties @@ -0,0 +1,28 @@ +# +# Copyright (c) 2022, 2023 Oracle and/or its affiliates. +# +# Licensed 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. +# + +# Send messages to the console +handlers=io.helidon.logging.jul.HelidonConsoleHandler + +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +# java.util.logging.SimpleFormatter.format = [%1$tc] %5$s %6$s%n +#java.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS %5$s%6$s%n +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n + +io.helidon.nima.level=INFO +# Global logging level. Can be overridden by specific loggers +.level=INFO diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java index 6f89f7db724..14830c94fad 100755 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java @@ -268,27 +268,6 @@ private void doHandle() throws InterruptedException { } dispatchHandler(); - // Flow-control: frame processing is done, free space reserved for frame in window - int length = frameHeader.length(); - if (length > 0) { - int streamId = frameHeader.streamId(); - if (streamId > 0 && frameHeader.type() != Http2FrameType.HEADERS) { - // Stream ID > 0: update conenction and stream - FlowControl.Inbound inboundFlowControl = stream(streamId) - .stream() - .inboundFlowControl(); - inboundFlowControl.incrementWindowSize(length); - LOGGER.log(System.Logger.Level.INFO, () -> String.format( - "SRV IFC: Full increment %d-> %d", length, - inboundFlowControl.getRemainingWindowSize())); - } else { - // Stream ID == 0: update connection only - inboundWindowSize.incrementWindowSize(length); - LOGGER.log(System.Logger.Level.INFO, () -> String.format( - "SRV IFC: Conn increment %d -> %d", - length, inboundWindowSize.getRemainingWindowSize())); - } - } } else { dispatchHandler(); } @@ -346,28 +325,6 @@ private void readFrame() { if (frameHeader.length() == 0) { frameInProgress = BufferData.empty(); } else { - // Flow-control: reading frameHeader.length() bytes from HTTP2 socket for known stream ID. - int length = frameHeader.length(); - if (length > 0) { - int streamId = frameHeader.streamId(); - if (streamId > 0 && frameHeader.type() != Http2FrameType.HEADERS) { - // Stream ID > 0: update conenction and stream - FlowControl.Inbound inboundFlowControl = stream(streamId) - .stream() - .inboundFlowControl(); - inboundFlowControl.decrementWindowSize(length); - LOGGER.log(System.Logger.Level.INFO, () -> String.format( - "SRV IFC: Full decrement %d-> %d", length, - inboundFlowControl.getRemainingWindowSize())); - } else { - // Stream ID == 0: update connection only - inboundWindowSize.decrementWindowSize(length); - LOGGER.log(System.Logger.Level.INFO, () -> String.format( - "SRV IFC: Conn decrement %d -> %d", - length, inboundWindowSize.getRemainingWindowSize())); - } - } - frameInProgress = reader.readBuffer(frameHeader.length()); } @@ -457,9 +414,9 @@ private void readWindowUpdateFrame() { // Used in inbound flow control instance to write WINDOW_UPDATE frame. private void writeWindowUpdateFrame(Http2WindowUpdate windowUpdateFrame) { + LOGGER.log(DEBUG, () -> String.format("SRV IFC: Sending WINDOW_UPDATE %s", windowUpdateFrame)); connectionWriter.write(windowUpdateFrame .toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); - LOGGER.log(System.Logger.Level.INFO, () -> String.format("SRV IFC: Connection WINDOW_UPDATE %s", windowUpdateFrame)); } private void doSettings() { @@ -570,6 +527,20 @@ private void dataFrame() { // todo we need to have some information about how much data is buffered for a stream // to prevent OOM (use flow control!) + // Flow-control: reading frameHeader.length() bytes from HTTP2 socket for known stream ID. + int length = frameHeader.length(); + if (length > 0) { + int streamId = frameHeader.streamId(); + if (streamId > 0 && frameHeader.type() != Http2FrameType.HEADERS) { + // Stream ID > 0: update conenction and stream + FlowControl.Inbound inboundFlowControl = stream(streamId) + .stream() + .inboundFlowControl(); + inboundFlowControl.decrementWindowSize(length); + } + } + + if (frameHeader.flags(Http2FrameTypes.DATA).padded()) { BufferData frameData = inProgressFrame(); int padLength = frameData.read(); @@ -777,12 +748,12 @@ private StreamContext stream(int streamId) { FlowControl.Inbound.Builder inboundFlowControlBuilder = http2Config.flowControlEnabled() ? FlowControl.builderInbound() .streamId(streamId) - .connectionWindowsize(inboundWindowSize) - .streamWindowsize(inboundInitialWindowSize) + .connectionWindowSize(inboundWindowSize) + .streamWindowSize(inboundInitialWindowSize) .streamMaxFrameSize(http2Config.maxFrameSize()) // Pass NOOP when flow control is turned off (but we still have to send WINDOW_UPDATE frames) : FlowControl.builderInbound() - .connectionWindowsize(inboundWindowSize) + .connectionWindowSize(inboundWindowSize) .noop(); streamContext = new StreamContext(streamId, new Http2Stream(ctx, diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java index 599d7875b0a..9e471d0803e 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java @@ -202,8 +202,8 @@ public void windowUpdate(Http2WindowUpdate windowUpdate) { // Used in inbound flow control instance to write WINDOW_UPDATE frame. void writeWindowUpdate(Http2WindowUpdate windowUpdate) { + LOGGER.log(System.Logger.Level.DEBUG, () -> String.format("SRV IFC: Sending stream WINDOW_UPDATE %s", windowUpdate)); writer.write(windowUpdate.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); - LOGGER.log(System.Logger.Level.INFO, () -> String.format("SRV IFC: Stream WINDOW_UPDATE %s", windowUpdate)); } @@ -332,6 +332,7 @@ private BufferData readEntityFromPipeline() { DataFrame frame; try { frame = inboundData.take(); + inboundFlowControl().incrementWindowSize(frame.header().length()); } catch (InterruptedException e) { // this stream was interrupted, does not make sense to do anything else return BufferData.empty(); diff --git a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/Http2ClientTest.java b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/Http2ClientTest.java index 3601188558a..f748d81d55a 100644 --- a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/Http2ClientTest.java +++ b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/Http2ClientTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,6 @@ import io.helidon.nima.webserver.WebServer; import io.helidon.nima.webserver.http.HttpRouting; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.is; @@ -66,7 +65,7 @@ class Http2ClientTest { .tls(insecureTls) .build(); this.plainClient = WebClient.builder(Http2.PROTOCOL) - .baseUri("https://localhost:" + plainPort + "/") + .baseUri("http://localhost:" + plainPort + "/") .build(); } @@ -104,7 +103,6 @@ void testHttp1() { } @Test - @Disabled("HTTP/2 Client not yet implemented") void testUpgrade() { Http2ClientResponse response = plainClient.get("/") .request(); @@ -116,7 +114,6 @@ void testUpgrade() { } @Test - @Disabled("HTTP/2 Client not yet implemented") void testAppProtocol() { Http2ClientResponse response = tlsClient.get("/") .request(); @@ -128,7 +125,6 @@ void testAppProtocol() { } @Test - @Disabled("HTTP/2 Client not yet implemented") void testPriorKnowledge() { Http2ClientResponse response = tlsClient.get("/") .priorKnowledge(true) diff --git a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/PostTest.java b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/PostTest.java index 36e91c1f2ff..d8b03551d37 100644 --- a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/PostTest.java +++ b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/PostTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,9 +28,8 @@ import io.helidon.common.http.Http.Header; import io.helidon.common.http.Http.HeaderName; import io.helidon.common.http.Http.HeaderValue; -import io.helidon.common.http.Http.HeaderValues; import io.helidon.nima.http2.webclient.Http2; -import io.helidon.nima.http2.webclient.Http2ClientRequest; +import io.helidon.nima.http2.webclient.Http2Client; import io.helidon.nima.http2.webclient.Http2ClientResponse; import io.helidon.nima.webclient.WebClient; import io.helidon.nima.webserver.WebServer; @@ -41,15 +40,12 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static io.helidon.common.testing.http.junit5.HttpHeaderMatcher.hasHeader; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -// todo implement http/2 client -@Disabled class PostTest { private static final byte[] BYTES = new byte[256]; private static final HeaderName REQUEST_HEADER_NAME = Header.create("X-REquEst-HEADeR"); @@ -61,7 +57,7 @@ class PostTest { RESPONSE_HEADER_VALUE_STRING); private static WebServer server; - private static Http2ClientRequest request; + private static Http2Client client; static { Random random = new Random(); @@ -72,6 +68,7 @@ class PostTest { static void startServer() { server = WebServer.builder() .host("localhost") + .port(8080) .addRouting(HttpRouting.builder() .route(Http.Method.POST, "/string", Handler.create(String.class, Routes::string)) .route(Http.Method.POST, "/bytes", Handler.create(byte[].class, Routes::bytes)) @@ -82,11 +79,10 @@ static void startServer() { .build() .start(); - request = WebClient.builder(Http2.PROTOCOL) + client = WebClient.builder(Http2.PROTOCOL) .baseUri("http://localhost:" + server.port()) .priorKnowledge(true) - .build() - .method(Http.Method.POST); + .build(); } @AfterAll @@ -98,7 +94,9 @@ static void stopServer() { @Test void testStringRoute() { - Http2ClientResponse response = request.uri("/string") + Http2ClientResponse response = client + .method(Http.Method.POST) + .uri("/string") .submit("Hello"); assertThat(response.status(), is(Http.Status.OK_200)); @@ -106,12 +104,13 @@ void testStringRoute() { assertThat(entity, is("Hello")); Headers headers = response.headers(); assertThat("Should have correct length", headers.contentLength(), is(OptionalLong.of(5))); - assertThat(headers, hasHeader(HeaderValues.CONNECTION_KEEP_ALIVE)); } @Test void testByteRoute() { - Http2ClientResponse response = request.uri("/bytes") + Http2ClientResponse response = client + .method(Http.Method.POST) + .uri("/bytes") .submit(BYTES); assertThat(response.status(), is(Http.Status.OK_200)); @@ -119,12 +118,13 @@ void testByteRoute() { assertThat(entity, is(BYTES)); Headers headers = response.headers(); assertThat(headers.contentLength(), is(OptionalLong.of(BYTES.length))); - assertThat(headers, hasHeader(HeaderValues.CONNECTION_KEEP_ALIVE)); } @Test void testChunkedRoute() { - Http2ClientResponse response = request.uri("/chunked") + Http2ClientResponse response = client + .method(Http.Method.POST) + .uri("/chunked") .outputStream(outputStream -> { outputStream.write(BYTES); outputStream.close(); @@ -133,14 +133,13 @@ void testChunkedRoute() { assertThat(response.status(), is(Http.Status.OK_200)); byte[] entity = response.entity().as(byte[].class); assertThat(entity, is(BYTES)); - Headers headers = response.headers(); - assertThat(headers, hasHeader(HeaderValues.TRANSFER_ENCODING_CHUNKED)); - assertThat(headers, hasHeader(HeaderValues.CONNECTION_KEEP_ALIVE)); } @Test void testHeadersRoute() { - Http2ClientResponse response = request.uri("/headers") + Http2ClientResponse response = client + .method(Http.Method.POST) + .uri("/headers") .header(REQUEST_HEADER_VALUE) .submit("Hello"); @@ -149,26 +148,24 @@ void testHeadersRoute() { assertThat(entity, is("Hello")); Headers headers = response.headers(); assertThat(headers.contentLength(), is(OptionalLong.of(5))); - assertThat(headers, hasHeader(HeaderValues.CONNECTION_KEEP_ALIVE)); assertThat(headers, hasHeader(REQUEST_HEADER_VALUE)); assertThat(headers, hasHeader(RESPONSE_HEADER_VALUE)); } @Test void testCloseRoute() { - Http2ClientResponse response = request.uri("/close") + Http2ClientResponse response = client + .method(Http.Method.POST) + .uri("/close") .submit("Hello"); assertThat(response.status(), is(Http.Status.NO_CONTENT_204)); String entity = response.entity().as(String.class); assertThat(entity, is("")); - Headers headers = response.headers(); - assertThat(headers, hasHeader(HeaderValues.CONNECTION_CLOSE)); } private static class Routes { public static void close(ServerRequest req, ServerResponse res) { - res.header(HeaderValues.CONNECTION_CLOSE); res.status(Http.Status.NO_CONTENT_204); res.send(); } diff --git a/nima/tests/integration/http2/client/src/test/resources/logging-test.properties b/nima/tests/integration/http2/client/src/test/resources/logging-test.properties new file mode 100644 index 00000000000..eba7d6f4be5 --- /dev/null +++ b/nima/tests/integration/http2/client/src/test/resources/logging-test.properties @@ -0,0 +1,21 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed 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. +# +handlers=java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level=FINEST +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.nima.level=INFO diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/UriHelper.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/UriHelper.java index 9c5e2910294..cc19bf45ad8 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/UriHelper.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/UriHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package io.helidon.nima.webclient; import java.net.URI; +import java.util.Map; import io.helidon.common.uri.UriEncoding; import io.helidon.common.uri.UriQuery; @@ -26,6 +27,11 @@ * Helper for client URI handling. */ public class UriHelper { + + private static final Map DEFAULT_PORTS = Map.of( + "http", 80, + "https", 443 + ); private final String baseScheme; private final String baseAuthority; private final String basePath; @@ -162,6 +168,9 @@ public String host() { * @return port */ public int port() { + if (this.port == -1) { + return DEFAULT_PORTS.getOrDefault(this.scheme, -1); + } return port; } diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java index a0a8cd0e408..ae3e6058386 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ClientRequestImpl.java @@ -50,7 +50,6 @@ import io.helidon.nima.http.media.EntityWriter; import io.helidon.nima.http.media.MediaContext; import io.helidon.nima.webclient.ClientConnection; -import io.helidon.nima.webclient.ConnectionKey; import io.helidon.nima.webclient.UriHelper; import static java.lang.System.Logger.Level.DEBUG; diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ConnectionKey.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ConnectionKey.java similarity index 86% rename from nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ConnectionKey.java rename to nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ConnectionKey.java index 1c26859a261..dff31fb59d3 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/ConnectionKey.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/ConnectionKey.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,10 @@ * limitations under the License. */ -package io.helidon.nima.webclient; +package io.helidon.nima.webclient.http1; import io.helidon.nima.common.tls.Tls; +import io.helidon.nima.webclient.DnsAddressLookup; import io.helidon.nima.webclient.spi.DnsResolver; /** @@ -29,7 +30,7 @@ * @param dnsResolver DNS resolver to be used * @param dnsAddressLookup DNS address lookup strategy */ -public record ConnectionKey(String scheme, +record ConnectionKey(String scheme, String host, int port, Tls tls, diff --git a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java index 7c41ece8541..f1d72ddb2bf 100644 --- a/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java +++ b/nima/webclient/webclient/src/main/java/io/helidon/nima/webclient/http1/Http1ClientConnection.java @@ -39,7 +39,6 @@ import io.helidon.common.socket.SocketOptions; import io.helidon.common.socket.TlsSocket; import io.helidon.nima.webclient.ClientConnection; -import io.helidon.nima.webclient.ConnectionKey; import io.helidon.nima.webclient.spi.DnsResolver; import static java.lang.System.Logger.Level.DEBUG; diff --git a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/UriHelperTest.java b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/UriHelperTest.java index b16295ed985..8d91470d5d3 100644 --- a/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/UriHelperTest.java +++ b/nima/webclient/webclient/src/test/java/io/helidon/nima/webclient/UriHelperTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,10 +34,22 @@ void testDefaults() { assertThat(helper.authority(), is("localhost")); assertThat(helper.host(), is("localhost")); assertThat(helper.path(), is("")); - assertThat(helper.port(), is(-1)); + assertThat(helper.port(), is(80)); assertThat(helper.scheme(), is("http")); } + @Test + void testDefaultsHttps() { + UriQueryWriteable query = UriQueryWriteable.create(); + UriHelper helper = UriHelper.create(URI.create("https://localhost"), query); + + assertThat(helper.authority(), is("localhost")); + assertThat(helper.host(), is("localhost")); + assertThat(helper.path(), is("")); + assertThat(helper.port(), is(443)); + assertThat(helper.scheme(), is("https")); + } + @Test void testNonDefaults() { UriQueryWriteable query = UriQueryWriteable.create(); From dff8d84fc087ab62fc513e3e284d4c0465f8a223 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Kraus?= Date: Fri, 17 Mar 2023 13:37:54 +0100 Subject: [PATCH 03/17] HTTP/2 Server Flow-control - window update strategies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Kraus --- .../io/helidon/nima/http2/FlowControl.java | 4 +- .../helidon/nima/http2/FlowControlImpl.java | 4 +- .../helidon/nima/http2/FlowControlNoop.java | 2 +- .../io/helidon/nima/http2/Http2Settings.java | 6 ++- .../io/helidon/nima/http2/WindowSize.java | 4 +- .../io/helidon/nima/http2/WindowSizeImpl.java | 40 +++++++++++++++---- .../webclient/Http2ClientConnection.java | 11 ++++- .../nima/http2/webclient/FlowControlTest.java | 9 +++-- .../nima/http2/webserver/Http2Connection.java | 2 +- 9 files changed, 59 insertions(+), 23 deletions(-) diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java index c3619f88cea..0b356523f16 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java @@ -32,9 +32,9 @@ public interface FlowControl { /** * Reset stream window size. * - * @param increment increment + * @param size new window size */ - void resetStreamWindowSize(long increment); + void resetStreamWindowSize(int size); /** * Remaining window size in bytes. diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java index ca3d87edc79..29990eeee9f 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java @@ -35,8 +35,8 @@ public void decrementWindowSize(int decrement) { } @Override - public void resetStreamWindowSize(long increment) { - streamWindowSize().resetWindowSize(increment); + public void resetStreamWindowSize(int size) { + streamWindowSize().resetWindowSize(size); } @Override diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java index 6d3d74802a7..f6625e3e175 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java @@ -24,7 +24,7 @@ public void decrementWindowSize(int decrement) { } @Override - public void resetStreamWindowSize(long increment) { + public void resetStreamWindowSize(int size) { } @Override diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Settings.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Settings.java index 0739c7aa5f5..fc6cdcbee1d 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Settings.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Settings.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ * HTTP settings frame. */ public final class Http2Settings implements Http2Frame { + private static final System.Logger LOGGER = System.getLogger(Http2Settings.class.getName()); private final Map values; Http2Settings(Map values) { @@ -89,9 +90,10 @@ public Http2FrameData toFrameData(Http2Settings settings, int streamId, Http2Fla values.values().forEach(it -> { Object value = it.value(); Http2Setting setting = (Http2Setting) it.setting(); + LOGGER.log(System.Logger.Level.DEBUG, + () -> String.format(" - Http2Settings %s: %s", it.setting().toString(), it.value().toString())); setting.write(data, value); }); - Http2FrameHeader header = Http2FrameHeader.create(data.available(), frameTypes(), flags, diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java index f397422648e..40f60de679f 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java @@ -78,9 +78,9 @@ static WindowSize.Inbound createInboundNoop(Consumer windowUp /** * Reset window size. * - * @param size window size + * @param size new window size */ - void resetWindowSize(long size); + void resetWindowSize(int size); /** * Increment window size. diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java index 5b4fd2f9800..ee7750bc26f 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java @@ -15,6 +15,7 @@ */ package io.helidon.nima.http2; +import java.lang.System.Logger.Level; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -28,18 +29,25 @@ */ abstract class WindowSizeImpl implements WindowSize { + private static final System.Logger LOGGER = System.getLogger(WindowSizeImpl.class.getName()); + + private int windowSize; private final AtomicInteger remainingWindowSize; private WindowSizeImpl(int initialWindowSize) { - remainingWindowSize = new AtomicInteger(initialWindowSize); + this.windowSize = initialWindowSize; + this.remainingWindowSize = new AtomicInteger(initialWindowSize); } @Override - public void resetWindowSize(long size) { + public void resetWindowSize(int size) { // When the value of SETTINGS_INITIAL_WINDOW_SIZE changes, // a receiver MUST adjust the size of all stream flow-control windows that // it maintains by the difference between the new value and the old value - remainingWindowSize.updateAndGet(o -> (int) size - o); + remainingWindowSize.updateAndGet(o -> o + size - windowSize); + windowSize = size; + LOGGER.log(Level.DEBUG, + () -> String.format("Reset window size %d, remaining %d", windowSize, remainingWindowSize.get())); } @Override @@ -48,12 +56,16 @@ public boolean incrementWindowSize(int increment) { .getAndUpdate(r -> r < 0 || MAX_WIN_SIZE - r > increment ? increment + r : MAX_WIN_SIZE); + LOGGER.log(Level.DEBUG, + () -> String.format("Increment window size %d, remaining %d", increment, remainingWindowSize.get())); return MAX_WIN_SIZE - remaining <= increment; } @Override public void decrementWindowSize(int decrement) { remainingWindowSize.updateAndGet(operand -> operand - decrement); + LOGGER.log(Level.DEBUG, + () -> String.format("Decrement window size %d, remaining %d", decrement, remainingWindowSize.get())); } @Override @@ -124,6 +136,10 @@ public void blockTillUpdate() { //TODO configurable timeout updated.get().get(100, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { + LOGGER.log(Level.WARNING, + () -> String.format("Exception %s caught while waiting for window update: %s", + e.getClass().getName(), + e.getMessage())); } } } @@ -156,7 +172,7 @@ public boolean incrementWindowSize(int increment) { } @Override - public void resetWindowSize(long size) { + public void resetWindowSize(int size) { } @Override @@ -226,9 +242,7 @@ private enum Type { private static Type select(Context context) { // Bisection strategy requires at least 4 frames to be placed inside window - //FIXME: Find out why bisection strategy gets deadlocked -// return context.maxFrameSize * 4 < context.maxWindowsize ? BISECTION : SIMPLE; - return SIMPLE; + return context.maxFrameSize * 4 <= context.maxWindowsize ? BISECTION : SIMPLE; } } @@ -251,8 +265,9 @@ private Simple(Context context, Consumer windowUpdateWriter) @Override void windowUpdate(int increment) { windowUpdateWriter().accept(new Http2WindowUpdate(increment)); + LOGGER.log(Level.DEBUG, + () -> String.format("Window update increment %d", increment)); } - } /** @@ -274,8 +289,17 @@ private Bisection(Context context, Consumer windowUpdateWrite @Override void windowUpdate(int increment) { delayedIncrement += increment; + LOGGER.log(Level.DEBUG, + () -> String.format("Window update hidden increment %d, total %d, watermark %d", + increment, + delayedIncrement, + watermark)); if (delayedIncrement > watermark) { windowUpdateWriter().accept(new Http2WindowUpdate(delayedIncrement)); + LOGGER.log(Level.DEBUG, + () -> String.format("Window update real increment %d, watermark %d", + delayedIncrement, + watermark)); delayedIncrement = 0; } } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java index b7a9a07dc43..027f8c4a7fa 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java @@ -188,15 +188,22 @@ private void handle() { if (http2Settings.hasValue(Http2Setting.MAX_FRAME_SIZE)) { maxFrameSize = Math.toIntExact(http2Settings.value(Http2Setting.MAX_FRAME_SIZE)); } - // §6.5.2 Update initial window size for new streams + // §6.5.2 Update initial window size for new streams and window sizes of all already existing streams if (http2Settings.hasValue(Http2Setting.INITIAL_WINDOW_SIZE)) { - Long initWinSize = http2Settings.value(Http2Setting.INITIAL_WINDOW_SIZE); + long initWinSize = http2Settings.value(Http2Setting.INITIAL_WINDOW_SIZE); if (initWinSize > WindowSize.MAX_WIN_SIZE) { goAway(frameHeader.streamId(), Http2ErrorCode.FLOW_CONTROL, "Window size too big. Max: "); //FIXME: close connection? return; } + // Update streams window size + streams.values().forEach(stream -> stream.outboundFlowControl().resetStreamWindowSize((int) initWinSize)); streamInitialWindowSize = Math.toIntExact(initWinSize); + // Update connection window size + outboundConnectionWindowSize.resetWindowSize((int) initWinSize); + LOGGER.log(DEBUG, + () -> String.format("Http2Settings window size increment on client: %d", + (initWinSize - WindowSize.DEFAULT_WIN_SIZE))); } // §6.5.3 Settings Synchronization ackSettings(); diff --git a/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/FlowControlTest.java b/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/FlowControlTest.java index b57ace433d1..1b027093604 100644 --- a/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/FlowControlTest.java +++ b/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/FlowControlTest.java @@ -24,14 +24,14 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + import io.helidon.nima.http2.webserver.Http2Route; import io.helidon.nima.testing.junit5.webserver.ServerTest; import io.helidon.nima.testing.junit5.webserver.SetUpServer; import io.helidon.nima.webserver.WebServer; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Test; - import static io.helidon.common.http.Http.Method.PUT; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -39,6 +39,7 @@ @ServerTest public class FlowControlTest { + private static final System.Logger LOGGER = System.getLogger(FlowControlTest.class.getName()); private static volatile CompletableFuture flowControlServerLatch = new CompletableFuture<>(); private static volatile CompletableFuture flowControlClientLatch = new CompletableFuture<>(); private static final ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor(); @@ -103,11 +104,13 @@ void flowControl() throws ExecutionException, InterruptedException, TimeoutExcep out -> { for (int i = 0; i < 5; i++) { byte[] bytes = data10k.getBytes(); + LOGGER.log(System.Logger.Level.INFO, () -> String.format("CL IF: Sending %d bytes", bytes.length)); out.write(bytes); sentData.updateAndGet(o -> o + bytes.length); } for (int i = 0; i < 5; i++) { byte[] bytes = data10k.toUpperCase().getBytes(); + LOGGER.log(System.Logger.Level.INFO, () -> String.format("CL IF: Sending %d bytes", bytes.length)); out.write(bytes); sentData.updateAndGet(o -> o + bytes.length); } diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java index 14830c94fad..38e8ca558b1 100755 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java @@ -457,7 +457,7 @@ private void doSettings() { for (StreamContext sctx : streams.values()) { Http2StreamState streamState = sctx.stream.streamState(); if (streamState == Http2StreamState.OPEN || streamState == Http2StreamState.HALF_CLOSED_REMOTE) { - sctx.stream.outboundFlowControl().resetStreamWindowSize(it); + sctx.stream.outboundFlowControl().resetStreamWindowSize((int) it); } } From 78d3f7b6bde8c6e698966cfab6eceb7600652eb2 Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Fri, 24 Mar 2023 19:01:36 +0100 Subject: [PATCH 04/17] HTTP/2 Client Flow-control - inbound/outbound --- .../grpc/webserver/GrpcProtocolHandler.java | 3 +- .../nima/http2/ConnectionFlowControl.java | 153 +++++++++++ .../io/helidon/nima/http2/FlowControl.java | 136 +-------- .../helidon/nima/http2/FlowControlImpl.java | 81 ++++-- .../helidon/nima/http2/FlowControlNoop.java | 16 +- .../nima/http2/Http2ConnectionWriter.java | 13 +- .../io/helidon/nima/http2/Http2Setting.java | 4 +- .../io/helidon/nima/http2/Http2Stream.java | 9 +- .../helidon/nima/http2/Http2StreamWriter.java | 11 +- .../helidon/nima/http2/StreamFlowControl.java | 75 +++++ .../io/helidon/nima/http2/WindowSize.java | 52 ++-- .../io/helidon/nima/http2/WindowSizeImpl.java | 156 ++++++----- .../helidon/nima/http2/Http2HeadersTest.java | 21 +- .../webclient/Http2ClientConnection.java | 171 +++++++----- .../http2/webclient/Http2ClientStream.java | 113 +++++--- .../nima/http2/webclient/FlowControlTest.java | 142 ---------- .../test/resources/logging-test.properties | 9 +- .../nima/http2/webserver/Http2Config.java | 8 - .../nima/http2/webserver/Http2Connection.java | 240 ++++++++-------- .../http2/webserver/Http2ServerResponse.java | 4 +- .../nima/http2/webserver/Http2Stream.java | 53 ++-- nima/tests/integration/http2/server/pom.xml | 10 + .../server/src/main/java/module-info.java | 17 ++ .../http2/webserver/FlowControlTest.java | 259 ++++++++++++++++++ .../server/src/test/java/module-info.java | 25 ++ .../test/resources/logging-test.properties | 8 +- 26 files changed, 1077 insertions(+), 712 deletions(-) create mode 100644 nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java create mode 100644 nima/http2/http2/src/main/java/io/helidon/nima/http2/StreamFlowControl.java delete mode 100644 nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/FlowControlTest.java create mode 100644 nima/tests/integration/http2/server/src/main/java/module-info.java create mode 100644 nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java create mode 100644 nima/tests/integration/http2/server/src/test/java/module-info.java diff --git a/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandler.java b/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandler.java index 8f90468427a..8d70c5ba092 100644 --- a/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandler.java +++ b/nima/grpc/webserver/src/main/java/io/helidon/nima/grpc/webserver/GrpcProtocolHandler.java @@ -175,7 +175,8 @@ public void sendMessage(RES message) { Http2Flag.DataFlags.create(0), streamId); - streamWriter.write(new Http2FrameData(header, bufferData), FlowControl.Outbound.NOOP); + //FIXME: FC and MAX_FRAME_SIZE + streamWriter.writeData(new Http2FrameData(header, bufferData), FlowControl.Outbound.NOOP); } @Override diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java new file mode 100644 index 00000000000..9f578b4c8db --- /dev/null +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.http2; + +import java.util.function.BiConsumer; + +import static java.lang.System.Logger.Level.INFO; + +/** + * HTTP/2 Flow control for connection. + */ +public class ConnectionFlowControl { + + private static final System.Logger LOGGER = System.getLogger(FlowControl.class.getName()); + + private final Type type; + private final BiConsumer windowUpdateWriter; + private final WindowSize.Inbound inboundConnectionWindowSize; + private final WindowSize.Outbound outboundConnectionWindowSize; + private int maxFrameSize = WindowSize.DEFAULT_MAX_FRAME_SIZE; + private int initialWindowSize = WindowSize.DEFAULT_WIN_SIZE; + + /** + * Create connection HTTP/2 flow-control for server side. + * + * @param windowUpdateWriter method called for sending WINDOW_UPDATE frames to the client. + * @return Connection HTTP/2 flow-control + */ + public static ConnectionFlowControl createServer(BiConsumer windowUpdateWriter){ + return new ConnectionFlowControl(Type.SERVER, windowUpdateWriter); + } + + /** + * Create connection HTTP/2 flow-control for client side. + * + * @param windowUpdateWriter method called for sending WINDOW_UPDATE frames to the server. + * @return Connection HTTP/2 flow-control + */ + public static ConnectionFlowControl createClient(BiConsumer windowUpdateWriter){ + return new ConnectionFlowControl(Type.CLIENT, windowUpdateWriter); + } + + private ConnectionFlowControl(Type type, BiConsumer windowUpdateWriter) { + this.type = type; + this.windowUpdateWriter = windowUpdateWriter; + //FIXME: configurable max frame size? + this.inboundConnectionWindowSize = + WindowSize.createInbound(type, + 0, + WindowSize.DEFAULT_WIN_SIZE, + WindowSize.DEFAULT_MAX_FRAME_SIZE, + windowUpdateWriter); + outboundConnectionWindowSize = + WindowSize.createOutbound(type, 0, this); + } + + /** + * Create stream specific inbound and outbound flow control. + * + * @param streamId stream id + * @return stream flow control + */ + public StreamFlowControl createStreamFlowControl(int streamId) { + return new StreamFlowControl(type, streamId, this, windowUpdateWriter); + } + + /** + * Increment outbound connection flow control window, called when WINDOW_UPDATE is received. + * + * @param increment number of bytes other side has requested on top of actual demand + * @return outbound window size after increment + */ + public long incrementOutboundConnectionWindowSize(int increment) { + return outboundConnectionWindowSize.incrementWindowSize(increment); + } + + /** + * Decrement inbound connection flow control window, called when DATA frame is received. + * @param decrement received DATA frame size in bytes + * @return inbound window size after decrement + */ + public long decrementInboundConnectionWindowSize(int decrement) { + return inboundConnectionWindowSize.decrementWindowSize(decrement); + } + + /** + * Reset MAX_FRAME_SIZE for all streams, existing and future ones. + * + * @param maxFrameSize to split data frames according to when larger + */ + public void resetMaxFrameSize(int maxFrameSize) { + this.maxFrameSize = maxFrameSize; + } + + /** + * Reset an initial window size value for outbound flow control windows of a new streams. + * Don't forget to call stream.flowControl().outbound().resetStreamWindowSize(...) for each stream + * to align window size of existing streams. + * + * @param initialWindowSize INIT_WINDOW_SIZE received + */ + public void resetInitialWindowSize(int initialWindowSize) { + LOGGER.log(INFO, () -> String.format("%s OFC STR *: Recv INIT_WINDOW_SIZE %s", type, initialWindowSize)); + this.initialWindowSize = initialWindowSize; + } + + /** + * Connection outbound flow control window, + * decrements when DATA are sent and increments when WINDOW_UPDATE or INIT_WINDOW_SIZE is received. + * Blocks sending when window is depleted. + * + * @return connection outbound flow control window + */ + public WindowSize.Outbound outbound() { + return outboundConnectionWindowSize; + } + + /** + * Connection inbound window is always manipulated by respective stream flow control, + * therefore package private is enough. + * + * @return connection inbound flow control window + */ + WindowSize.Inbound inbound() { + return inboundConnectionWindowSize; + } + + int maxFrameSize() { + return maxFrameSize; + } + + int initialWindowSize() { + return initialWindowSize; + } + + enum Type { + SERVER, CLIENT; + } +} diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java index 0b356523f16..ceaa4d20192 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControl.java @@ -15,8 +15,6 @@ */ package io.helidon.nima.http2; -import java.util.function.Consumer; - /** * Flow control used by HTTP/2 for backpressure. */ @@ -43,31 +41,6 @@ public interface FlowControl { */ int getRemainingWindowSize(); - /** - * Create inbound flow control builder for a stream. - * - * @return a new inbound flow control builder - */ - static FlowControl.Inbound.Builder builderInbound() { - return new FlowControl.Inbound.Builder(); - } - - /** - * Create outbound flow control for a stream. - * - * @param streamId stream id - * @param streamInitialWindowSize initial window size for stream - * @param connectionWindowSize connection window size - * @return a new flow control - */ - static FlowControl.Outbound createOutbound(int streamId, - int streamInitialWindowSize, - WindowSize.Outbound connectionWindowSize) { - return new FlowControlImpl.Outbound(streamId, - streamInitialWindowSize, - connectionWindowSize); - } - /** * Inbound flow control used by HTTP/2 for backpressure. */ @@ -79,108 +52,6 @@ interface Inbound extends FlowControl { * @param increment increment in bytes */ void incrementWindowSize(int increment); - - /** - * Inbound flow control builder. - */ - class Builder implements io.helidon.common.Builder { - - private int streamId; - private int streamWindowSize; - private int streamMaxFrameSize; - private WindowSize.Inbound connectionWindowSize; - private Consumer windowUpdateStreamWriter; - private boolean noop; - - private Builder() { - this.streamId = 0; - this.streamWindowSize = 0; - this.streamMaxFrameSize = 0; - this.connectionWindowSize = null; - this.windowUpdateStreamWriter = null; - this.noop = false; - } - - @Override - public FlowControl.Inbound build() { - return noop - ? new FlowControlNoop.Inbound(connectionWindowSize, - windowUpdateStreamWriter) - : new FlowControlImpl.Inbound(streamId, - streamWindowSize, - streamMaxFrameSize, - connectionWindowSize, - windowUpdateStreamWriter); - } - - /** - * Trigger build of NOOP flow control (flow control turned off). - * NOOP flow control will be returned regardless of other setting when this method is called. - * - * @return this builder - */ - public Builder noop() { - noop = true; - return this; - } - - /** - * Set HTTP/2 stream ID. - * - * @param streamId HTTP/2 stream ID - * @return this builder - */ - public Builder streamId(int streamId) { - this.streamId = streamId; - return this; - } - - /** - * Set HTTP/2 connection window size. - * - * @param windowSize HTTP/2 connection window size - * @return this builder - */ - public Builder connectionWindowSize(WindowSize.Inbound windowSize) { - this.connectionWindowSize = windowSize; - return this; - } - - /** - * Set HTTP/2 stream window size. - * - * @param windowSize HTTP/2 stream window size - * @return this builder - */ - public Builder streamWindowSize(int windowSize) { - this.streamWindowSize = windowSize; - return this; - } - - /** - * Set HTTP/2 stream window size. - * - * @param maxFrameSize HTTP/2 stream maximum frame size size - * @return this builder - */ - public Builder streamMaxFrameSize(int maxFrameSize) { - this.streamMaxFrameSize = maxFrameSize; - return this; - } - - /** - * Set writer method for current HTTP/2 stream WINDOW_UPDATE frame. - * - * @param windowUpdateWriter WINDOW_UPDATE frame writer for current HTTP/2 stream - * @return this builder - */ - public Builder windowUpdateStreamWriter(Consumer windowUpdateWriter) { - this.windowUpdateStreamWriter = windowUpdateWriter; - return this; - } - - } - } /** @@ -199,7 +70,7 @@ interface Outbound extends FlowControl { * @param increment increment in bytes * @return {@code true} if succeeded, {@code false} if timed out */ - boolean incrementStreamWindowSize(int increment); + long incrementStreamWindowSize(int increment); /** * Split frame into frames that can be sent. @@ -215,6 +86,11 @@ interface Outbound extends FlowControl { */ void blockTillUpdate(); + /** + * MAX_FRAME_SIZE setting last received from the other side or default. + * @return MAX_FRAME_SIZE + */ + int maxFrameSize(); } } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java index 29990eeee9f..db267de459f 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java @@ -16,10 +16,14 @@ package io.helidon.nima.http2; import java.util.Objects; -import java.util.function.Consumer; +import java.util.function.BiConsumer; + +import static java.lang.System.Logger.Level.DEBUG; abstract class FlowControlImpl implements FlowControl { + private static final System.Logger LOGGER = System.getLogger(FlowControl.class.getName()); + private final int streamId; FlowControlImpl(int streamId) { @@ -27,12 +31,8 @@ abstract class FlowControlImpl implements FlowControl { } abstract WindowSize connectionWindowSize(); - abstract WindowSize streamWindowSize(); - public void decrementWindowSize(int decrement) { - connectionWindowSize().decrementWindowSize(decrement); - streamWindowSize().decrementWindowSize(decrement); - } + abstract WindowSize streamWindowSize(); @Override public void resetStreamWindowSize(int size) { @@ -49,6 +49,10 @@ public int getRemainingWindowSize() { ); } + protected int streamId() { + return this.streamId; + } + @Override public String toString() { return "FlowControlImpl{" @@ -62,20 +66,25 @@ static class Inbound extends FlowControlImpl implements FlowControl.Inbound { private final WindowSize.Inbound connectionWindowSize; private final WindowSize.Inbound streamWindowSize; + private final ConnectionFlowControl.Type type; - Inbound(int streamId, + Inbound(ConnectionFlowControl.Type type, + int streamId, int streamInitialWindowSize, int streamMaxFrameSize, WindowSize.Inbound connectionWindowSize, - Consumer windowUpdateStreamWriter) { + BiConsumer windowUpdateStreamWriter) { super(streamId); + this.type = type; if (streamInitialWindowSize == 0) { throw new IllegalArgumentException("Window size in bytes for stream-level flow control was not set."); } Objects.requireNonNull(connectionWindowSize, "Window size in bytes for connection-level flow control was not set."); Objects.requireNonNull(windowUpdateStreamWriter, "Stream-level window update writer was not set."); this.connectionWindowSize = connectionWindowSize; - this.streamWindowSize = WindowSize.createInbound(streamInitialWindowSize, + this.streamWindowSize = WindowSize.createInbound(type, + streamId, + streamInitialWindowSize, streamMaxFrameSize, windowUpdateStreamWriter); } @@ -90,30 +99,42 @@ WindowSize streamWindowSize() { return streamWindowSize; } + @Override + public void decrementWindowSize(int decrement) { + long strRemaining = streamWindowSize().decrementWindowSize(decrement); + LOGGER.log(DEBUG, () -> String.format("%s IFC STR %d: -%d(%d)", type, streamId(), decrement, strRemaining)); + long connRemaining = connectionWindowSize().decrementWindowSize(decrement); + LOGGER.log(DEBUG, () -> String.format("%s IFC STR 0: -%d(%d)", type, decrement, connRemaining)); + } + @Override public void incrementWindowSize(int increment) { - streamWindowSize.incrementWindowSize(increment); - connectionWindowSize.incrementWindowSize(increment); + long strRemaining = streamWindowSize.incrementWindowSize(increment); + LOGGER.log(DEBUG, () -> String.format("%s IFC STR %d: +%d(%d)", type, streamId(), increment, strRemaining)); + long conRemaining = connectionWindowSize.incrementWindowSize(increment); + LOGGER.log(DEBUG, () -> String.format("%s IFC STR 0: +%d(%d)", type, increment, conRemaining)); } } static class Outbound extends FlowControlImpl implements FlowControl.Outbound { - private final WindowSize.Outbound connectionWindowSize; + private final ConnectionFlowControl.Type type; + private final ConnectionFlowControl connectionFlowControl; private final WindowSize.Outbound streamWindowSize; - Outbound(int streamId, - int streamInitialWindowSize, - WindowSize.Outbound connectionWindowSize) { + Outbound(ConnectionFlowControl.Type type, + int streamId, + ConnectionFlowControl connectionFlowControl) { super(streamId); - this.connectionWindowSize = connectionWindowSize; - this.streamWindowSize = WindowSize.createOutbound(streamInitialWindowSize); + this.type = type; + this.connectionFlowControl = connectionFlowControl; + this.streamWindowSize = WindowSize.createOutbound(type, streamId, connectionFlowControl); } @Override WindowSize connectionWindowSize() { - return connectionWindowSize; + return connectionFlowControl.outbound(); } @Override @@ -121,11 +142,20 @@ WindowSize streamWindowSize() { return streamWindowSize; } + public void decrementWindowSize(int decrement) { + long strRemaining = streamWindowSize().decrementWindowSize(decrement); + LOGGER.log(DEBUG, () -> String.format("%s OFC STR %d: -%d(%d)", type, streamId(), decrement, strRemaining)); + + long connRemaining = connectionWindowSize().decrementWindowSize(decrement); + LOGGER.log(DEBUG, () -> String.format("%s OFC STR 0: -%d(%d)", type, decrement, connRemaining)); + } + @Override - public boolean incrementStreamWindowSize(int increment) { - boolean result = streamWindowSize.incrementWindowSize(increment); - connectionWindowSize.triggerUpdate(); - return result; + public long incrementStreamWindowSize(int increment) { + long remaining = streamWindowSize.incrementWindowSize(increment); + LOGGER.log(DEBUG, () -> String.format("%s OFC STR %d: +%d(%d)", type, streamId(), increment, remaining)); + connectionFlowControl.outbound().triggerUpdate(); + return remaining; } @Override @@ -135,9 +165,14 @@ public Http2FrameData[] cut(Http2FrameData frame) { @Override public void blockTillUpdate() { - connectionWindowSize.blockTillUpdate(); + connectionFlowControl.outbound().blockTillUpdate(); streamWindowSize.blockTillUpdate(); } + + @Override + public int maxFrameSize() { + return connectionFlowControl.maxFrameSize(); + } } } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java index f6625e3e175..14296f8fc8c 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlNoop.java @@ -15,7 +15,7 @@ */ package io.helidon.nima.http2; -import java.util.function.Consumer; +import java.util.function.BiConsumer; class FlowControlNoop implements FlowControl { @@ -39,9 +39,11 @@ static class Inbound extends FlowControlNoop implements FlowControl.Inbound { private final WindowSize.Inbound connectionWindowSize; private final WindowSize.Inbound streamWindowSize; - Inbound(WindowSize.Inbound connectionWindowSize, Consumer windowUpdateStreamWriter) { + Inbound(int streamId, + WindowSize.Inbound connectionWindowSize, + BiConsumer windowUpdateStreamWriter) { this.connectionWindowSize = connectionWindowSize; - this.streamWindowSize = WindowSize.createInboundNoop(windowUpdateStreamWriter); + this.streamWindowSize = WindowSize.createInboundNoop(streamId, windowUpdateStreamWriter); } @Override @@ -55,8 +57,8 @@ public void incrementWindowSize(int increment) { static class Outbound extends FlowControlNoop implements FlowControl.Outbound { @Override - public boolean incrementStreamWindowSize(int increment) { - return false; + public long incrementStreamWindowSize(int increment) { + return WindowSize.MAX_WIN_SIZE; } @Override @@ -68,6 +70,10 @@ public Http2FrameData[] cut(Http2FrameData frame) { public void blockTillUpdate() { } + @Override + public int maxFrameSize() { + return WindowSize.DEFAULT_MAX_FRAME_SIZE; + } } } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java index 1156272b02e..998a3febee8 100755 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2ConnectionWriter.java @@ -57,11 +57,14 @@ public Http2ConnectionWriter(SocketContext ctx, DataWriter writer, List { int identifier(); /** - * Typed default value of this settiing. + * Typed default value of this setting. * * @return default value */ diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Stream.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Stream.java index 608f1a5f6c7..25ccf5df3dc 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Stream.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Stream.java @@ -79,13 +79,6 @@ public interface Http2Stream { * * @return flow control */ - FlowControl.Outbound outboundFlowControl(); - - /** - * Inbound flow control of this stream. - * - * @return flow control - */ - FlowControl.Inbound inboundFlowControl(); + StreamFlowControl flowControl(); } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamWriter.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamWriter.java index 4d17e210c3f..0ec72179b68 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamWriter.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamWriter.java @@ -24,9 +24,16 @@ public interface Http2StreamWriter { * Write a frame. * * @param frame frame to write - * @param flowControl flow control */ - void write(Http2FrameData frame, FlowControl.Outbound flowControl); + void write(Http2FrameData frame); + + /** + * Write a frame with flow control. + * + * @param frame + * @param flowControl + */ + void writeData(Http2FrameData frame, FlowControl.Outbound flowControl); /** * Write headers with no (or streaming) entity. diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/StreamFlowControl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/StreamFlowControl.java new file mode 100644 index 00000000000..0c9a93845a6 --- /dev/null +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/StreamFlowControl.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.http2; + +import java.util.function.BiConsumer; + +/** + * Stream specific HTTP/2 flow control. + *
+ * Manages: + *
    + *
  • Inbound - counts the received data and sends WINDOWS_UPDATE frames to the other side when needed.
  • + *
  • Outbound - counts the sent data, monitors requested data by received WINDOWS_UPDATE frames + * and blocks the sending until enough data is requested.
  • + *
+ * + * Each, inbound and inbound keeps 2 separate credit windows, connection one which is common to all streams + * and stream window. + */ +public class StreamFlowControl { + private final FlowControl.Outbound outboundFlowControl; + private final FlowControl.Inbound inboundFlowControl; + + StreamFlowControl(ConnectionFlowControl.Type type, + int streamId, + ConnectionFlowControl connectionFlowControl, + BiConsumer writeUpdate) { + + outboundFlowControl = + new FlowControlImpl.Outbound(type, + streamId, + connectionFlowControl); + + inboundFlowControl = + new FlowControlImpl.Inbound(type, + streamId, + WindowSize.DEFAULT_WIN_SIZE, //FIXME: configurable initial win size + WindowSize.DEFAULT_MAX_FRAME_SIZE, //FIXME: configurable initial max frame size + connectionFlowControl.inbound(), + writeUpdate); + } + + /** + * Outbound flow control, ensures that no more than requested + * amount of data is sent to the other side. + * + * @return outbound flow control + */ + public FlowControl.Outbound outbound() { + return outboundFlowControl; + } + + /** + * Inbound flow control, monitors received but by handler unconsumed data and requests + * more when there is enough space in the buffer. + * + * @return inbound flow control + */ + public FlowControl.Inbound inbound() { + return inboundFlowControl; + } +} diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java index 40f60de679f..1759405acd4 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSize.java @@ -15,7 +15,7 @@ */ package io.helidon.nima.http2; -import java.util.function.Consumer; +import java.util.function.BiConsumer; /** * Window size container, used with {@link io.helidon.nima.http2.FlowControl}. @@ -26,6 +26,14 @@ public interface WindowSize { * Default window size. */ int DEFAULT_WIN_SIZE = 65_535; + /** + * Default and smallest possible setting for MAX_FRAME_SIZE (2^14). + */ + int DEFAULT_MAX_FRAME_SIZE = 16384; + /** + * Largest possible setting for MAX_FRAME_SIZE (2^24-1). + */ + int MAX_MAX_FRAME_SIZE = 16_777_215; /** * Maximal window size. @@ -35,44 +43,44 @@ public interface WindowSize { /** * Create inbound window size container with initial window size set. * + * @param type + * @param streamId * @param initialWindowSize initial window size * @param maxFrameSize maximal frame size * @param windowUpdateWriter writer method for HTTP/2 WINDOW_UPDATE frame * @return a new window size container */ - static WindowSize.Inbound createInbound(int initialWindowSize, + static WindowSize.Inbound createInbound(ConnectionFlowControl.Type type, + int streamId, + int initialWindowSize, int maxFrameSize, - Consumer windowUpdateWriter) { - return new WindowSizeImpl.Inbound(initialWindowSize, maxFrameSize, windowUpdateWriter); - } - - /** - * Create outbound window size container with default initial size. - * - * @return a new window size container - */ - static WindowSize.Outbound createOutbound() { - return new WindowSizeImpl.Outbound(WindowSize.DEFAULT_WIN_SIZE); + BiConsumer windowUpdateWriter) { + return new WindowSizeImpl.Inbound(type, streamId, initialWindowSize, maxFrameSize, windowUpdateWriter); } /** * Create outbound window size container with initial window size set. * - * @param initialWindowSize initial window size + * @param type Server or client + * @param streamId stream id + * @param connectionFlowControl connection flow control * @return a new window size container */ - static WindowSize.Outbound createOutbound(int initialWindowSize) { - return new WindowSizeImpl.Outbound(initialWindowSize); + static WindowSize.Outbound createOutbound(ConnectionFlowControl.Type type, + int streamId, + ConnectionFlowControl connectionFlowControl) { + return new WindowSizeImpl.Outbound(type, streamId, connectionFlowControl); } /** * Create inbound window size container with flow control turned off. * + * @param streamId stream id or 0 for connection * @param windowUpdateWriter WINDOW_UPDATE frame writer * @return a new window size container */ - static WindowSize.Inbound createInboundNoop(Consumer windowUpdateWriter) { - return new WindowSizeImpl.InboundNoop(windowUpdateWriter); + static WindowSize.Inbound createInboundNoop(int streamId, BiConsumer windowUpdateWriter) { + return new WindowSizeImpl.InboundNoop(streamId, windowUpdateWriter); } /** @@ -88,19 +96,20 @@ static WindowSize.Inbound createInboundNoop(Consumer windowUp * @param increment increment * @return whether the increment succeeded */ - boolean incrementWindowSize(int increment); + long incrementWindowSize(int increment); /** * Decrement window size. * * @param decrement decrement + * @return remaining size */ - void decrementWindowSize(int decrement); + int decrementWindowSize(int decrement); /** * Remaining window size. * - * @return remaining sze + * @return remaining size */ int getRemainingWindowSize(); @@ -126,7 +135,6 @@ interface Outbound extends WindowSize { * */ void blockTillUpdate(); - } } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java index ee7750bc26f..6018b878245 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java @@ -15,26 +15,32 @@ */ package io.helidon.nima.http2; -import java.lang.System.Logger.Level; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; +import java.util.function.BiConsumer; + +import static java.lang.System.Logger.Level.DEBUG; +import static java.lang.System.Logger.Level.INFO; /** * Window size container, used with {@link io.helidon.nima.http2.FlowControl}. */ abstract class WindowSizeImpl implements WindowSize { - private static final System.Logger LOGGER = System.getLogger(WindowSizeImpl.class.getName()); + private static final System.Logger LOGGER = System.getLogger(FlowControl.class.getName()); + private final ConnectionFlowControl.Type type; + private final int streamId; private int windowSize; private final AtomicInteger remainingWindowSize; - private WindowSizeImpl(int initialWindowSize) { + private WindowSizeImpl(ConnectionFlowControl.Type type, int streamId, int initialWindowSize) { + this.type = type; + this.streamId = streamId; this.windowSize = initialWindowSize; this.remainingWindowSize = new AtomicInteger(initialWindowSize); } @@ -46,26 +52,23 @@ public void resetWindowSize(int size) { // it maintains by the difference between the new value and the old value remainingWindowSize.updateAndGet(o -> o + size - windowSize); windowSize = size; - LOGGER.log(Level.DEBUG, - () -> String.format("Reset window size %d, remaining %d", windowSize, remainingWindowSize.get())); + LOGGER.log(DEBUG, () -> String.format("%s OFC STR %d: Recv INITIAL_WINDOW_SIZE %d(%d)", + type, streamId, windowSize, remainingWindowSize.get())); } @Override - public boolean incrementWindowSize(int increment) { + public long incrementWindowSize(int increment) { int remaining = remainingWindowSize .getAndUpdate(r -> r < 0 || MAX_WIN_SIZE - r > increment ? increment + r : MAX_WIN_SIZE); - LOGGER.log(Level.DEBUG, - () -> String.format("Increment window size %d, remaining %d", increment, remainingWindowSize.get())); - return MAX_WIN_SIZE - remaining <= increment; + + return remaining + increment; } @Override - public void decrementWindowSize(int decrement) { - remainingWindowSize.updateAndGet(operand -> operand - decrement); - LOGGER.log(Level.DEBUG, - () -> String.format("Decrement window size %d, remaining %d", decrement, remainingWindowSize.get())); + public int decrementWindowSize(int decrement) { + return remainingWindowSize.updateAndGet(operand -> operand - decrement); } @Override @@ -84,24 +87,32 @@ public String toString() { static final class Inbound extends WindowSizeImpl implements WindowSize.Inbound { private final Strategy strategy; + private final ConnectionFlowControl.Type type; + private final int streamId; - Inbound(int initialWindowSize, int maxFrameSize, Consumer windowUpdateWriter) { - super(initialWindowSize); + Inbound(ConnectionFlowControl.Type type, + int streamId, + int initialWindowSize, + int maxFrameSize, + BiConsumer windowUpdateWriter) { + super(type, streamId, initialWindowSize); + this.type = type; + this.streamId = streamId; // Strategy selection based on initialWindowSize and maxFrameSize - this.strategy = Strategy.create(new Strategy.Context(maxFrameSize, initialWindowSize), - windowUpdateWriter); + this.strategy = Strategy.create(new Strategy.Context(maxFrameSize, initialWindowSize), streamId, windowUpdateWriter); } @Override - public boolean incrementWindowSize(int increment) { - boolean result = super.incrementWindowSize(increment); + public long incrementWindowSize(int increment) { // 6.9 // A receiver MUST treat the receipt of a WINDOW_UPDATE frame // with a flow-control window increment of 0 as a stream error if (increment > 0) { - strategy.windowUpdate(increment); + long result = super.incrementWindowSize(increment); + strategy.windowUpdate(this.type, this.streamId, increment); + return result; } - return result; + return super.getRemainingWindowSize(); } } @@ -112,16 +123,20 @@ public boolean incrementWindowSize(int increment) { static final class Outbound extends WindowSizeImpl implements WindowSize.Outbound { private final AtomicReference> updated = new AtomicReference<>(new CompletableFuture<>()); + private final ConnectionFlowControl.Type type; + private final int streamId; - Outbound(int initialWindowSize) { - super(initialWindowSize); + Outbound(ConnectionFlowControl.Type type, int streamId, ConnectionFlowControl connectionFlowControl) { + super(type, streamId, connectionFlowControl.initialWindowSize()); + this.type = type; + this.streamId = streamId; } @Override - public boolean incrementWindowSize(int increment) { - boolean result = super.incrementWindowSize(increment); + public long incrementWindowSize(int increment) { + long remaining = super.incrementWindowSize(increment); triggerUpdate(); - return result; + return remaining; } @Override @@ -131,44 +146,42 @@ public void triggerUpdate() { @Override public void blockTillUpdate() { - while (getRemainingWindowSize() < 1){ + while (getRemainingWindowSize() < 1) { try { //TODO configurable timeout - updated.get().get(100, TimeUnit.MILLISECONDS); + updated.get().get(500, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { - LOGGER.log(Level.WARNING, - () -> String.format("Exception %s caught while waiting for window update: %s", - e.getClass().getName(), - e.getMessage())); + LOGGER.log(DEBUG, () -> String.format("%s OFC STR %d: Window depleted, waiting for update.", type, streamId)); } } } - } /** * Inbound window size container with flow control turned off. */ - public static final class InboundNoop implements WindowSize.Inbound { + public static final class InboundNoop implements WindowSize.Inbound { private static final int WIN_SIZE_WATERMARK = MAX_WIN_SIZE / 2; - private final Consumer windowUpdateWriter; + private final int streamId; + private final BiConsumer windowUpdateWriter; private int delayedIncrement; - InboundNoop(Consumer windowUpdateWriter) { + InboundNoop(int streamId, BiConsumer windowUpdateWriter) { + this.streamId = streamId; this.windowUpdateWriter = windowUpdateWriter; this.delayedIncrement = 0; } @Override - public boolean incrementWindowSize(int increment) { + public long incrementWindowSize(int increment) { // Send WINDOW_UPDATE frame joined for at least 1/2 of the maximum space delayedIncrement += increment; if (delayedIncrement > WIN_SIZE_WATERMARK) { - windowUpdateWriter.accept(new Http2WindowUpdate(delayedIncrement)); + windowUpdateWriter.accept(streamId, new Http2WindowUpdate(delayedIncrement)); delayedIncrement = 0; } - return true; + return getRemainingWindowSize(); } @Override @@ -176,7 +189,8 @@ public void resetWindowSize(int size) { } @Override - public void decrementWindowSize(int decrement) { + public int decrementWindowSize(int decrement) { + return WindowSize.MAX_WIN_SIZE; } @Override @@ -194,26 +208,31 @@ public String toString() { abstract static class Strategy { private final Context context; - private final Consumer windowUpdateWriter; + private final int streamId; + private final BiConsumer windowUpdateWriter; - private Strategy(Context context, Consumer windowUpdateWriter) { + private Strategy(Context context, int streamId, BiConsumer windowUpdateWriter) { this.context = context; + this.streamId = streamId; this.windowUpdateWriter = windowUpdateWriter; } - abstract void windowUpdate(int increment); + abstract void windowUpdate(ConnectionFlowControl.Type type, int increment, int i); Context context() { return context; } - Consumer - windowUpdateWriter() { + int streamId(){ + return this.streamId; + } + + BiConsumer windowUpdateWriter() { return windowUpdateWriter; } private interface StrategyConstructor { - Strategy create(Context context, Consumer windowUpdateWriter); + Strategy create(Context context, int streamId, BiConsumer windowUpdateWriter); } // Strategy Type to instance mapping array @@ -223,9 +242,9 @@ private interface StrategyConstructor { }; // Strategy implementation factory - private static Strategy create(Context context, Consumer windowUpdateWriter) { + private static Strategy create(Context context, int streamId, BiConsumer windowUpdateWriter) { return CREATORS[Type.select(context).ordinal()] - .create(context, windowUpdateWriter); + .create(context, streamId, windowUpdateWriter); } private enum Type { @@ -242,14 +261,14 @@ private enum Type { private static Type select(Context context) { // Bisection strategy requires at least 4 frames to be placed inside window - return context.maxFrameSize * 4 <= context.maxWindowsize ? BISECTION : SIMPLE; + return context.maxFrameSize * 4 <= context.initialWindowSize ? BISECTION : SIMPLE; } } private record Context( int maxFrameSize, - int maxWindowsize) { + int initialWindowSize) { } /** @@ -258,16 +277,16 @@ private record Context( */ private static final class Simple extends Strategy { - private Simple(Context context, Consumer windowUpdateWriter) { - super(context, windowUpdateWriter); + private Simple(Context context, int streamId, BiConsumer windowUpdateWriter) { + super(context, streamId, windowUpdateWriter); } @Override - void windowUpdate(int increment) { - windowUpdateWriter().accept(new Http2WindowUpdate(increment)); - LOGGER.log(Level.DEBUG, - () -> String.format("Window update increment %d", increment)); + void windowUpdate(ConnectionFlowControl.Type type, int streamId, int increment) { + LOGGER.log(INFO, () -> String.format("%s IFC STR %d: Send WINDOW_UPDATE %s", type, streamId, increment)); + windowUpdateWriter().accept(streamId(), new Http2WindowUpdate(increment)); } + } /** @@ -280,26 +299,21 @@ private static final class Bisection extends Strategy { private final int watermark; - private Bisection(Context context, Consumer windowUpdateWriter) { - super(context, windowUpdateWriter); + private Bisection(Context context, int streamId, BiConsumer windowUpdateWriter) { + super(context, streamId, windowUpdateWriter); this.delayedIncrement = 0; - this.watermark = context().maxWindowsize() / 2; + this.watermark = context().initialWindowSize() / 2; } @Override - void windowUpdate(int increment) { + void windowUpdate(ConnectionFlowControl.Type type, int streamId, int increment) { + LOGGER.log(DEBUG, () -> String.format("%s IFC STR %d: Deferred WINDOW_UPDATE %d, total %d, watermark %d", + type, streamId, increment, delayedIncrement, watermark)); delayedIncrement += increment; - LOGGER.log(Level.DEBUG, - () -> String.format("Window update hidden increment %d, total %d, watermark %d", - increment, - delayedIncrement, - watermark)); if (delayedIncrement > watermark) { - windowUpdateWriter().accept(new Http2WindowUpdate(delayedIncrement)); - LOGGER.log(Level.DEBUG, - () -> String.format("Window update real increment %d, watermark %d", - delayedIncrement, - watermark)); + LOGGER.log(DEBUG, () -> String.format("%s IFC STR %d: Send WINDOW_UPDATE %d, watermark %d", + type, streamId, delayedIncrement, watermark)); + windowUpdateWriter().accept(streamId(), new Http2WindowUpdate(delayedIncrement)); delayedIncrement = 0; } } diff --git a/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java b/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java index 14a0923d4b5..ba9fccd9c09 100644 --- a/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java +++ b/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java @@ -264,11 +264,6 @@ private Http2Headers headers(String hexEncoded, DynamicTable dynamicTable) { private Http2Stream stream() { return new Http2Stream() { - private static final FlowControl.Inbound FC_IN_NOOP = FlowControl.builderInbound() - .noop() - .connectionWindowSize(WindowSize.createInboundNoop(http2WindowUpdate -> {})) - .windowUpdateStreamWriter(http2WindowUpdate -> {}) - .build(); @Override public void rstStream(Http2RstStream rstStream) { @@ -306,13 +301,8 @@ public Http2StreamState streamState() { } @Override - public FlowControl.Outbound outboundFlowControl() { - return FlowControl.Outbound.NOOP; - } - - @Override - public FlowControl.Inbound inboundFlowControl() { - return FC_IN_NOOP; + public StreamFlowControl flowControl() { + return null; } }; } @@ -320,7 +310,12 @@ public FlowControl.Inbound inboundFlowControl() { private static class DevNullWriter implements Http2StreamWriter { @Override - public void write(Http2FrameData frame, FlowControl.Outbound flowControl) { + public void write(Http2FrameData frame) { + } + + @Override + public void writeData(Http2FrameData frame, FlowControl.Outbound flowControl) { + } @Override diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java index 027f8c4a7fa..50eea0ab48e 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java @@ -17,7 +17,6 @@ package io.helidon.nima.http2.webclient; import java.io.IOException; -import java.io.InputStream; import java.io.UncheckedIOException; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -51,16 +50,20 @@ import io.helidon.common.socket.SocketOptions; import io.helidon.common.socket.SocketWriter; import io.helidon.common.socket.TlsSocket; -import io.helidon.nima.http2.FlowControl; +import io.helidon.nima.http2.ConnectionFlowControl; import io.helidon.nima.http2.Http2ConnectionWriter; import io.helidon.nima.http2.Http2ErrorCode; +import io.helidon.nima.http2.Http2Exception; import io.helidon.nima.http2.Http2Flag; import io.helidon.nima.http2.Http2FrameData; import io.helidon.nima.http2.Http2FrameHeader; import io.helidon.nima.http2.Http2FrameListener; +import io.helidon.nima.http2.Http2FrameTypes; import io.helidon.nima.http2.Http2GoAway; import io.helidon.nima.http2.Http2Headers; import io.helidon.nima.http2.Http2LoggingFrameListener; +import io.helidon.nima.http2.Http2Ping; +import io.helidon.nima.http2.Http2RstStream; import io.helidon.nima.http2.Http2Setting; import io.helidon.nima.http2.Http2Settings; import io.helidon.nima.http2.Http2WindowUpdate; @@ -98,20 +101,20 @@ class Http2ClientConnection { private final Map> buffer = new HashMap<>(); private final Map streams = new HashMap<>(); private final Lock connectionLock = new ReentrantLock(); + private final ConnectionFlowControl connectionFlowControl; + + private Http2Settings serverSettings = Http2Settings.builder() + .build(); private String channelId; private Socket socket; private PlainSocket helidonSocket; private Http2ConnectionWriter writer; - private InputStream inputStream; private DataReader reader; private DataWriter dataWriter; - private Http2Headers.DynamicTable inboundDynamicTable = + private final Http2Headers.DynamicTable inboundDynamicTable = Http2Headers.DynamicTable.create(Http2Setting.HEADER_TABLE_SIZE.defaultValue()); private Future handleTask; - private int streamInitialWindowSize = WindowSize.DEFAULT_WIN_SIZE; - private final WindowSize.Outbound outboundConnectionWindowSize = WindowSize.createOutbound(); - private int maxFrameSize = 16_384; Http2ClientConnection(ExecutorService executor, SocketOptions socketOptions, @@ -123,6 +126,11 @@ class Http2ClientConnection { this.connectionKey = connectionKey; this.primaryPath = primaryPath; this.priorKnowledge = priorKnowledge; + this.connectionFlowControl = ConnectionFlowControl.createClient(this::writeWindowsUpdate); + } + + private void writeWindowsUpdate(int streamId, Http2WindowUpdate windowUpdateFrame) { + writer.write(windowUpdateFrame.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create())); } Http2ClientConnection connect() { @@ -168,6 +176,9 @@ private void handle() { } else { data = BufferData.empty(); } + + int streamId = frameHeader.streamId(); + switch (frameHeader.type()) { case GO_AWAY: Http2GoAway http2GoAway = Http2GoAway.create(data); @@ -179,31 +190,26 @@ private void handle() { + " lastStreamId: " + http2GoAway.lastStreamId()); case SETTINGS: - Http2Settings http2Settings = Http2Settings.create(data); + serverSettings = Http2Settings.create(data); recvListener.frameHeader(helidonSocket, frameHeader); - recvListener.frame(helidonSocket, http2Settings); + recvListener.frame(helidonSocket, serverSettings); // §4.3.1 Endpoint communicates the size chosen by its HPACK decoder context - inboundDynamicTable.protocolMaxTableSize(http2Settings.value(Http2Setting.HEADER_TABLE_SIZE)); - //FIXME: MAX_FRAME_SIZE can be only int - if (http2Settings.hasValue(Http2Setting.MAX_FRAME_SIZE)) { - maxFrameSize = Math.toIntExact(http2Settings.value(Http2Setting.MAX_FRAME_SIZE)); + inboundDynamicTable.protocolMaxTableSize(serverSettings.value(Http2Setting.HEADER_TABLE_SIZE)); + if (serverSettings.hasValue(Http2Setting.MAX_FRAME_SIZE)) { + connectionFlowControl.resetMaxFrameSize(serverSettings.value(Http2Setting.MAX_FRAME_SIZE).intValue()); } // §6.5.2 Update initial window size for new streams and window sizes of all already existing streams - if (http2Settings.hasValue(Http2Setting.INITIAL_WINDOW_SIZE)) { - long initWinSize = http2Settings.value(Http2Setting.INITIAL_WINDOW_SIZE); - if (initWinSize > WindowSize.MAX_WIN_SIZE) { - goAway(frameHeader.streamId(), Http2ErrorCode.FLOW_CONTROL, "Window size too big. Max: "); - //FIXME: close connection? - return; + if (serverSettings.hasValue(Http2Setting.INITIAL_WINDOW_SIZE)) { + Long initWinSizeLong = serverSettings.value(Http2Setting.INITIAL_WINDOW_SIZE); + if (initWinSizeLong > WindowSize.MAX_WIN_SIZE) { + goAway(streamId, Http2ErrorCode.FLOW_CONTROL, "Window size too big. Max: "); + throw new Http2Exception(Http2ErrorCode.PROTOCOL, + "Received too big INITIAL_WINDOW_SIZE " + initWinSizeLong); } - // Update streams window size - streams.values().forEach(stream -> stream.outboundFlowControl().resetStreamWindowSize((int) initWinSize)); - streamInitialWindowSize = Math.toIntExact(initWinSize); - // Update connection window size - outboundConnectionWindowSize.resetWindowSize((int) initWinSize); - LOGGER.log(DEBUG, - () -> String.format("Http2Settings window size increment on client: %d", - (initWinSize - WindowSize.DEFAULT_WIN_SIZE))); + int initWinSize = initWinSizeLong.intValue(); + connectionFlowControl.resetInitialWindowSize(initWinSize); + streams.values().forEach(stream -> stream.flowControl().outbound().resetStreamWindowSize(initWinSize)); + } // §6.5.3 Settings Synchronization ackSettings(); @@ -211,33 +217,67 @@ private void handle() { return; case WINDOW_UPDATE: - Http2WindowUpdate http2WindowUpdate = Http2WindowUpdate.create(data); + Http2WindowUpdate windowUpdate = Http2WindowUpdate.create(data); recvListener.frameHeader(helidonSocket, frameHeader); - recvListener.frame(helidonSocket, http2WindowUpdate); + recvListener.frame(helidonSocket, windowUpdate); // Outbound flow-control window update - int increment = http2WindowUpdate.windowSizeIncrement(); - if (frameHeader.streamId() == 0) { - outboundConnectionWindowSize.incrementWindowSize(increment); + if (streamId == 0) { + int increment = windowUpdate.windowSizeIncrement(); + boolean overflow; + // overall connection + if (increment == 0) { + Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.PROTOCOL, "Window size 0"); + writer.write(frame.toFrameData(serverSettings, 0, Http2Flag.NoFlags.create())); + } + overflow = connectionFlowControl.incrementOutboundConnectionWindowSize(increment) > WindowSize.MAX_WIN_SIZE; + if (overflow) { + Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.FLOW_CONTROL, "Window size too big. Max: "); + writer.write(frame.toFrameData(serverSettings, 0, Http2Flag.NoFlags.create())); + } + + } else { - streams.get(frameHeader.streamId()) - .outboundFlowControl() - .incrementStreamWindowSize(increment); + streams.get(streamId) + .windowUpdate(windowUpdate); } return; - - default: - if (frameHeader.streamId() != 0) { - try { - // Don't let streams to steal frame parts - // Always read whole frame(frameHeader+data) at once - connectionLock.lock(); - buffer(frameHeader.streamId()).add(new Http2FrameData(frameHeader, data)); - } finally { - connectionLock.unlock(); - } - return; + case PING: + if (streamId != 0) { + throw new Http2Exception(Http2ErrorCode.PROTOCOL, + "Received ping for a stream " + streamId); + } + if (frameHeader.length() != 8) { + throw new Http2Exception(Http2ErrorCode.FRAME_SIZE, + "Received ping with wrong size. Should be 8 bytes, is " + frameHeader.length()); } + if (!frameHeader.flags(Http2FrameTypes.PING).ack()) { + Http2Ping ping = Http2Ping.create(data); + recvListener.frame(helidonSocket, ping); + BufferData frame = ping.data(); + Http2FrameHeader header = Http2FrameHeader.create(frame.available(), + Http2FrameTypes.PING, + Http2Flag.PingFlags.create(Http2Flag.ACK), + 0); + writer.write(new Http2FrameData(header, frame)); + } + break; + + case RST_STREAM: + Http2RstStream rstStream = Http2RstStream.create(data); + recvListener.frame(helidonSocket, rstStream); + stream(streamId).rstStream(rstStream); + break; + case DATA: + connectionFlowControl.decrementInboundConnectionWindowSize(frameHeader.length()); + enqueue(streamId, new Http2FrameData(frameHeader, data)); + break; + + case HEADERS: + enqueue(streamId, new Http2FrameData(frameHeader, data)); + return; + + default: //FIXME: other frame types LOGGER.log(WARNING, "Unsupported frame type!! " + frameHeader.type()); } @@ -247,6 +287,7 @@ private void handle() { Http2ClientStream stream(int priority) { //FIXME: priority return new Http2ClientStream(this, + serverSettings, helidonSocket, streamIdSeq); } @@ -272,6 +313,15 @@ void close() { } } + private void enqueue(int streamId, Http2FrameData frameData){ + try { + connectionLock.lock(); + buffer(streamId).add(frameData); + } finally { + connectionLock.unlock(); + } + } + private void doConnect() throws IOException { boolean useTls = "https".equals(connectionKey.scheme()) && connectionKey.tls() != null; @@ -295,7 +345,6 @@ private void doConnect() throws IOException { dataWriter = SocketWriter.create(executor, helidonSocket, 32); this.reader = new DataReader(helidonSocket); - inputStream = socket.getInputStream(); this.writer = new Http2ConnectionWriter(helidonSocket, dataWriter, List.of()); if (sslSocket != null) { @@ -318,8 +367,7 @@ private void doConnect() throws IOException { sendPreface(true); } - handleTask = java.util.concurrent.Executors.newSingleThreadExecutor().submit(() -> { -// handleTask = executor.submit(() -> { + handleTask = executor.submit(() -> { while (!Thread.interrupted()) { handle(); } @@ -333,13 +381,13 @@ private void ackSettings() { Http2FrameData frameData = http2Settings.toFrameData(null, 0, flags); sendListener.frameHeader(helidonSocket, frameData.header()); sendListener.frame(helidonSocket, http2Settings); - writer.write(frameData, FlowControl.Outbound.NOOP); + writer.write(frameData); } private void goAway(int streamId, Http2ErrorCode errorCode, String msg) { Http2Settings http2Settings = Http2Settings.create(); Http2GoAway frame = new Http2GoAway(streamId, errorCode, msg); - writer.write(frame.toFrameData(http2Settings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); + writer.write(frame.toFrameData(http2Settings, 0, Http2Flag.NoFlags.create())); } private void sendPreface(boolean sendSettings){ @@ -355,16 +403,15 @@ private void sendPreface(boolean sendSettings){ Http2FrameData frameData = http2Settings.toFrameData(null, 0, flags); sendListener.frameHeader(helidonSocket, frameData.header()); sendListener.frame(helidonSocket, http2Settings); - writer.write(frameData, FlowControl.Outbound.NOOP); + writer.write(frameData); } - // todo win update it needed after prolog? // win update - Http2WindowUpdate windowUpdate = new Http2WindowUpdate(10000); + Http2WindowUpdate windowUpdate = new Http2WindowUpdate(10000); //FIXME: configurable Http2Flag.NoFlags flags = Http2Flag.NoFlags.create(); Http2FrameData frameData = windowUpdate.toFrameData(null, 0, flags); sendListener.frameHeader(helidonSocket, frameData.header()); sendListener.frame(helidonSocket, windowUpdate); - writer.write(frameData, FlowControl.Outbound.NOOP); + writer.write(frameData); } private void httpUpgrade() { @@ -459,15 +506,7 @@ public Http2Headers.DynamicTable getInboundDynamicTable() { return this.inboundDynamicTable; } - int streamInitialWindowSize() { - return streamInitialWindowSize; - } - - WindowSize.Outbound outboundConnectionWindowSize() { - return outboundConnectionWindowSize; - } - - public int maxFrameSize() { - return maxFrameSize; + ConnectionFlowControl flowControl(){ + return this.connectionFlowControl; } } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java index 2235b4d4a19..96db8b89429 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java @@ -27,8 +27,8 @@ import io.helidon.nima.http.encoding.ContentDecoder; import io.helidon.nima.http.media.MediaContext; import io.helidon.nima.http.media.ReadableEntityBase; -import io.helidon.nima.http2.FlowControl; import io.helidon.nima.http2.Http2ErrorCode; +import io.helidon.nima.http2.Http2Exception; import io.helidon.nima.http2.Http2Flag; import io.helidon.nima.http2.Http2FrameData; import io.helidon.nima.http2.Http2FrameHeader; @@ -40,31 +40,37 @@ import io.helidon.nima.http2.Http2LoggingFrameListener; import io.helidon.nima.http2.Http2Priority; import io.helidon.nima.http2.Http2RstStream; +import io.helidon.nima.http2.Http2Setting; import io.helidon.nima.http2.Http2Settings; import io.helidon.nima.http2.Http2Stream; import io.helidon.nima.http2.Http2StreamState; import io.helidon.nima.http2.Http2WindowUpdate; +import io.helidon.nima.http2.StreamFlowControl; +import io.helidon.nima.http2.WindowSize; import io.helidon.nima.webclient.ClientResponseEntity; class Http2ClientStream implements Http2Stream { private static final System.Logger LOGGER = System.getLogger(Http2ClientStream.class.getName()); private final Http2ClientConnection connection; + private final Http2Settings serverSettings; private final SocketContext ctx; private final LockingStreamIdSequence streamIdSeq; - private FlowControl.Outbound outboundFlowControl; private final Http2FrameListener sendListener = new Http2LoggingFrameListener("cl-send"); private final Http2FrameListener recvListener = new Http2LoggingFrameListener("cl-recv"); // todo configure private final Http2Settings settings = Http2Settings.create(); - private volatile Http2StreamState state = Http2StreamState.IDLE; + private Http2StreamState state = Http2StreamState.IDLE; private Http2Headers currentHeaders; private int streamId; + private StreamFlowControl flowControl; Http2ClientStream(Http2ClientConnection connection, + Http2Settings serverSettings, SocketContext ctx, LockingStreamIdSequence streamIdSeq) { this.connection = connection; + this.serverSettings = serverSettings; this.ctx = ctx; this.streamIdSeq = streamIdSeq; } @@ -101,25 +107,29 @@ Http2FrameData readOne() { recvListener.frame(ctx, frameData.data()); int flags = frameData.header().flags(); - if ((flags & Http2Flag.END_OF_STREAM) == Http2Flag.END_OF_STREAM) { + boolean endOfStream = (flags & Http2Flag.END_OF_STREAM) == Http2Flag.END_OF_STREAM; + if (endOfStream) { state = Http2StreamState.CLOSED; } switch (frameData.header().type()) { - case DATA: - return frameData; - case HEADERS: - var requestHuffman = new Http2HuffmanDecoder(); - currentHeaders = Http2Headers.create(this, connection.getInboundDynamicTable(), requestHuffman, frameData); - break; - case RST_STREAM: - state = Http2StreamState.CLOSED; - //FIXME: Kill just the stream - throw new RuntimeException("Reset of " + streamId + " stream received!"); - default: - //FIXME: Settings, outbound flow control - LOGGER.log(System.Logger.Level.DEBUG, "Dropping frame " + frameData.header() + " expected header or data."); - } + case DATA: + data(frameData.header(), frameData.data()); + return frameData; + case HEADERS: + var requestHuffman = new Http2HuffmanDecoder(); + Http2Headers http2Headers = Http2Headers.create(this, + connection.getInboundDynamicTable(), + requestHuffman, + frameData); + this.headers(http2Headers, endOfStream); + break; + case RST_STREAM: + this.rstStream(Http2RstStream.create(frameData.data())); + break; + default: + LOGGER.log(System.Logger.Level.DEBUG, "Dropping frame " + frameData.header() + " expected header or data."); + } } return null; } @@ -149,12 +159,10 @@ void write(Http2Headers http2Headers, boolean endOfStream) { // §5.1.1 - The identifier of a newly established stream MUST be numerically // greater than all streams that the initiating endpoint has opened or reserved. this.streamId = streamIdSeq.lockAndNext(); - outboundFlowControl = FlowControl.createOutbound(streamId, - connection.streamInitialWindowSize(), - connection.outboundConnectionWindowSize()); + this.flowControl = connection.flowControl().createStreamFlowControl(streamId); this.connection.addStream(streamId, this); // First call to the server-starting stream, needs to be increasing sequence of odd numbers - connection.getWriter().writeHeaders(http2Headers, streamId, flags, outboundFlowControl); + connection.getWriter().writeHeaders(http2Headers, streamId, flags, flowControl.outbound()); } finally { streamIdSeq.unlock(); } @@ -168,17 +176,14 @@ void writeData(BufferData entityBytes, boolean endOfStream) { : 0), streamId); Http2FrameData frameData = new Http2FrameData(frameHeader, entityBytes); - splitAndWrite(frameData, endOfStream); + splitAndWrite(frameData); } - void splitAndWrite(Http2FrameData frameData, boolean endOfStream) { - // todo handle flow control - int maxFrameSize = connection.maxFrameSize(); - - var frm = frameData; + void splitAndWrite(Http2FrameData frameData) { + int maxFrameSize = this.serverSettings.value(Http2Setting.MAX_FRAME_SIZE).intValue(); // Split to frames if bigger than max frame size - Http2FrameData[] frames = frm.split(maxFrameSize); + Http2FrameData[] frames = frameData.split(maxFrameSize); for (Http2FrameData frame : frames) { write(frame, frame.header().flags(Http2FrameTypes.DATA).endOfStream()); } @@ -204,27 +209,54 @@ private void write(Http2FrameData frameData, boolean endOfStream) { true, endOfStream, false); - connection.getWriter().write(frameData, outboundFlowControl()); + connection.getWriter().writeData(frameData, + flowControl().outbound()); } @Override public void rstStream(Http2RstStream rstStream) { - //FIXME: reset stream + if (state == Http2StreamState.IDLE) { + throw new Http2Exception(Http2ErrorCode.PROTOCOL, + "Received RST_STREAM for stream " + + streamId + " in IDLE state"); + } + state = Http2StreamState.CLOSED; + throw new RuntimeException("Reset of " + streamId + " stream received!"); } @Override public void windowUpdate(Http2WindowUpdate windowUpdate) { - //FIXME: win update + if (state == Http2StreamState.IDLE) { + throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Received WINDOW_UPDATE for stream " + + streamId + " in state IDLE"); + } + + int increment = windowUpdate.windowSizeIncrement(); + + //6.9/2 + if (increment == 0) { + Http2RstStream frame = new Http2RstStream(Http2ErrorCode.PROTOCOL); + connection.getWriter().write(frame.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create())); + } + //6.9.1/3 + if (flowControl.outbound().incrementStreamWindowSize(increment) > WindowSize.MAX_WIN_SIZE) { + Http2RstStream frame = new Http2RstStream(Http2ErrorCode.FLOW_CONTROL); + connection.getWriter().write(frame.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create())); + } + + flowControl() + .outbound() + .incrementStreamWindowSize(increment); } @Override public void headers(Http2Headers headers, boolean endOfStream) { - throw new UnsupportedOperationException("Not applicable on client."); + currentHeaders = headers; } @Override public void data(Http2FrameHeader header, BufferData data) { - throw new UnsupportedOperationException("Not applicable on client."); + flowControl.inbound().incrementWindowSize(header.length()); } @Override @@ -239,19 +271,12 @@ public int streamId() { @Override public Http2StreamState streamState() { - //FIXME: State check - throw new UnsupportedOperationException("Not implemented yet!"); + return state; } @Override - public FlowControl.Outbound outboundFlowControl() { - return outboundFlowControl; - } - - @Override - public FlowControl.Inbound inboundFlowControl() { - //FIXME: inbound flow control - return null; + public StreamFlowControl flowControl() { + return flowControl; } class ClientOutputStream extends OutputStream { diff --git a/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/FlowControlTest.java b/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/FlowControlTest.java deleted file mode 100644 index 1b027093604..00000000000 --- a/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/FlowControlTest.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed 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 io.helidon.nima.http2.webclient; - -import java.io.InputStream; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicLong; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.Test; - -import io.helidon.nima.http2.webserver.Http2Route; -import io.helidon.nima.testing.junit5.webserver.ServerTest; -import io.helidon.nima.testing.junit5.webserver.SetUpServer; -import io.helidon.nima.webserver.WebServer; - -import static io.helidon.common.http.Http.Method.PUT; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; - -@ServerTest -public class FlowControlTest { - - private static final System.Logger LOGGER = System.getLogger(FlowControlTest.class.getName()); - private static volatile CompletableFuture flowControlServerLatch = new CompletableFuture<>(); - private static volatile CompletableFuture flowControlClientLatch = new CompletableFuture<>(); - private static final ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor(); - private static final int TIMEOUT_SEC = 15; - private final WebServer server; - - @SetUpServer - static void setUpServer(WebServer.Builder serverBuilder) { - serverBuilder - .defaultSocket(builder -> builder.port(-1) - .host("localhost") - ) - .routing(router -> router - .route(Http2Route.route(PUT, "/flow-control", (req, res) -> { - StringBuilder sb = new StringBuilder(); - AtomicLong cnt = new AtomicLong(); - InputStream is = req.content().inputStream(); - for (byte[] b = is.readNBytes(5_000); - b.length != 0; - b = is.readNBytes(5_000)) { - int lastLength = b.length; - long receivedData = cnt.updateAndGet(o -> o + lastLength); - if (receivedData > 0) { - // Unblock client to assert sent data - flowControlClientLatch.complete(null); - // Block server, give client time to assert - flowControlServerLatch.join(); - } - sb.append(new String(b)); - } - is.close(); - res.send(sb.toString()); - })) - ); - } - - FlowControlTest(WebServer server) { - this.server = server; - } - - @Test - void flowControl() throws ExecutionException, InterruptedException, TimeoutException { - flowControlServerLatch = new CompletableFuture<>(); - flowControlClientLatch = new CompletableFuture<>(); - AtomicLong sentData = new AtomicLong(); - - var client = Http2Client.builder() - .priorKnowledge(true) - .baseUri("http://localhost:" + server.port()) - .build(); - - String data10k = "Helidon!!!".repeat(1_000); - - var req = client.method(PUT) - .path("/flow-control"); - - CompletableFuture responded = new CompletableFuture<>(); - - exec.submit(() -> { - try (var res = req - .outputStream( - out -> { - for (int i = 0; i < 5; i++) { - byte[] bytes = data10k.getBytes(); - LOGGER.log(System.Logger.Level.INFO, () -> String.format("CL IF: Sending %d bytes", bytes.length)); - out.write(bytes); - sentData.updateAndGet(o -> o + bytes.length); - } - for (int i = 0; i < 5; i++) { - byte[] bytes = data10k.toUpperCase().getBytes(); - LOGGER.log(System.Logger.Level.INFO, () -> String.format("CL IF: Sending %d bytes", bytes.length)); - out.write(bytes); - sentData.updateAndGet(o -> o + bytes.length); - } - out.close(); - } - )) { - responded.complete(res.as(String.class)); - } - }); - - flowControlClientLatch.get(TIMEOUT_SEC, TimeUnit.SECONDS); - // Now client can't send more, because server didn't ask for it (Window update) - Thread.sleep(150); - assertThat(sentData.get(), is(70_000L)); - // Let server ask for the rest of the data - flowControlServerLatch.complete(null); - String response = responded.get(TIMEOUT_SEC, TimeUnit.SECONDS); - assertThat(sentData.get(), is(100_000L)); - assertThat(response, is(data10k.repeat(5) + data10k.toUpperCase().repeat(5))); - } - - @AfterAll - static void afterAll() throws InterruptedException { - exec.shutdown(); - if (!exec.awaitTermination(TIMEOUT_SEC, TimeUnit.SECONDS)) { - exec.shutdownNow(); - } - } -} diff --git a/nima/http2/webclient/src/test/resources/logging-test.properties b/nima/http2/webclient/src/test/resources/logging-test.properties index 253bb2c1bc2..ebb86e90e96 100644 --- a/nima/http2/webclient/src/test/resources/logging-test.properties +++ b/nima/http2/webclient/src/test/resources/logging-test.properties @@ -15,14 +15,17 @@ # # Send messages to the console -handlers=io.helidon.logging.jul.HelidonConsoleHandler +handlers=java.util.logging.ConsoleHandler java.util.logging.ConsoleHandler.level=INFO java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter # java.util.logging.SimpleFormatter.format = [%1$tc] %5$s %6$s%n -#java.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS %5$s%6$s%n -java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n +java.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS %5$s%6$s%n +#java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s %5$s%6$s%n io.helidon.nima.level=INFO +io.helidon.nima.http2.WindowSizeImpl.level=ALL +io.helidon.nima.http2.FlowControl.level=ALL +io.helidon.nima.http2.FlowControlImpl.level=ALL # Global logging level. Can be overridden by specific loggers .level=INFO diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java index 147e8e18652..07601a43edc 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java @@ -47,14 +47,6 @@ public interface Http2Config { @ConfiguredOption("0xFFFFFFFFL") long maxHeaderListSize(); - /** - * Initial maximal size of client frames. - * - * @return maximal size in bytes - */ - @ConfiguredOption("16384") - int maxClientFrameSize(); - /** * Maximum number of concurrent streams that the server will allow. * Defaults to {@code 8192}. This limit is directional: it applies to the number of streams that the sender diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java index 38e8ca558b1..a6920614315 100755 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java @@ -20,7 +20,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import io.helidon.common.buffers.BufferData; import io.helidon.common.buffers.DataReader; @@ -30,7 +29,7 @@ import io.helidon.common.http.Http.HeaderValues; import io.helidon.common.http.HttpPrologue; import io.helidon.common.task.InterruptableTask; -import io.helidon.nima.http2.FlowControl; +import io.helidon.nima.http2.ConnectionFlowControl; import io.helidon.nima.http2.Http2ConnectionWriter; import io.helidon.nima.http2.Http2ErrorCode; import io.helidon.nima.http2.Http2Exception; @@ -90,6 +89,7 @@ public class Http2Connection implements ServerConnection, InterruptableTask subProviders) { this.ctx = ctx; @@ -129,26 +122,26 @@ public class Http2Connection implements ServerConnection, InterruptableTask 0 - ? http2Config.maxStreamWindowSize() - : http2Config.maxWindowSize(); - } else { - // Pass NOOP when flow control is turned off (but we still have to send WINDOW_UPDATE frames) - this.inboundWindowSize = WindowSize.createInboundNoop(this::writeWindowUpdateFrame); - this.inboundInitialWindowSize = WindowSize.MAX_WIN_SIZE; - } +// if (http2Config.flowControlEnabled()) { +// this.inboundWindowSize = WindowSize.createInbound(FlowControl.Type.SERVER, +// 0, +// WindowSize.DEFAULT_WIN_SIZE, +// http2Config.maxFrameSize(), +// this::writeWindowUpdateFrame); +// this.inboundInitialWindowSize = http2Config.maxStreamWindowSize() > 0 +// ? http2Config.maxStreamWindowSize() +// : http2Config.maxWindowSize(); +// } else { +// // Pass NOOP when flow control is turned off (but we still have to send WINDOW_UPDATE frames) +// this.inboundWindowSize = WindowSize.createInboundNoop(this::writeWindowUpdateFrame); +// this.inboundInitialWindowSize = WindowSize.DEFAULT_WIN_SIZE; +// } + + // Flow control is initialized by RFC 9113 default values + this.flowControl = ConnectionFlowControl.createServer(this::writeWindowUpdateFrame); } @Override @@ -169,7 +162,7 @@ public void handle() throws InterruptedException { Http2GoAway frame = new Http2GoAway(0, e.code(), sendErrorDetails ? e.getMessage() : ""); - connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); + connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create())); state = State.FINISHED; } catch (CloseConnectionException | InterruptedException e) { throw e; @@ -181,22 +174,79 @@ public void handle() throws InterruptedException { Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.INTERNAL, sendErrorDetails ? e.getClass().getName() + ": " + e.getMessage() : ""); - connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); + connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create())); state = State.FINISHED; throw e; } } /** - * Client settings, obtained from HTTP/2 upgrade request. + * Client settings, obtained from SETTINGS frame or HTTP/2 upgrade request. * * @param http2Settings client settings to use */ public void clientSettings(Http2Settings http2Settings) { this.clientSettings = http2Settings; + this.receiveFrameListener.frame(ctx, clientSettings); if (this.clientSettings.hasValue(Http2Setting.HEADER_TABLE_SIZE)) { updateHeaderTableSize(clientSettings.value(Http2Setting.HEADER_TABLE_SIZE)); } + + if (this.clientSettings.hasValue(Http2Setting.INITIAL_WINDOW_SIZE)) { + Long initialWindowSize = clientSettings.value(Http2Setting.INITIAL_WINDOW_SIZE); + + //6.9.2/3 - legal range for the increment to the flow-control window is 1 to 2^31-1 (2,147,483,647) octets. + if (initialWindowSize > WindowSize.MAX_WIN_SIZE) { + Http2GoAway frame = new Http2GoAway(0, + Http2ErrorCode.FLOW_CONTROL, + "Window " + initialWindowSize + " size too large"); + connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create())); + } + + //6.9.1/1 - changing the flow-control window for streams that are not yet active + flowControl.resetInitialWindowSize(initialWindowSize.intValue()); + + //6.9.2/1 - SETTINGS frame can alter the initial flow-control + // window size for streams with active flow-control windows (that is, + // streams in the "open" or "half-closed (remote)" state) + for (StreamContext sctx : streams.values()) { + Http2StreamState streamState = sctx.stream.streamState(); + if (streamState == Http2StreamState.OPEN || streamState == Http2StreamState.HALF_CLOSED_REMOTE) { + sctx.stream.flowControl().outbound().resetStreamWindowSize(initialWindowSize.intValue()); + } + } + + // Unblock frames waiting for update + this.flowControl.outbound().triggerUpdate(); + } + + if (this.clientSettings.hasValue(Http2Setting.MAX_FRAME_SIZE)) { + Long maxFrameSize = this.clientSettings.value(Http2Setting.MAX_FRAME_SIZE); + // specification defines, that the frame size must be between the initial size (16384) and 2^24-1 + if (maxFrameSize < WindowSize.DEFAULT_MAX_FRAME_SIZE || maxFrameSize > WindowSize.MAX_MAX_FRAME_SIZE) { + throw new Http2Exception(Http2ErrorCode.PROTOCOL, + "Frame size must be between 2^14 and 2^24-1, but is: " + maxFrameSize); + } + + flowControl.resetMaxFrameSize(maxFrameSize.intValue()); + } + + // Set server MAX_CONCURRENT_STREAMS limit when client sends number lower than hard limit + // from configuration. Refuse settings if client sends larger number than is configured. + this.clientSettings.presentValue(Http2Setting.MAX_CONCURRENT_STREAMS) + .ifPresent(it -> { + if (http2Config.maxConcurrentStreams() >= it) { + maxClientConcurrentStreams = it; + } else { + Http2GoAway frame = + new Http2GoAway(0, + Http2ErrorCode.PROTOCOL, + "Value of maximum concurrent streams limit " + it + + " exceeded hard limit value " + + http2Config.maxConcurrentStreams()); + connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create())); + } + }); } /** @@ -376,8 +426,7 @@ private void doContinuation() { } private void writeServerSettings() { - connectionWriter.write(serverSettings - .toFrameData(serverSettings, 0, Http2Flag.SettingsFlags.create(0)), FlowControl.Outbound.NOOP); + connectionWriter.write(serverSettings.toFrameData(serverSettings, 0, Http2Flag.SettingsFlags.create(0))); state = State.READ_FRAME; } @@ -387,24 +436,23 @@ private void readWindowUpdateFrame() { state = State.READ_FRAME; boolean overflow; + int increment = windowUpdate.windowSizeIncrement(); + int streamId = frameHeader.streamId(); - if (frameHeader.streamId() == 0) { + if (streamId == 0) { // overall connection - // todo implement - if (windowUpdate.windowSizeIncrement() == 0) { + if (increment == 0) { Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.PROTOCOL, "Window size 0"); - connectionWriter.write(frame - .toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); + connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create())); } - overflow = outboundWindowSize.incrementWindowSize(windowUpdate.windowSizeIncrement()); + overflow = flowControl.incrementOutboundConnectionWindowSize(increment) > WindowSize.MAX_WIN_SIZE; if (overflow) { Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.FLOW_CONTROL, "Window size too big. Max: "); - connectionWriter.write(frame - .toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); + connectionWriter.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create())); } } else { try { - StreamContext stream = stream(frameHeader.streamId()); + StreamContext stream = stream(streamId); stream.stream().windowUpdate(windowUpdate); } catch (Http2Exception ignored) { // stream closed @@ -413,10 +461,8 @@ private void readWindowUpdateFrame() { } // Used in inbound flow control instance to write WINDOW_UPDATE frame. - private void writeWindowUpdateFrame(Http2WindowUpdate windowUpdateFrame) { - LOGGER.log(DEBUG, () -> String.format("SRV IFC: Sending WINDOW_UPDATE %s", windowUpdateFrame)); - connectionWriter.write(windowUpdateFrame - .toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); + private void writeWindowUpdateFrame(int streamId, Http2WindowUpdate windowUpdateFrame) { + connectionWriter.write(windowUpdateFrame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create())); } private void doSettings() { @@ -431,68 +477,9 @@ private void doSettings() { throw new Http2Exception(Http2ErrorCode.FRAME_SIZE, "Settings with ACK should not have payload."); } } else { - this.clientSettings = Http2Settings.create(inProgressFrame()); - this.receiveFrameListener.frame(ctx, clientSettings); - - this.clientSettings.presentValue(Http2Setting.HEADER_TABLE_SIZE) - .ifPresent(this::updateHeaderTableSize); - - Optional windowSize = this.clientSettings.presentValue(Http2Setting.INITIAL_WINDOW_SIZE); - if (windowSize.isPresent()) { - long it = windowSize.get(); - - //6.9.2/3 - legal range for the increment to the flow-control window is 1 to 2^31-1 (2,147,483,647) octets. - if (it > WindowSize.MAX_WIN_SIZE) { - Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.FLOW_CONTROL, "Window " + it + " size too large"); - connectionWriter.write(frame - .toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); - } - - //6.9.1/1 - changing the flow-control window for streams that are not yet active - outboundInitialWindowSize = (int) it; - - //6.9.2/1 - SETTINGS frame can alter the initial flow-control - // window size for streams with active flow-control windows (that is, - // streams in the "open" or "half-closed (remote)" state) - for (StreamContext sctx : streams.values()) { - Http2StreamState streamState = sctx.stream.streamState(); - if (streamState == Http2StreamState.OPEN || streamState == Http2StreamState.HALF_CLOSED_REMOTE) { - sctx.stream.outboundFlowControl().resetStreamWindowSize((int) it); - } - } - - // Unblock frames waiting for update - this.outboundWindowSize.triggerUpdate(); - } - - this.clientSettings.presentValue(Http2Setting.MAX_FRAME_SIZE) - .ifPresent(it -> this.maxClientFrameSize = it); - - // specification defines, that the frame size must be between the initial size (16384) and 2^24-1 - if (this.maxClientFrameSize < 16_384 || this.maxClientFrameSize > 16_777_215) { - throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Frame size must be between 2^14 and 2^24-1, but is: " - + maxClientFrameSize); - } - - // Set server MAX_CONCURRENT_STREAMS limit when client sends number lower than hard limit - // from configuration. Refuse settings if client sends larger number than is configured. - this.clientSettings.presentValue(Http2Setting.MAX_CONCURRENT_STREAMS) - .ifPresent(it -> { - if (http2Config.maxConcurrentStreams() >= it) { - maxClientConcurrentStreams = it; - } else { - Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.PROTOCOL, - "Value of maximum concurrent streams limit " + it - + " exceeded hard limit value " + http2Config.maxConcurrentStreams()); - connectionWriter.write( - frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()), - FlowControl.Outbound.NOOP); - - } - }); + clientSettings(Http2Settings.create(inProgressFrame())); // TODO for each - // Http2Setting.MAX_CONCURRENT_STREAMS; // Http2Setting.MAX_HEADER_LIST_SIZE; state = State.ACK_SETTINGS; } @@ -501,7 +488,7 @@ private void doSettings() { private void ackSettings() { Http2Flag.SettingsFlags flags = Http2Flag.SettingsFlags.create(Http2Flag.ACK); Http2FrameHeader header = Http2FrameHeader.create(0, Http2FrameTypes.SETTINGS, flags, 0); - connectionWriter.write(new Http2FrameData(header, BufferData.empty()), FlowControl.Outbound.NOOP); + connectionWriter.write(new Http2FrameData(header, BufferData.empty())); state = State.READ_FRAME; if (upgradeHeaders != null) { @@ -522,21 +509,19 @@ private void ackSettings() { private void dataFrame() { BufferData buffer; - StreamContext stream = stream(frameHeader.streamId()); + int streamId = frameHeader.streamId(); + StreamContext stream = stream(streamId); stream.stream().checkDataReceivable(); - // todo we need to have some information about how much data is buffered for a stream - // to prevent OOM (use flow control!) // Flow-control: reading frameHeader.length() bytes from HTTP2 socket for known stream ID. int length = frameHeader.length(); if (length > 0) { - int streamId = frameHeader.streamId(); if (streamId > 0 && frameHeader.type() != Http2FrameType.HEADERS) { - // Stream ID > 0: update conenction and stream - FlowControl.Inbound inboundFlowControl = stream(streamId) - .stream() - .inboundFlowControl(); - inboundFlowControl.decrementWindowSize(length); + // Stream ID > 0: update connection and stream + stream.stream() + .flowControl() + .inbound() + .decrementWindowSize(length); } } @@ -681,7 +666,7 @@ private void writePingAck() { Http2Flag.PingFlags.create(Http2Flag.ACK), 0); ping = null; - connectionWriter.write(new Http2FrameData(header, frame), FlowControl.Outbound.NOOP); + connectionWriter.write(new Http2FrameData(header, frame)); state = State.READ_FRAME; } @@ -745,16 +730,16 @@ private StreamContext stream(int streamId) { "Maximum concurrent streams limit " + maxClientConcurrentStreams + " exceeded"); } // Pass NOOP when flow control is turned off - FlowControl.Inbound.Builder inboundFlowControlBuilder = http2Config.flowControlEnabled() - ? FlowControl.builderInbound() - .streamId(streamId) - .connectionWindowSize(inboundWindowSize) - .streamWindowSize(inboundInitialWindowSize) - .streamMaxFrameSize(http2Config.maxFrameSize()) - // Pass NOOP when flow control is turned off (but we still have to send WINDOW_UPDATE frames) - : FlowControl.builderInbound() - .connectionWindowSize(inboundWindowSize) - .noop(); +// FlowControl.Inbound.Builder inboundFlowControlBuilder = http2Config.flowControlEnabled() +// ? FlowControl.builderInbound(FlowControl.Type.SERVER) +// .streamId(streamId) +// .connectionWindowSize(inboundWindowSize) +// .streamWindowSize(inboundInitialWindowSize) +// .streamMaxFrameSize(http2Config.maxFrameSize()) +// // Pass NOOP when flow control is turned off (but we still have to send WINDOW_UPDATE frames) +// : FlowControl.builderInbound(FlowControl.Type.SERVER) +// .connectionWindowSize(inboundWindowSize) +// .noop(); streamContext = new StreamContext(streamId, new Http2Stream(ctx, routing, @@ -764,10 +749,7 @@ private StreamContext stream(int streamId) { serverSettings, clientSettings, connectionWriter, - inboundFlowControlBuilder, - FlowControl.createOutbound(streamId, - outboundInitialWindowSize, - outboundWindowSize))); + flowControl)); streams.put(streamId, streamContext); } diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ServerResponse.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ServerResponse.java index 776eeca936a..f0aa6b065b5 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ServerResponse.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2ServerResponse.java @@ -328,7 +328,7 @@ private void writeChunk(BufferData buffer) { bytesWritten += frameData.header().length(); bytesWritten += Http2FrameHeader.LENGTH; - writer.write(frameData, flowControl); + writer.writeData(frameData, flowControl); } private void sendEndOfStream() { @@ -340,7 +340,7 @@ private void sendEndOfStream() { bytesWritten += frameData.header().length(); bytesWritten += Http2FrameHeader.LENGTH; - writer.write(frameData, flowControl); + writer.writeData(frameData, flowControl); } } } diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java index 9e471d0803e..d6cc6f3f72c 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Stream.java @@ -29,7 +29,7 @@ import io.helidon.common.http.ServerResponseHeaders; import io.helidon.common.socket.SocketWriterException; import io.helidon.nima.http.encoding.ContentDecoder; -import io.helidon.nima.http2.FlowControl; +import io.helidon.nima.http2.ConnectionFlowControl; import io.helidon.nima.http2.Http2ErrorCode; import io.helidon.nima.http2.Http2Exception; import io.helidon.nima.http2.Http2Flag; @@ -43,6 +43,8 @@ import io.helidon.nima.http2.Http2StreamState; import io.helidon.nima.http2.Http2StreamWriter; import io.helidon.nima.http2.Http2WindowUpdate; +import io.helidon.nima.http2.StreamFlowControl; +import io.helidon.nima.http2.WindowSize; import io.helidon.nima.http2.webserver.spi.Http2SubProtocolSelector; import io.helidon.nima.http2.webserver.spi.SubProtocolResult; import io.helidon.nima.webserver.CloseConnectionException; @@ -70,6 +72,8 @@ public class Http2Stream implements Runnable, io.helidon.nima.http2.Http2Stream private final Http2StreamWriter writer; private final Router router; private final ArrayBlockingQueue inboundData = new ArrayBlockingQueue<>(32); + private final StreamFlowControl flowControl; + private boolean wasLastDataFrame = false; private volatile Http2Headers headers; private volatile Http2Priority priority; @@ -79,8 +83,6 @@ public class Http2Stream implements Runnable, io.helidon.nima.http2.Http2Stream private long expectedLength = -1; private HttpRouting routing; private HttpPrologue prologue; - private final FlowControl.Inbound inboundFlowControl; - private final FlowControl.Outbound outboundFlowControl; /** * A new HTTP/2 server stream. @@ -93,8 +95,7 @@ public class Http2Stream implements Runnable, io.helidon.nima.http2.Http2Stream * @param serverSettings server settings * @param clientSettings client settings * @param writer writer - * @param inboundFlowControlBuilder inbound flow control builder - * @param outboundFlowControl outbound flow control + * @param connectionFlowControl connection flow control */ public Http2Stream(ConnectionContext ctx, HttpRouting routing, @@ -104,8 +105,7 @@ public Http2Stream(ConnectionContext ctx, Http2Settings serverSettings, Http2Settings clientSettings, Http2StreamWriter writer, - FlowControl.Inbound.Builder inboundFlowControlBuilder, - FlowControl.Outbound outboundFlowControl) { + ConnectionFlowControl connectionFlowControl) { this.ctx = ctx; this.routing = routing; this.http2Config = http2Config; @@ -115,10 +115,7 @@ public Http2Stream(ConnectionContext ctx, this.clientSettings = clientSettings; this.writer = writer; this.router = ctx.router(); - this.inboundFlowControl = inboundFlowControlBuilder - .windowUpdateStreamWriter(this::writeWindowUpdate) - .build(); - this.outboundFlowControl = outboundFlowControl; + this.flowControl = connectionFlowControl.createStreamFlowControl(streamId); } /** @@ -191,22 +188,15 @@ public void windowUpdate(Http2WindowUpdate windowUpdate) { //6.9/2 if (windowUpdate.windowSizeIncrement() == 0) { Http2RstStream frame = new Http2RstStream(Http2ErrorCode.PROTOCOL); - writer.write(frame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); + writer.write(frame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create())); } //6.9.1/3 - if (outboundFlowControl.incrementStreamWindowSize(windowUpdate.windowSizeIncrement())) { + if (flowControl.outbound().incrementStreamWindowSize(windowUpdate.windowSizeIncrement()) > WindowSize.MAX_WIN_SIZE) { Http2RstStream frame = new Http2RstStream(Http2ErrorCode.FLOW_CONTROL); - writer.write(frame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); + writer.write(frame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create())); } } - // Used in inbound flow control instance to write WINDOW_UPDATE frame. - void writeWindowUpdate(Http2WindowUpdate windowUpdate) { - LOGGER.log(System.Logger.Level.DEBUG, () -> String.format("SRV IFC: Sending stream WINDOW_UPDATE %s", windowUpdate)); - writer.write(windowUpdate.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()), FlowControl.Outbound.NOOP); - } - - // this method is called from connection thread and start the // thread o this stream @Override @@ -229,7 +219,7 @@ public void data(Http2FrameHeader header, BufferData data) { if (expectedLength != -1 && expectedLength < header.length()) { state = Http2StreamState.CLOSED; Http2RstStream rst = new Http2RstStream(Http2ErrorCode.PROTOCOL); - writer.write(rst.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()), outboundFlowControl); + writer.write(rst.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create())); return; } if (expectedLength != -1) { @@ -262,13 +252,8 @@ public Http2StreamState streamState() { } @Override - public FlowControl.Outbound outboundFlowControl() { - return outboundFlowControl; - } - - @Override - public FlowControl.Inbound inboundFlowControl() { - return inboundFlowControl; + public StreamFlowControl flowControl() { + return this.flowControl; } @Override @@ -280,7 +265,7 @@ public void run() { handle(); } catch (SocketWriterException | CloseConnectionException | UncheckedIOException e) { Http2RstStream rst = new Http2RstStream(Http2ErrorCode.STREAM_CLOSED); - writer.write(rst.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create()), outboundFlowControl); + writer.write(rst.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create())); // no sense in throwing an exception, as this is invoked from an executor service directly } catch (RequestException e) { DirectHandler handler = ctx.listenerContext() @@ -302,7 +287,7 @@ public void run() { writer.writeHeaders(http2Headers, streamId, Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS | Http2Flag.END_OF_STREAM), - outboundFlowControl); + flowControl.outbound()); } else { Http2FrameHeader dataHeader = Http2FrameHeader.create(message.length, Http2FrameTypes.DATA, @@ -312,7 +297,7 @@ public void run() { streamId, Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS), new Http2FrameData(dataHeader, BufferData.create(message)), - outboundFlowControl); + flowControl.outbound()); } } finally { headers = null; @@ -332,7 +317,7 @@ private BufferData readEntityFromPipeline() { DataFrame frame; try { frame = inboundData.take(); - inboundFlowControl().incrementWindowSize(frame.header().length()); + flowControl.inbound().incrementWindowSize(frame.header().length()); } catch (InterruptedException e) { // this stream was interrupted, does not make sense to do anything else return BufferData.empty(); @@ -393,7 +378,7 @@ private void handle() { decoder, streamId, this::readEntityFromPipeline); - Http2ServerResponse response = new Http2ServerResponse(ctx, request, writer, streamId, outboundFlowControl); + Http2ServerResponse response = new Http2ServerResponse(ctx, request, writer, streamId, flowControl.outbound()); try { routing.route(ctx, request, response); } finally { diff --git a/nima/tests/integration/http2/server/pom.xml b/nima/tests/integration/http2/server/pom.xml index 2183e0fcf0c..dc2b6c10af8 100644 --- a/nima/tests/integration/http2/server/pom.xml +++ b/nima/tests/integration/http2/server/pom.xml @@ -37,6 +37,11 @@ helidon-nima-testing-junit5-webserver test + + io.helidon.common + helidon-common-reactive + test + org.junit.jupiter junit-jupiter-api @@ -47,5 +52,10 @@ hamcrest-all test + + io.helidon.nima.http2 + helidon-nima-http2-webclient + test + diff --git a/nima/tests/integration/http2/server/src/main/java/module-info.java b/nima/tests/integration/http2/server/src/main/java/module-info.java new file mode 100644 index 00000000000..187e694572b --- /dev/null +++ b/nima/tests/integration/http2/server/src/main/java/module-info.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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. + */ +module helidon.nima.tests.integration.http2.webserver { +} \ No newline at end of file diff --git a/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java new file mode 100644 index 00000000000..14d846e1166 --- /dev/null +++ b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.tests.integration.http2.webserver; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; + +import io.helidon.common.reactive.BufferedEmittingPublisher; +import io.helidon.common.reactive.Multi; +import io.helidon.nima.http2.webclient.Http2Client; +import io.helidon.nima.http2.webserver.DefaultHttp2Config; +import io.helidon.nima.http2.webserver.Http2ConnectionProvider; +import io.helidon.nima.http2.webserver.Http2Route; +import io.helidon.nima.testing.junit5.webserver.ServerTest; +import io.helidon.nima.testing.junit5.webserver.SetUpServer; +import io.helidon.nima.webserver.WebServer; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static io.helidon.common.http.Http.Method.GET; +import static io.helidon.common.http.Http.Method.PUT; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThan; + +@ServerTest +public class FlowControlTest { + + private static final System.Logger LOGGER = System.getLogger(FlowControlTest.class.getName()); + private static volatile CompletableFuture flowControlServerLatch = new CompletableFuture<>(); + private static volatile CompletableFuture flowControlClientLatch = new CompletableFuture<>(); + private static final ExecutorService exec = Executors.newCachedThreadPool(); + private static final String DATA_10_K = "Helidon!!!".repeat(1_000); + private static final String EXPECTED = DATA_10_K.repeat(5) + DATA_10_K.toUpperCase().repeat(5); + private static final int TIMEOUT_SEC = 150; + private final WebServer server; + + @SetUpServer + static void setUpServer(WebServer.Builder serverBuilder) { + serverBuilder + .addConnectionProvider(Http2ConnectionProvider.builder() + .http2Config(DefaultHttp2Config.builder() + .maxWindowSize(65537) + ) + .build()) + .defaultSocket(builder -> builder + .port(-1) + .host("localhost") + ) + .routing(router -> router + .route(Http2Route.route(GET, "/", (req, res) -> res.send("OK"))) + .route(Http2Route.route(PUT, "/flow-control", (req, res) -> { + StringBuilder sb = new StringBuilder(); + AtomicLong cnt = new AtomicLong(); + InputStream is = req.content().inputStream(); + for (byte[] b = is.readNBytes(5_000); + b.length != 0; + b = is.readNBytes(5_000)) { + int lastLength = b.length; + long receivedData = cnt.updateAndGet(o -> o + lastLength); + if (receivedData > 0) { + // Unblock client to assert sent data + flowControlClientLatch.complete(null); + // Block server, give client time to assert + flowControlServerLatch.join(); + } + sb.append(new String(b)); + } + is.close(); + res.send(sb.toString()); + + })) + .route(Http2Route.route(GET, "/flow-control", (req, res) -> { + res.send(EXPECTED); + })) + ); + } + + FlowControlTest(WebServer server) { + this.server = server; + } + + @Test + void flowControlWebClientInOut() throws ExecutionException, InterruptedException, TimeoutException { + flowControlServerLatch = new CompletableFuture<>(); + flowControlClientLatch = new CompletableFuture<>(); + AtomicLong sentData = new AtomicLong(); + + var client = Http2Client.builder() + .priorKnowledge(true) + .baseUri("http://localhost:" + server.port()) + .build(); + + var req = client.method(PUT) + .path("/flow-control"); + + CompletableFuture responded = new CompletableFuture<>(); + + exec.submit(() -> { + try (var res = req + .outputStream( + out -> { + for (int i = 0; i < 5; i++) { + byte[] bytes = DATA_10_K.getBytes(); + LOGGER.log(System.Logger.Level.DEBUG, + () -> String.format("CL: Sending %d bytes", bytes.length)); + out.write(bytes); + sentData.updateAndGet(o -> o + bytes.length); + } + for (int i = 0; i < 5; i++) { + byte[] bytes = DATA_10_K.toUpperCase().getBytes(); + LOGGER.log(System.Logger.Level.DEBUG, + () -> String.format("CL: Sending %d bytes", bytes.length)); + out.write(bytes); + sentData.updateAndGet(o -> o + bytes.length); + } + out.close(); + } + )) { + responded.complete(res.as(String.class)); + } + }); + + flowControlClientLatch.get(TIMEOUT_SEC, TimeUnit.SECONDS); + // Now client can't send more, because server didn't ask for it (Window update) + // Wait a bit if more than allowed is sent + Thread.sleep(300); + // Depends on the win update strategy, can't be full 100k + assertThat(sentData.get(), lessThan(99_000L)); + // Let server ask for the rest of the data + flowControlServerLatch.complete(null); + String response = responded.get(TIMEOUT_SEC, TimeUnit.SECONDS); + assertThat(sentData.get(), is(100_000L)); + assertThat(response, is(EXPECTED)); + } + + @Test + void flowControlWebClientInbound() { + var client = Http2Client.builder() + .priorKnowledge(true) + .baseUri("http://localhost:" + server.port()) + .build(); + + try (var res = client.method(GET) + .path("/flow-control") + .request()) { + assertThat(res.entity().as(String.class), is(EXPECTED)); + } + } + + @Test + void flowControlHttpClientInOut() throws ExecutionException, InterruptedException, TimeoutException, IOException { + flowControlServerLatch = new CompletableFuture<>(); + flowControlClientLatch = new CompletableFuture<>(); + AtomicLong sentData = new AtomicLong(); + + BufferedEmittingPublisher publisher = BufferedEmittingPublisher.create(); + + HttpClient cl = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_2) + .connectTimeout(Duration.ofSeconds(5)) + .build(); + + cl.send(HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + server.port())) + .GET().build(), HttpResponse.BodyHandlers.discarding()); + + CompletableFuture responded = new CompletableFuture<>(); + + exec.submit(() -> { + try { + HttpResponse response = + cl.send(HttpRequest.newBuilder() + .timeout(Duration.ofSeconds(50)) + .uri(URI.create("http://localhost:" + server.port() + "/flow-control")) + .PUT(HttpRequest.BodyPublishers.fromPublisher( + Multi.create(publisher) + .peek(bb -> sentData.updateAndGet( + o -> o + bb.array().length)) + .log(Level.FINE) + )) + .build(), + HttpResponse.BodyHandlers.ofString()); + responded.complete(response.body()); + } catch (IOException | InterruptedException e) { + responded.completeExceptionally(e); + } + }); + + for (int i = 0; i < 5; i++) { + byte[] bytes = DATA_10_K.getBytes(); + LOGGER.log(System.Logger.Level.DEBUG, () -> String.format("CL: Sending %d bytes", bytes.length)); + publisher.emit(ByteBuffer.wrap(bytes)); + } + for (int i = 0; i < 5; i++) { + byte[] bytes = DATA_10_K.toUpperCase().getBytes(); + LOGGER.log(System.Logger.Level.DEBUG, () -> String.format("CL: Sending %d bytes", bytes.length)); + publisher.emit(ByteBuffer.wrap(bytes)); + } + + publisher.complete(); + + flowControlClientLatch.get(TIMEOUT_SEC, TimeUnit.SECONDS); + // Now client can't send more, because server didn't ask for it (Window update) + // Wait a bit if more than allowed is sent + Thread.sleep(300); + // Depends on the win update strategy, can't be full 100k + assertThat(sentData.get(), lessThan(99_000L)); + // Let server ask for the rest of the data + flowControlServerLatch.complete(null); + String response = responded.get(TIMEOUT_SEC, TimeUnit.SECONDS); + assertThat(sentData.get(), is(100_000L)); + assertThat(response, is(EXPECTED)); + } + + // @Test + void name() { + flowControlServerLatch = new CompletableFuture<>(); + flowControlClientLatch = new CompletableFuture<>(); + flowControlServerLatch.complete(null); + new CompletableFuture().join(); + } + + @AfterAll + static void afterAll() throws InterruptedException { + exec.shutdown(); + if (!exec.awaitTermination(TIMEOUT_SEC, TimeUnit.SECONDS)) { + exec.shutdownNow(); + } + } +} diff --git a/nima/tests/integration/http2/server/src/test/java/module-info.java b/nima/tests/integration/http2/server/src/test/java/module-info.java new file mode 100644 index 00000000000..59320d44af8 --- /dev/null +++ b/nima/tests/integration/http2/server/src/test/java/module-info.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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. + */ +open module helidon.nima.tests.integration.http2.webserver { + requires java.logging; + requires java.net.http; + requires hamcrest.all; + requires org.junit.jupiter.api; + requires io.helidon.nima.http2.webserver; + requires io.helidon.nima.testing.junit5.webserver; + requires io.helidon.common.reactive; + requires io.helidon.nima.http2.webclient; +} \ No newline at end of file diff --git a/nima/tests/integration/http2/server/src/test/resources/logging-test.properties b/nima/tests/integration/http2/server/src/test/resources/logging-test.properties index 1a57678ee5e..a15b86667a7 100644 --- a/nima/tests/integration/http2/server/src/test/resources/logging-test.properties +++ b/nima/tests/integration/http2/server/src/test/resources/logging-test.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 Oracle and/or its affiliates. +# Copyright (c) 2022, 2023 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,7 +15,11 @@ # handlers=java.util.logging.ConsoleHandler java.util.logging.ConsoleHandler.level=FINEST -java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS %5$s%6$s%n +#java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n # Global logging level. Can be overridden by specific loggers +io.helidon.nima.http2.WindowSizeImpl.level=ALL +io.helidon.nima.http2.FlowControl.level=ALL .level=INFO io.helidon.nima.level=INFO From c1aa6fd92443752885c0bbf6392bc3207e3e1fbd Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Sat, 1 Apr 2023 14:38:03 +0200 Subject: [PATCH 05/17] HTTP/2 Client Flow-control - tests --- dependencies/pom.xml | 6 + .../nima/http/media/ReadableEntityBase.java | 4 +- nima/http2/http2/pom.xml | 10 + .../nima/http2/ConnectionFlowControl.java | 6 +- .../helidon/nima/http2/FlowControlImpl.java | 17 +- .../io/helidon/nima/http2/Http2FrameData.java | 2 +- .../io/helidon/nima/http2/WindowSizeImpl.java | 24 +- .../nima/http2/MaxFrameSizeSplitTest.java | 132 +++++++++++ .../test/resources/logging-test.properties | 23 ++ .../webclient/Http2ClientConnection.java | 26 ++- .../Http2ClientConnectionHandler.java | 4 +- .../http2/webclient/Http2ClientStream.java | 3 - nima/tests/integration/http2/client/pom.xml | 5 + .../http2/client/ClientFlowControlTest.java | 219 ++++++++++++++++++ 14 files changed, 444 insertions(+), 37 deletions(-) create mode 100644 nima/http2/http2/src/test/java/io/helidon/nima/http2/MaxFrameSizeSplitTest.java create mode 100644 nima/http2/http2/src/test/resources/logging-test.properties create mode 100644 nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java diff --git a/dependencies/pom.xml b/dependencies/pom.xml index 76725507d8a..7064f1619ab 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -146,6 +146,7 @@ 2.0 1.4.2 2.0.4 + 4.3.7 5.0.SP3 5.1.0.Final 2.0.4 @@ -1385,6 +1386,11 @@ mssql-jdbc ${version.lib.mssql-jdbc} + + io.vertx + vertx-core + ${version.lib.vertx-core} + diff --git a/nima/http/media/media/src/main/java/io/helidon/nima/http/media/ReadableEntityBase.java b/nima/http/media/media/src/main/java/io/helidon/nima/http/media/ReadableEntityBase.java index 211482f1071..6460f4bb2c1 100644 --- a/nima/http/media/media/src/main/java/io/helidon/nima/http/media/ReadableEntityBase.java +++ b/nima/http/media/media/src/main/java/io/helidon/nima/http/media/ReadableEntityBase.java @@ -226,7 +226,7 @@ public int read() throws IOException { return -1; } ensureBuffer(512); - if (currentBuffer == null) { + if (finished || currentBuffer == null) { return -1; } return currentBuffer.read(); @@ -238,7 +238,7 @@ public int read(byte[] b, int off, int len) throws IOException { return -1; } ensureBuffer(len); - if (currentBuffer == null) { + if (finished || currentBuffer == null) { return -1; } return currentBuffer.read(b, off, len); diff --git a/nima/http2/http2/pom.xml b/nima/http2/http2/pom.xml index 9db9c531a8c..02ef0700952 100644 --- a/nima/http2/http2/pom.xml +++ b/nima/http2/http2/pom.xml @@ -46,6 +46,16 @@ hamcrest-all test + + org.junit.jupiter + junit-jupiter-params + test + + + io.helidon.logging + helidon-logging-jul + test + diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java index 9f578b4c8db..610d891fd66 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java @@ -18,14 +18,14 @@ import java.util.function.BiConsumer; -import static java.lang.System.Logger.Level.INFO; +import static java.lang.System.Logger.Level.DEBUG; /** * HTTP/2 Flow control for connection. */ public class ConnectionFlowControl { - private static final System.Logger LOGGER = System.getLogger(FlowControl.class.getName()); + private static final System.Logger LOGGER_OUTBOUND = System.getLogger(FlowControl.class.getName() + ".ofc"); private final Type type; private final BiConsumer windowUpdateWriter; @@ -114,7 +114,7 @@ public void resetMaxFrameSize(int maxFrameSize) { * @param initialWindowSize INIT_WINDOW_SIZE received */ public void resetInitialWindowSize(int initialWindowSize) { - LOGGER.log(INFO, () -> String.format("%s OFC STR *: Recv INIT_WINDOW_SIZE %s", type, initialWindowSize)); + LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR *: Recv INIT_WINDOW_SIZE %s", type, initialWindowSize)); this.initialWindowSize = initialWindowSize; } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java index db267de459f..d36707a8b39 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java @@ -22,7 +22,8 @@ abstract class FlowControlImpl implements FlowControl { - private static final System.Logger LOGGER = System.getLogger(FlowControl.class.getName()); + private static final System.Logger LOGGER_INBOUND = System.getLogger(FlowControl.class.getName() + ".ifc"); + private static final System.Logger LOGGER_OUTBOUND = System.getLogger(FlowControl.class.getName() + ".ofc"); private final int streamId; @@ -102,17 +103,17 @@ WindowSize streamWindowSize() { @Override public void decrementWindowSize(int decrement) { long strRemaining = streamWindowSize().decrementWindowSize(decrement); - LOGGER.log(DEBUG, () -> String.format("%s IFC STR %d: -%d(%d)", type, streamId(), decrement, strRemaining)); + LOGGER_INBOUND.log(DEBUG, () -> String.format("%s IFC STR %d: -%d(%d)", type, streamId(), decrement, strRemaining)); long connRemaining = connectionWindowSize().decrementWindowSize(decrement); - LOGGER.log(DEBUG, () -> String.format("%s IFC STR 0: -%d(%d)", type, decrement, connRemaining)); + LOGGER_INBOUND.log(DEBUG, () -> String.format("%s IFC STR 0: -%d(%d)", type, decrement, connRemaining)); } @Override public void incrementWindowSize(int increment) { long strRemaining = streamWindowSize.incrementWindowSize(increment); - LOGGER.log(DEBUG, () -> String.format("%s IFC STR %d: +%d(%d)", type, streamId(), increment, strRemaining)); + LOGGER_INBOUND.log(DEBUG, () -> String.format("%s IFC STR %d: +%d(%d)", type, streamId(), increment, strRemaining)); long conRemaining = connectionWindowSize.incrementWindowSize(increment); - LOGGER.log(DEBUG, () -> String.format("%s IFC STR 0: +%d(%d)", type, increment, conRemaining)); + LOGGER_INBOUND.log(DEBUG, () -> String.format("%s IFC STR 0: +%d(%d)", type, increment, conRemaining)); } } @@ -144,16 +145,16 @@ WindowSize streamWindowSize() { public void decrementWindowSize(int decrement) { long strRemaining = streamWindowSize().decrementWindowSize(decrement); - LOGGER.log(DEBUG, () -> String.format("%s OFC STR %d: -%d(%d)", type, streamId(), decrement, strRemaining)); + LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR %d: -%d(%d)", type, streamId(), decrement, strRemaining)); long connRemaining = connectionWindowSize().decrementWindowSize(decrement); - LOGGER.log(DEBUG, () -> String.format("%s OFC STR 0: -%d(%d)", type, decrement, connRemaining)); + LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR 0: -%d(%d)", type, decrement, connRemaining)); } @Override public long incrementStreamWindowSize(int increment) { long remaining = streamWindowSize.incrementWindowSize(increment); - LOGGER.log(DEBUG, () -> String.format("%s OFC STR %d: +%d(%d)", type, streamId(), increment, remaining)); + LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR %d: +%d(%d)", type, streamId(), increment, remaining)); connectionFlowControl.outbound().triggerUpdate(); return remaining; } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2FrameData.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2FrameData.java index 1ff653c3738..6cac83d87d3 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2FrameData.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2FrameData.java @@ -57,7 +57,7 @@ public Http2FrameData[] split(int size) { for (int i = 0; i < allFrames; i++) { boolean lastFrame = allFrames == i + 1; // only last frame can be smaller than max size - byte[] data = new byte[lastFrame ? lastFrameSize : size]; + byte[] data = new byte[lastFrame ? (lastFrameSize != 0 ? lastFrameSize : size) : size]; this.data().read(data); BufferData bufferData = BufferData.create(data); splitFrames[i] = new Http2FrameData( diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java index 6018b878245..b55e7ddce1a 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java @@ -24,14 +24,14 @@ import java.util.function.BiConsumer; import static java.lang.System.Logger.Level.DEBUG; -import static java.lang.System.Logger.Level.INFO; /** * Window size container, used with {@link io.helidon.nima.http2.FlowControl}. */ abstract class WindowSizeImpl implements WindowSize { - private static final System.Logger LOGGER = System.getLogger(FlowControl.class.getName()); + private static final System.Logger LOGGER_INBOUND = System.getLogger(FlowControl.class.getName() + ".ifc"); + private static final System.Logger LOGGER_OUTBOUND = System.getLogger(FlowControl.class.getName() + ".ofc"); private final ConnectionFlowControl.Type type; private final int streamId; @@ -52,8 +52,8 @@ public void resetWindowSize(int size) { // it maintains by the difference between the new value and the old value remainingWindowSize.updateAndGet(o -> o + size - windowSize); windowSize = size; - LOGGER.log(DEBUG, () -> String.format("%s OFC STR %d: Recv INITIAL_WINDOW_SIZE %d(%d)", - type, streamId, windowSize, remainingWindowSize.get())); + LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR %d: Recv INITIAL_WINDOW_SIZE %d(%d)", + type, streamId, windowSize, remainingWindowSize.get())); } @Override @@ -135,6 +135,7 @@ static final class Outbound extends WindowSizeImpl implements WindowSize.Outboun @Override public long incrementWindowSize(int increment) { long remaining = super.incrementWindowSize(increment); + LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR %d: +%d(%d)", type, streamId, increment, remaining)); triggerUpdate(); return remaining; } @@ -151,7 +152,8 @@ public void blockTillUpdate() { //TODO configurable timeout updated.get().get(500, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { - LOGGER.log(DEBUG, () -> String.format("%s OFC STR %d: Window depleted, waiting for update.", type, streamId)); + LOGGER_OUTBOUND.log(DEBUG, () -> + String.format("%s OFC STR %d: Window depleted, waiting for update.", type, streamId)); } } } @@ -223,7 +225,7 @@ Context context() { return context; } - int streamId(){ + int streamId() { return this.streamId; } @@ -283,7 +285,7 @@ private Simple(Context context, int streamId, BiConsumer String.format("%s IFC STR %d: Send WINDOW_UPDATE %s", type, streamId, increment)); + LOGGER_INBOUND.log(DEBUG, () -> String.format("%s IFC STR %d: Send WINDOW_UPDATE %s", type, streamId, increment)); windowUpdateWriter().accept(streamId(), new Http2WindowUpdate(increment)); } @@ -307,12 +309,12 @@ private Bisection(Context context, int streamId, BiConsumer String.format("%s IFC STR %d: Deferred WINDOW_UPDATE %d, total %d, watermark %d", - type, streamId, increment, delayedIncrement, watermark)); + LOGGER_INBOUND.log(DEBUG, () -> String.format("%s IFC STR %d: Deferred WINDOW_UPDATE %d, total %d, watermark %d", + type, streamId, increment, delayedIncrement, watermark)); delayedIncrement += increment; if (delayedIncrement > watermark) { - LOGGER.log(DEBUG, () -> String.format("%s IFC STR %d: Send WINDOW_UPDATE %d, watermark %d", - type, streamId, delayedIncrement, watermark)); + LOGGER_INBOUND.log(DEBUG, () -> String.format("%s IFC STR %d: Send WINDOW_UPDATE %d, watermark %d", + type, streamId, delayedIncrement, watermark)); windowUpdateWriter().accept(streamId(), new Http2WindowUpdate(delayedIncrement)); delayedIncrement = 0; } diff --git a/nima/http2/http2/src/test/java/io/helidon/nima/http2/MaxFrameSizeSplitTest.java b/nima/http2/http2/src/test/java/io/helidon/nima/http2/MaxFrameSizeSplitTest.java new file mode 100644 index 00000000000..6b07d46648b --- /dev/null +++ b/nima/http2/http2/src/test/java/io/helidon/nima/http2/MaxFrameSizeSplitTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.http2; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.stream.Stream; + +import io.helidon.common.buffers.BufferData; +import io.helidon.logging.common.LogConfig; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static java.lang.System.Logger.Level.DEBUG; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class MaxFrameSizeSplitTest { + + private static final System.Logger LOGGER = System.getLogger(MaxFrameSizeSplitTest.class.getName()); + + private static final String TEST_STRING = "Helidon data!!!!"; + private static final byte[] TEST_DATA = TEST_STRING.getBytes(StandardCharsets.UTF_8); + + @BeforeAll + static void beforeAll() { + LogConfig.configureRuntime(); + } + + private static Stream splitMultiple() { + return Stream.of( + Arguments.of(17, 1, 16), + Arguments.of(16, 1, 16), + Arguments.of(15, 2, 1), + Arguments.of(14, 2, 2), + Arguments.of(13, 2, 3), + Arguments.of(12, 2, 4), + Arguments.of(11, 2, 5), + Arguments.of(10, 2, 6), + Arguments.of(9, 2, 7), + Arguments.of(8, 2, 8), + Arguments.of(7, 3, 2), + Arguments.of(6, 3, 4), + Arguments.of(5, 4, 1), + Arguments.of(4, 4, 4), + Arguments.of(3, 6, 1), + Arguments.of(2, 8, 2), + Arguments.of(1, 16, 1) + ); + } + + @ParameterizedTest + @MethodSource + void splitMultiple(int sizeOfFrames, + int numberOfFrames, + int sizeOfLastFrame) { + LOGGER.log(DEBUG, "Splitting " + Arrays.toString(TEST_DATA) + " to frames of max size " + sizeOfFrames); + + Http2FrameData frameData = createFrameData(TEST_DATA); + Http2FrameData[] split = frameData.split(sizeOfFrames); + assertThat("Unexpected number of frames", split.length, is(numberOfFrames)); + + BufferData joined = Stream.of(split) + .collect(() -> BufferData.create(TEST_DATA.length), + (bb, b) -> bb.write(b.data()), + (bb, bb2) -> { + }); + + assertThat("Result after split and join differs", + joined.readString(joined.available(), StandardCharsets.UTF_8), + is(TEST_STRING)); + + // Reload data depleted by previous test + split = createFrameData(TEST_DATA).split(sizeOfFrames); + + for (int i = 0; i < numberOfFrames - 1; i++) { + Http2FrameData frame = split[i]; + assertThat("Only last frame can have endOfStream flag", + frame.header().flags(Http2FrameTypes.DATA).endOfStream(), + is(false)); + + byte[] bytes = toBytes(frame); + LOGGER.log(DEBUG, i + ". frame: " + Arrays.toString(bytes)); + assertThat("Unexpected size of frame " + i, bytes.length, is(sizeOfFrames)); + } + + Http2FrameData lastFrame = split[numberOfFrames - 1]; + assertThat("Last frame is missing endOfStream flag", + lastFrame.header().flags(Http2FrameTypes.DATA).endOfStream(), + is(true)); + + byte[] bytes = toBytes(lastFrame); + LOGGER.log(DEBUG, numberOfFrames - 1 + ". frame: " + Arrays.toString(bytes)); + assertThat("Unexpected size of the last frame", bytes.length, is(sizeOfLastFrame)); + } + + private Http2FrameData createFrameData(byte[] data) { + Http2FrameHeader http2FrameHeader = Http2FrameHeader.create(data.length, + Http2FrameTypes.DATA, + Http2Flag.DataFlags.create(Http2Flag.DataFlags.END_OF_STREAM), + 1); + return new Http2FrameData(http2FrameHeader, BufferData.create(data)); + } + + private byte[] toBytes(Http2FrameData frameData) { + return toBytes(frameData.data()); + } + + private byte[] toBytes(BufferData data) { + byte[] b = new byte[data.available()]; + data.read(b); + return b; + } +} diff --git a/nima/http2/http2/src/test/resources/logging-test.properties b/nima/http2/http2/src/test/resources/logging-test.properties new file mode 100644 index 00000000000..12ca00d657e --- /dev/null +++ b/nima/http2/http2/src/test/resources/logging-test.properties @@ -0,0 +1,23 @@ +# +# Copyright (c) 2023 Oracle and/or its affiliates. +# +# Licensed 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. +# +handlers=io.helidon.logging.jul.HelidonConsoleHandler +# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n +#java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +# Global logging level. Can be overridden by specific loggers +.level=INFO +io.helidon.nima.level=INFO +#io.helidon.nima.http2.MaxFrameSizeSplitTest.level=ALL diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java index 50eea0ab48e..8b2deecb1b9 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java @@ -33,6 +33,7 @@ import java.util.Queue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; +import java.util.concurrent.Semaphore; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -101,6 +102,7 @@ class Http2ClientConnection { private final Map> buffer = new HashMap<>(); private final Map streams = new HashMap<>(); private final Lock connectionLock = new ReentrantLock(); + private final Semaphore dequeSemaphore = new Semaphore(1); private final ConnectionFlowControl connectionFlowControl; private Http2Settings serverSettings = Http2Settings.builder() @@ -156,10 +158,12 @@ private Queue buffer(int streamId) { Http2FrameData readNextFrame(int streamId) { try { - // Don't let streams to steal frame parts - // Always read whole frame(frameHeader+data) at once + // Block deque thread when queue is empty + dequeSemaphore.acquire(); connectionLock.lock(); return buffer(streamId).poll(); + } catch (InterruptedException e) { + throw new RuntimeException(e); } finally { connectionLock.unlock(); } @@ -269,7 +273,7 @@ private void handle() { break; case DATA: - connectionFlowControl.decrementInboundConnectionWindowSize(frameHeader.length()); + stream(streamId).flowControl().inbound().decrementWindowSize(frameHeader.length()); enqueue(streamId, new Http2FrameData(frameHeader, data)); break; @@ -277,14 +281,21 @@ private void handle() { enqueue(streamId, new Http2FrameData(frameHeader, data)); return; + case CONTINUATION: + //FIXME: Header continuation + throw new UnsupportedOperationException("Continuation support is not implemented yet!"); + default: - //FIXME: other frame types LOGGER.log(WARNING, "Unsupported frame type!! " + frameHeader.type()); } } - Http2ClientStream stream(int priority) { + Http2ClientStream stream(int streamId) { + return streams.get(streamId); + } + + Http2ClientStream createStream(int priority) { //FIXME: priority return new Http2ClientStream(this, serverSettings, @@ -298,7 +309,7 @@ void addStream(int streamId, Http2ClientStream stream){ Http2ClientStream tryStream(int priority) { try { - return stream(priority); + return createStream(priority); } catch (IllegalStateException | UncheckedIOException e) { return null; } @@ -312,13 +323,14 @@ void close() { e.printStackTrace(); } } - private void enqueue(int streamId, Http2FrameData frameData){ try { connectionLock.lock(); buffer(streamId).add(frameData); } finally { connectionLock.unlock(); + // Release deque threads + dequeSemaphore.release(); } } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnectionHandler.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnectionHandler.java index 4a19e808596..23769684231 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnectionHandler.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnectionHandler.java @@ -60,12 +60,12 @@ public Http2ClientStream newStream(boolean priorKnowledge, int priority) { Http2ClientStream stream; if (conn == null) { conn = createConnection(connectionKey, priorKnowledge); - stream = conn.stream(priority); + stream = conn.createStream(priority); } else { stream = conn.tryStream(priority); if (stream == null) { conn = createConnection(connectionKey, priorKnowledge); - stream = conn.stream(priority); + stream = conn.createStream(priority); } } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java index 96db8b89429..61bcfd199e8 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java @@ -124,9 +124,6 @@ Http2FrameData readOne() { frameData); this.headers(http2Headers, endOfStream); break; - case RST_STREAM: - this.rstStream(Http2RstStream.create(frameData.data())); - break; default: LOGGER.log(System.Logger.Level.DEBUG, "Dropping frame " + frameData.header() + " expected header or data."); } diff --git a/nima/tests/integration/http2/client/pom.xml b/nima/tests/integration/http2/client/pom.xml index f1ad9871b87..13302101d9f 100644 --- a/nima/tests/integration/http2/client/pom.xml +++ b/nima/tests/integration/http2/client/pom.xml @@ -51,5 +51,10 @@ hamcrest-all test + + io.vertx + vertx-core + test + diff --git a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java new file mode 100644 index 00000000000..b5cd92c0e83 --- /dev/null +++ b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.tests.integration.http2.client; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicLong; + +import io.helidon.common.http.Http; +import io.helidon.logging.common.LogConfig; +import io.helidon.nima.http2.WindowSize; +import io.helidon.nima.http2.webclient.Http2; +import io.helidon.nima.http2.webclient.Http2ClientResponse; +import io.helidon.nima.webclient.WebClient; + +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerRequest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class ClientFlowControlTest { + + private static final System.Logger LOGGER = System.getLogger(ClientFlowControlTest.class.getName()); + private static final Duration TIMEOUT = Duration.ofSeconds(10); + private static final String DATA_10_K = "Helidon!!!".repeat(1_000); + private static final byte[] BYTES_10_K = DATA_10_K.getBytes(StandardCharsets.UTF_8); + private static final String DATA_10_K_UP_CASE = DATA_10_K.toUpperCase(); + private static final byte[] BYTES_10_K_UP_CASE = DATA_10_K_UP_CASE.getBytes(StandardCharsets.UTF_8); + private static final String EXPECTED = DATA_10_K.repeat(5) + DATA_10_K_UP_CASE.repeat(5); + private static final AtomicLong inboundServerSentData = new AtomicLong(); + private static final Vertx vertx = Vertx.vertx(); + private static final ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor(); + private static HttpServer server; + private static CompletableFuture outboundTestServerRequestRef = new CompletableFuture<>(); + + @BeforeAll + static void beforeAll() throws ExecutionException, InterruptedException, TimeoutException { + LogConfig.configureRuntime(); + server = vertx.createHttpServer() + .requestHandler(req -> { + switch (req.path()) { + case "/in" -> { + for (int i = 0; i < 5; i++) { + req.response().write(DATA_10_K) + .andThen(event -> LOGGER.log(System.Logger.Level.DEBUG, "Vertx server sent " + + inboundServerSentData.addAndGet(BYTES_10_K.length))); + } + for (int i = 5; i < 10; i++) { + req.response().write(DATA_10_K.toUpperCase()) + .andThen(event -> LOGGER.log(System.Logger.Level.DEBUG, "Vertx server sent " + + inboundServerSentData.addAndGet(BYTES_10_K_UP_CASE.length))); + } + req.end(); + } + case "/out" -> { + req.pause(); + outboundTestServerRequestRef.complete(req); + req.handler(e -> { + req.response().write(e); + }).endHandler(event -> req.response().end()); + } + } + }) + .listen(-1) + .toCompletionStage() + .toCompletableFuture() + .get(TIMEOUT.toMillis(), MILLISECONDS); + } + + @Test + void clientOutbound() throws InterruptedException, ExecutionException, TimeoutException { + outboundTestServerRequestRef = new CompletableFuture<>(); + + //Chunk of the frame size * 2 so frame splitting is tested too + int chunkSize = WindowSize.DEFAULT_MAX_FRAME_SIZE * 2; + + AtomicLong clientSentData = new AtomicLong(); + + ByteArrayInputStream baos = new ByteArrayInputStream(EXPECTED.getBytes()); + CompletableFuture clientFuture = CompletableFuture.supplyAsync(() -> { + try (Http2ClientResponse res = WebClient.builder(Http2.PROTOCOL) + .baseUri("http://localhost:" + server.actualPort() + "/") + .build() + .method(Http.Method.PUT) + .path("/out") + .priorKnowledge(true) + .outputStream(out -> { + while (baos.available() > 0) { + byte[] chunk = baos.readNBytes(chunkSize); + clientSentData.addAndGet(chunk.length); + out.write(chunk); + } + out.close(); + })) { + return res.as(String.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + }, exec); + HttpServerRequest req = outboundTestServerRequestRef.get(TIMEOUT.toMillis(), MILLISECONDS); + req.fetch(1); + awaitSize("Two client chunks should fit to default window size", clientSentData, chunkSize * 2L); + // Vertx translates frames to chunks of default MAX_FRAME_SIZE 16384 + // 4 * 16384 depletes Vertx's windows size and force it to send update + req.fetch(3); + awaitSize("Three client chunks should have been sent now", clientSentData, chunkSize * 3L); + req.fetch(Long.MAX_VALUE); + awaitSize("Three client chunks should have been sent now", + clientSentData, + EXPECTED.getBytes(StandardCharsets.UTF_8).length); + assertThat("Echo endpoint should have returned exactly same data", + clientFuture.get(TIMEOUT.toMillis(), MILLISECONDS), + is(EXPECTED)); + } + + @Test + void clientInbound() throws InterruptedException { + + AtomicLong receivedByteSize = new AtomicLong(); + try (Http2ClientResponse res = WebClient.builder(Http2.PROTOCOL) + .baseUri("http://localhost:" + server.actualPort() + "/") + .build() + .method(Http.Method.GET) + .path("/in") + .priorKnowledge(true) + .request()) { + + final ByteBuffer bb = ByteBuffer.allocate(EXPECTED.getBytes().length); + final InputStream is = res.inputStream(); + + // Accept only 10k out of initial win size (64k) + Semaphore semaphore = new Semaphore(10_000); + CompletableFuture.runAsync(() -> { + try { + int b = -1; + for (int i = 0; i < bb.capacity(); i++) { + semaphore.acquire(); + b = is.read(); + if (b == -1) { + break; + } + receivedByteSize.incrementAndGet(); + bb.put((byte) b); + } + is.close(); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + }, exec); + + awaitSize("Server should have accepted 70k, one 10k chunk over 64k(initial win size)", + inboundServerSentData, 70_000); + // Give it time to make a mistake and send more if FC doesn't work correctly + Thread.sleep(300); + assertThat("", inboundServerSentData.get(), is(70_000L)); + + // Unblock and accept the rest of the data + semaphore.release(Integer.MAX_VALUE); + awaitSize("Rest of the data 100k should have been received", receivedByteSize, EXPECTED.getBytes().length); + + String result = new String(bb.array(), StandardCharsets.UTF_8); + assertThat("Echo endpoint should have returned exactly same data", result, is(EXPECTED)); + } + } + + private void awaitSize(String msg, AtomicLong actualSize, long expected) + throws InterruptedException { + + for (int i = 10; i < 5000 && actualSize.get() < expected; i *= 10) { + Thread.sleep(i); + } + assertThat(msg, actualSize.get(), is(expected)); + } + + @AfterAll + static void afterAll() { + server.close(); + vertx.close(); + exec.shutdown(); + try { + if (!exec.awaitTermination(TIMEOUT.toMillis(), MILLISECONDS)) { + exec.shutdownNow(); + } + } catch (InterruptedException e) { + exec.shutdownNow(); + } + } +} From 94d2e3ce9d91cb0c1fb903a31a65143e68decd34 Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Sun, 2 Apr 2023 14:36:15 +0200 Subject: [PATCH 06/17] HTTP/2 Client Continuation --- .../helidon/nima/http2/Http2StreamState.java | 6 +- .../http2/webclient/ClientRequestImpl.java | 65 ++++++--- .../http2/webclient/ConnectionContext.java | 29 ++++ .../nima/http2/webclient/Http2Client.java | 76 +++++++++- .../webclient/Http2ClientConnection.java | 82 ++++------- .../Http2ClientConnectionHandler.java | 16 +-- .../nima/http2/webclient/Http2ClientImpl.java | 34 ++++- .../http2/webclient/Http2ClientRequest.java | 21 ++- .../http2/webclient/Http2ClientStream.java | 77 +++++++--- .../nima/http2/webclient/StreamBuffer.java | 68 +++++++++ .../webclient/StreamTimeoutException.java | 28 ++++ .../http2/client/ClientFlowControlTest.java | 15 +- .../integration/http2/client/GetTest.java | 5 +- .../integration/http2/client/HeadersTest.java | 135 ++++++++++++++++++ .../http2/client/Http2ClientTest.java | 3 +- .../integration/http2/client/PostTest.java | 2 +- .../http2/webserver/FlowControlTest.java | 9 +- .../http2/webserver/Http2ServerTest.java | 9 +- .../test/resources/logging-test.properties | 7 +- 19 files changed, 550 insertions(+), 137 deletions(-) create mode 100644 nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionContext.java create mode 100644 nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/StreamBuffer.java create mode 100644 nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/StreamTimeoutException.java create mode 100644 nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/HeadersTest.java diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamState.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamState.java index f1c9291d2de..e53858d3720 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamState.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamState.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -270,6 +270,10 @@ private static Http2StreamState checkHeaders(Http2StreamState current, return current; //receiving headers in progress } } + // 5.1. half-closed (local): An endpoint can receive any type of frame in this state + if (current == HALF_CLOSED_LOCAL) { + return HALF_CLOSED_LOCAL; + } throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Received " + type + " in invalid state: " + current); } } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java index 594afa140f5..aa5a8bcc344 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java @@ -20,6 +20,7 @@ import java.io.UncheckedIOException; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; @@ -45,7 +46,6 @@ class ClientRequestImpl implements Http2ClientRequest { static final HeaderValue USER_AGENT_HEADER = Header.create(Header.USER_AGENT, "Helidon Nima " + Version.VERSION); //todo Gracefully close connections in channel cache private static final Map CHANNEL_CACHE = new ConcurrentHashMap<>(); - private WritableHeaders explicitHeaders = WritableHeaders.create(); private final Http2ClientImpl client; @@ -57,20 +57,29 @@ class ClientRequestImpl implements Http2ClientRequest { private Tls tls; private int priority; private boolean priorKnowledge; + private long initialWindowSize; + private int maxFrameSize; + private long maxHeaderListSize; + private int connectionPrefetch; + private int requestPrefetch = 0; private ClientConnection explicitConnection; + private Duration timeout = Duration.ofSeconds(10); ClientRequestImpl(Http2ClientImpl client, ExecutorService executor, Http.Method method, UriHelper helper, - boolean priorKnowledge, Tls tls, UriQueryWriteable query) { this.client = client; this.executor = executor; this.method = method; this.uri = helper; - this.priorKnowledge = priorKnowledge; + this.priorKnowledge = client.priorKnowledge(); + this.initialWindowSize = client.initialWindowSize(); + this.maxFrameSize = client.maxFrameSize(); + this.maxHeaderListSize = client.maxHeaderListSize(); + this.connectionPrefetch = client.prefetch(); this.tls = tls == null || !tls.enabled() ? null : tls; this.query = query; } @@ -134,6 +143,9 @@ public Http2ClientResponse submit(Object entity) { Http2Headers http2Headers = prepareHeaders(headers); stream.write(http2Headers, entityBytes.length == 0); + + stream.flowControl().inbound().incrementWindowSize(requestPrefetch); + if (entityBytes.length != 0) { stream.writeData(BufferData.create(entityBytes), true); } @@ -194,6 +206,18 @@ public Http2ClientRequest priorKnowledge(boolean priorKnowledge) { return this; } + @Override + public Http2ClientRequest requestPrefetch(int requestPrefetch) { + this.requestPrefetch = requestPrefetch; + return this; + } + + @Override + public Http2ClientRequest timeout(Duration timeout) { + this.timeout = timeout; + return this; + } + UriHelper uriHelper() { return uri; } @@ -236,26 +260,33 @@ private Http2ClientStream reserveStream() { } } - private Http2ClientStream newStream(UriHelper uri){ + private Http2ClientStream newStream(UriHelper uri) { try { ConnectionKey connectionKey = new ConnectionKey(method, - uri.scheme(), - uri.host(), - uri.port(), - priorKnowledge, - tls, - client.dnsResolver(), - client.dnsAddressLookup()); + uri.scheme(), + uri.host(), + uri.port(), + priorKnowledge, + tls, + client.dnsResolver(), + client.dnsAddressLookup()); // this statement locks all threads - must not do anything complicated (just create a new instance) return CHANNEL_CACHE.computeIfAbsent(connectionKey, - key -> new Http2ClientConnectionHandler(executor, - SocketOptions.builder().build(), - uri.path(), - key)) + key -> new Http2ClientConnectionHandler(executor, + SocketOptions.builder().build(), + uri.path(), + key)) // this statement may block a single connection key - .newStream(priorKnowledge, priority); - } catch (UpgradeRedirectException e){ + .newStream(new ConnectionContext(priority, + priorKnowledge, + initialWindowSize, + maxFrameSize, + maxHeaderListSize, + connectionPrefetch, + requestPrefetch, + timeout)); + } catch (UpgradeRedirectException e) { return newStream(UriHelper.create(URI.create(e.redirectUri()), UriQueryWriteable.create())); } } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionContext.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionContext.java new file mode 100644 index 00000000000..dbad3f0124a --- /dev/null +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionContext.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.http2.webclient; + +import java.time.Duration; + +record ConnectionContext(int priority, + boolean priorKnowledge, + long initialWindowSize, + int maxFrameSize, + long maxHeaderListSize, + int connectionPrefetch, + int requestPrefetch, + Duration timeout) { +} diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2Client.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2Client.java index 0f1a8de6517..aedf09a9bbb 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2Client.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2Client.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package io.helidon.nima.http2.webclient; +import io.helidon.nima.http2.WindowSize; import io.helidon.nima.webclient.HttpClient; import io.helidon.nima.webclient.WebClient; @@ -39,6 +40,10 @@ static Http2ClientBuilder builder() { class Http2ClientBuilder extends WebClient.Builder { private boolean priorKnowledge; + private int maxFrameSize = WindowSize.DEFAULT_MAX_FRAME_SIZE; + private long maxHeaderListSize = -1; + private long initialWindowSize = WindowSize.DEFAULT_WIN_SIZE; + private int prefetch = 33554432; private Http2ClientBuilder() { } @@ -65,6 +70,59 @@ public Http2ClientBuilder priorKnowledge(boolean priorKnowledge) { return this; } + /** + * Configure initial MAX_FRAME_SIZE setting for new HTTP/2 connections. + * Maximum size of data frames in bytes the client is prepared to accept from the server. + * Default value is 2^14(16_384). + * + * @param maxFrameSize data frame size in bytes between 2^14(16_384) and 2^24-1(16_777_215) + * @return updated client + */ + public Http2ClientBuilder maxFrameSize(int maxFrameSize) { + if (maxFrameSize < WindowSize.DEFAULT_MAX_FRAME_SIZE || maxFrameSize > WindowSize.MAX_MAX_FRAME_SIZE) { + throw new IllegalArgumentException( + "Max frame size needs to be a number between 2^14(16_384) and 2^24-1(16_777_215)" + ); + } + this.maxFrameSize = maxFrameSize; + return this; + } + + /** + * Configure initial MAX_HEADER_LIST_SIZE setting for new HTTP/2 connections. + * Sends to the server the maximum header field section size client is prepared to accept. + * + * @param maxHeaderListSize units of octets + * @return updated client + */ + public Http2ClientBuilder maxHeaderListSize(long maxHeaderListSize){ + this.maxHeaderListSize = maxHeaderListSize; + return this; + } + + /** + * Configure INITIAL_WINDOW_SIZE setting for new HTTP/2 connections. + * Sends to the server the size of the largest frame payload client is willing to receive. + * + * @param initialWindowSize units of octets + * @return updated client + */ + public Http2ClientBuilder initialWindowSize(int initialWindowSize){ + this.initialWindowSize = initialWindowSize; + return this; + } + + /** + * First connection window update increment sent right after the connection is established. + * + * @param prefetch number of bytes the client is prepared to receive as data from all the streams combined + * @return updated client + */ + public Http2ClientBuilder prefetch(int prefetch) { + this.prefetch = prefetch; + return this; + } + @Override public Http2Client build() { return new Http2ClientImpl(this); @@ -73,5 +131,21 @@ public Http2Client build() { boolean priorKnowledge() { return priorKnowledge; } + + long maxHeaderListSize() { + return maxHeaderListSize; + } + + long initialWindowSize() { + return initialWindowSize; + } + + int prefetch() { + return prefetch; + } + + int maxFrameSize() { + return this.maxFrameSize; + } } } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java index 8b2deecb1b9..2720f35ec7a 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java @@ -24,18 +24,13 @@ import java.nio.charset.StandardCharsets; import java.security.cert.Certificate; import java.security.cert.X509Certificate; -import java.util.ArrayDeque; import java.util.Base64; import java.util.HashMap; import java.util.HexFormat; import java.util.List; import java.util.Map; -import java.util.Queue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -import java.util.concurrent.Semaphore; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; @@ -97,13 +92,10 @@ class Http2ClientConnection { private final SocketOptions socketOptions; private final ConnectionKey connectionKey; private final String primaryPath; - private final boolean priorKnowledge; private final LockingStreamIdSequence streamIdSeq = new LockingStreamIdSequence(); - private final Map> buffer = new HashMap<>(); private final Map streams = new HashMap<>(); - private final Lock connectionLock = new ReentrantLock(); - private final Semaphore dequeSemaphore = new Semaphore(1); private final ConnectionFlowControl connectionFlowControl; + private final ConnectionContext connectionContext; private Http2Settings serverSettings = Http2Settings.builder() .build(); @@ -122,16 +114,16 @@ class Http2ClientConnection { SocketOptions socketOptions, ConnectionKey connectionKey, String primaryPath, - boolean priorKnowledge) { + ConnectionContext connectionContext) { this.executor = executor; this.socketOptions = socketOptions; this.connectionKey = connectionKey; this.primaryPath = primaryPath; - this.priorKnowledge = priorKnowledge; + this.connectionContext = connectionContext; this.connectionFlowControl = ConnectionFlowControl.createClient(this::writeWindowsUpdate); } - private void writeWindowsUpdate(int streamId, Http2WindowUpdate windowUpdateFrame) { + void writeWindowsUpdate(int streamId, Http2WindowUpdate windowUpdateFrame) { writer.write(windowUpdateFrame.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create())); } @@ -152,23 +144,6 @@ Http2ClientConnection connect() { return this; } - private Queue buffer(int streamId) { - return buffer.computeIfAbsent(streamId, i -> new ArrayDeque<>()); - } - - Http2FrameData readNextFrame(int streamId) { - try { - // Block deque thread when queue is empty - dequeSemaphore.acquire(); - connectionLock.lock(); - return buffer(streamId).poll(); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } finally { - connectionLock.unlock(); - } - } - private void handle() { this.reader.ensureAvailable(); BufferData frameHeaderBuffer = this.reader.readBuffer(FRAME_HEADER_LENGTH); @@ -241,7 +216,7 @@ private void handle() { } else { - streams.get(streamId) + stream(streamId) .windowUpdate(windowUpdate); } return; @@ -273,18 +248,15 @@ private void handle() { break; case DATA: - stream(streamId).flowControl().inbound().decrementWindowSize(frameHeader.length()); - enqueue(streamId, new Http2FrameData(frameHeader, data)); + Http2ClientStream stream = stream(streamId); + stream.flowControl().inbound().decrementWindowSize(frameHeader.length()); + stream.push(new Http2FrameData(frameHeader, data)); break; - case HEADERS: - enqueue(streamId, new Http2FrameData(frameHeader, data)); + case HEADERS, CONTINUATION: + stream(streamId).push(new Http2FrameData(frameHeader, data)); return; - case CONTINUATION: - //FIXME: Header continuation - throw new UnsupportedOperationException("Continuation support is not implemented yet!"); - default: LOGGER.log(WARNING, "Unsupported frame type!! " + frameHeader.type()); } @@ -300,6 +272,7 @@ Http2ClientStream createStream(int priority) { return new Http2ClientStream(this, serverSettings, helidonSocket, + connectionContext, streamIdSeq); } @@ -307,6 +280,10 @@ void addStream(int streamId, Http2ClientStream stream){ this.streams.put(streamId, stream); } + void removeStream(int streamId) { + this.streams.remove(streamId); + } + Http2ClientStream tryStream(int priority) { try { return createStream(priority); @@ -323,16 +300,6 @@ void close() { e.printStackTrace(); } } - private void enqueue(int streamId, Http2FrameData frameData){ - try { - connectionLock.lock(); - buffer(streamId).add(frameData); - } finally { - connectionLock.unlock(); - // Release deque threads - dequeSemaphore.release(); - } - } private void doConnect() throws IOException { boolean useTls = "https".equals(connectionKey.scheme()) && connectionKey.tls() != null; @@ -371,7 +338,7 @@ private void doConnect() throws IOException { } } - if (!priorKnowledge && !useTls) { + if (!connectionContext.priorKnowledge() && !useTls) { httpUpgrade(); // Settings are part of the HTTP/1 upgrade request sendPreface(false); @@ -383,7 +350,7 @@ private void doConnect() throws IOException { while (!Thread.interrupted()) { handle(); } - System.out.println("Client listener interrupted!!!"); + LOGGER.log(DEBUG, () -> "Client listener interrupted"); }); } @@ -406,19 +373,24 @@ private void sendPreface(boolean sendSettings){ dataWriter.writeNow(BufferData.create(PRIOR_KNOWLEDGE_PREFACE)); if (sendSettings) { // §3.5 Preface bytes must be followed by setting frame - Http2Settings http2Settings = Http2Settings.builder() - .add(Http2Setting.MAX_HEADER_LIST_SIZE, 8192L) + Http2Settings.Builder b = Http2Settings.builder(); + if (connectionContext.maxHeaderListSize() > 0) { + b.add(Http2Setting.MAX_HEADER_LIST_SIZE, connectionContext.maxHeaderListSize()); + } + Http2Settings http2Settings = b + .add(Http2Setting.INITIAL_WINDOW_SIZE, connectionContext.initialWindowSize()) + .add(Http2Setting.MAX_FRAME_SIZE, (long) connectionContext.maxFrameSize()) .add(Http2Setting.ENABLE_PUSH, false) - // .add(Http2Setting., false) .build(); + Http2Flag.SettingsFlags flags = Http2Flag.SettingsFlags.create(0); Http2FrameData frameData = http2Settings.toFrameData(null, 0, flags); sendListener.frameHeader(helidonSocket, frameData.header()); sendListener.frame(helidonSocket, http2Settings); writer.write(frameData); } - // win update - Http2WindowUpdate windowUpdate = new Http2WindowUpdate(10000); //FIXME: configurable + // First connection window update, with prefetch increment + Http2WindowUpdate windowUpdate = new Http2WindowUpdate(connectionContext.connectionPrefetch()); Http2Flag.NoFlags flags = Http2Flag.NoFlags.create(); Http2FrameData frameData = windowUpdate.toFrameData(null, 0, flags); sendListener.frameHeader(helidonSocket, frameData.header()); diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnectionHandler.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnectionHandler.java index 23769684231..f9ecc29f166 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnectionHandler.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnectionHandler.java @@ -48,7 +48,7 @@ class Http2ClientConnectionHandler { this.connectionKey = connectionKey; } - public Http2ClientStream newStream(boolean priorKnowledge, int priority) { + public Http2ClientStream newStream(ConnectionContext ctx) { try { semaphore.acquire(); } catch (InterruptedException e) { @@ -59,13 +59,13 @@ public Http2ClientStream newStream(boolean priorKnowledge, int priority) { Http2ClientConnection conn = activeConnection.get(); Http2ClientStream stream; if (conn == null) { - conn = createConnection(connectionKey, priorKnowledge); - stream = conn.createStream(priority); + conn = createConnection(connectionKey, ctx); + stream = conn.createStream(ctx.priority()); } else { - stream = conn.tryStream(priority); + stream = conn.tryStream(ctx.priority()); if (stream == null) { - conn = createConnection(connectionKey, priorKnowledge); - stream = conn.createStream(priority); + conn = createConnection(connectionKey, ctx); + stream = conn.createStream(ctx.priority()); } } @@ -75,9 +75,9 @@ public Http2ClientStream newStream(boolean priorKnowledge, int priority) { } } - private Http2ClientConnection createConnection(ConnectionKey connectionKey, boolean priorKnowledge) { + private Http2ClientConnection createConnection(ConnectionKey connectionKey, ConnectionContext connectionContext) { Http2ClientConnection conn = - new Http2ClientConnection(executor, socketOptions, connectionKey, primaryPath, priorKnowledge); + new Http2ClientConnection(executor, socketOptions, connectionKey, primaryPath, connectionContext); conn.connect(); activeConnection.set(conn); fullConnections.add(conn); diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientImpl.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientImpl.java index 0df67d7e7a5..16a12db9e17 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientImpl.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,11 +23,19 @@ class Http2ClientImpl extends LoomClient implements Http2Client { - private boolean priorKnowledge; + private final int maxFrameSize; + private final long maxHeaderListSize; + private final long initialWindowSize; + private final int prefetch; + private final boolean priorKnowledge; Http2ClientImpl(Http2ClientBuilder builder) { super(builder); this.priorKnowledge = builder.priorKnowledge(); + this.maxFrameSize = builder.maxFrameSize(); + this.maxHeaderListSize = builder.maxHeaderListSize(); + this.initialWindowSize = builder.initialWindowSize(); + this.prefetch = builder.prefetch(); } @Override @@ -35,6 +43,26 @@ public Http2ClientRequest method(Http.Method method) { UriQueryWriteable query = UriQueryWriteable.create(); UriHelper helper = (uri() == null) ? UriHelper.create() : UriHelper.create(uri(), query); - return new ClientRequestImpl(this, executor(), method, helper, priorKnowledge, tls(), query); + return new ClientRequestImpl(this, executor(), method, helper, tls(), query); + } + + long maxHeaderListSize() { + return maxHeaderListSize; + } + + long initialWindowSize() { + return initialWindowSize; + } + + int prefetch() { + return prefetch; + } + + boolean priorKnowledge() { + return priorKnowledge; + } + + public int maxFrameSize() { + return this.maxFrameSize; } } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientRequest.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientRequest.java index 5c48a8aa5b1..fb642895617 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientRequest.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package io.helidon.nima.http2.webclient; +import java.time.Duration; + import io.helidon.nima.webclient.ClientRequest; /** @@ -39,4 +41,21 @@ public interface Http2ClientRequest extends ClientRequest continuationData = new ArrayList<>(); + private final StreamBuffer buffer; private Http2StreamState state = Http2StreamState.IDLE; private Http2Headers currentHeaders; - private int streamId; private StreamFlowControl flowControl; + private int streamId; Http2ClientStream(Http2ClientConnection connection, Http2Settings serverSettings, SocketContext ctx, + ConnectionContext connectionContext, LockingStreamIdSequence streamIdSeq) { this.connection = connection; this.serverSettings = serverSettings; this.ctx = ctx; + this.connectionContext = connectionContext; this.streamIdSeq = streamIdSeq; + this.buffer = new StreamBuffer(streamId, connectionContext.timeout()); } void cancel() { @@ -95,11 +106,20 @@ ReadableEntityBase entity() { } void close() { - //todo cleanup + connection.removeStream(streamId); } - Http2FrameData readOne() { - Http2FrameData frameData = connection.readNextFrame(streamId); + /** + * Push data or header frame in to stream buffer. + * + * @param frameData data or header frame + */ + void push(Http2FrameData frameData) { + buffer.push(frameData); + } + + private Http2FrameData readOne() { + Http2FrameData frameData = buffer.poll(); if (frameData != null) { @@ -108,25 +128,32 @@ Http2FrameData readOne() { int flags = frameData.header().flags(); boolean endOfStream = (flags & Http2Flag.END_OF_STREAM) == Http2Flag.END_OF_STREAM; - if (endOfStream) { - state = Http2StreamState.CLOSED; - } + boolean endOfHeaders = (flags & Http2Flag.END_OF_HEADERS) == Http2Flag.END_OF_HEADERS; + + this.state = Http2StreamState.checkAndGetState(this.state, + frameData.header().type(), + false, + endOfStream, + endOfHeaders); switch (frameData.header().type()) { case DATA: data(frameData.header(), frameData.data()); return frameData; - case HEADERS: - var requestHuffman = new Http2HuffmanDecoder(); - Http2Headers http2Headers = Http2Headers.create(this, - connection.getInboundDynamicTable(), - requestHuffman, - frameData); - this.headers(http2Headers, endOfStream); + case HEADERS, CONTINUATION: + continuationData.add(frameData); + if (endOfHeaders) { + var requestHuffman = new Http2HuffmanDecoder(); + Http2Headers http2Headers = Http2Headers.create(this, + connection.getInboundDynamicTable(), + requestHuffman, + continuationData.toArray(new Http2FrameData[0])); + this.headers(http2Headers, endOfStream); + } break; default: - LOGGER.log(System.Logger.Level.DEBUG, "Dropping frame " + frameData.header() + " expected header or data."); - } + LOGGER.log(DEBUG, "Dropping frame " + frameData.header() + " expected header or data."); + } } return null; } @@ -176,7 +203,7 @@ void writeData(BufferData entityBytes, boolean endOfStream) { splitAndWrite(frameData); } - void splitAndWrite(Http2FrameData frameData) { + private void splitAndWrite(Http2FrameData frameData) { int maxFrameSize = this.serverSettings.value(Http2Setting.MAX_FRAME_SIZE).intValue(); // Split to frames if bigger than max frame size @@ -217,16 +244,22 @@ public void rstStream(Http2RstStream rstStream) { "Received RST_STREAM for stream " + streamId + " in IDLE state"); } - state = Http2StreamState.CLOSED; + this.state = Http2StreamState.checkAndGetState(this.state, + Http2FrameType.RST_STREAM, + false, + false, + false); + throw new RuntimeException("Reset of " + streamId + " stream received!"); } @Override public void windowUpdate(Http2WindowUpdate windowUpdate) { - if (state == Http2StreamState.IDLE) { - throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Received WINDOW_UPDATE for stream " - + streamId + " in state IDLE"); - } + this.state = Http2StreamState.checkAndGetState(this.state, + Http2FrameType.WINDOW_UPDATE, + false, + false, + false); int increment = windowUpdate.windowSizeIncrement(); diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/StreamBuffer.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/StreamBuffer.java new file mode 100644 index 00000000000..d9e0a0eb0e3 --- /dev/null +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/StreamBuffer.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.http2.webclient; + +import java.time.Duration; +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import io.helidon.nima.http2.Http2FrameData; + +class StreamBuffer { + + private final Lock streamLock = new ReentrantLock(); + private final Semaphore dequeSemaphore = new Semaphore(1); + private final Queue buffer = new ArrayDeque<>(); + private final int streamId; + private final Duration timeout; + + StreamBuffer(int streamId, Duration timeout) { + this.streamId = streamId; + this.timeout = timeout; + } + + Http2FrameData poll() { + try { + // Block deque thread when queue is empty + // avoid CPU burning + if (!dequeSemaphore.tryAcquire(timeout.toMillis(), TimeUnit.MILLISECONDS)) { + throw new StreamTimeoutException(streamId, timeout); + } + streamLock.lock(); + return buffer.poll(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } finally { + streamLock.unlock(); + } + } + + void push(Http2FrameData frameData) { + try { + streamLock.lock(); + buffer.add(frameData); + } finally { + streamLock.unlock(); + // Release deque threads + dequeSemaphore.release(); + } + } +} diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/StreamTimeoutException.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/StreamTimeoutException.java new file mode 100644 index 00000000000..e8214036c07 --- /dev/null +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/StreamTimeoutException.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.http2.webclient; + +import java.time.Duration; + +/** + * Thrown when no data are received over the stream within configured request timeout. + */ +public class StreamTimeoutException extends RuntimeException { + StreamTimeoutException(int streamId, Duration timeout) { + super("No data received on stream " + streamId + " within the timeout " + timeout); + } +} diff --git a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java index b5cd92c0e83..e50f16bc99a 100644 --- a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java +++ b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java @@ -34,6 +34,7 @@ import io.helidon.logging.common.LogConfig; import io.helidon.nima.http2.WindowSize; import io.helidon.nima.http2.webclient.Http2; +import io.helidon.nima.http2.webclient.Http2Client; import io.helidon.nima.http2.webclient.Http2ClientResponse; import io.helidon.nima.webclient.WebClient; @@ -63,6 +64,7 @@ public class ClientFlowControlTest { private static final ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor(); private static HttpServer server; private static CompletableFuture outboundTestServerRequestRef = new CompletableFuture<>(); + private static Http2Client client; @BeforeAll static void beforeAll() throws ExecutionException, InterruptedException, TimeoutException { @@ -96,6 +98,11 @@ static void beforeAll() throws ExecutionException, InterruptedException, Timeout .toCompletionStage() .toCompletableFuture() .get(TIMEOUT.toMillis(), MILLISECONDS); + + client = WebClient.builder(Http2.PROTOCOL) + .baseUri("http://localhost:" + server.actualPort() + "/") + .prefetch(10000) + .build(); } @Test @@ -109,9 +116,7 @@ void clientOutbound() throws InterruptedException, ExecutionException, TimeoutEx ByteArrayInputStream baos = new ByteArrayInputStream(EXPECTED.getBytes()); CompletableFuture clientFuture = CompletableFuture.supplyAsync(() -> { - try (Http2ClientResponse res = WebClient.builder(Http2.PROTOCOL) - .baseUri("http://localhost:" + server.actualPort() + "/") - .build() + try (Http2ClientResponse res = client .method(Http.Method.PUT) .path("/out") .priorKnowledge(true) @@ -148,9 +153,7 @@ void clientOutbound() throws InterruptedException, ExecutionException, TimeoutEx void clientInbound() throws InterruptedException { AtomicLong receivedByteSize = new AtomicLong(); - try (Http2ClientResponse res = WebClient.builder(Http2.PROTOCOL) - .baseUri("http://localhost:" + server.actualPort() + "/") - .build() + try (Http2ClientResponse res = client .method(Http.Method.GET) .path("/in") .priorKnowledge(true) diff --git a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/GetTest.java b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/GetTest.java index df427752a98..62a66446200 100644 --- a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/GetTest.java +++ b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/GetTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,6 @@ import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.hamcrest.CoreMatchers.is; @@ -115,8 +114,6 @@ void testByteRoute() throws IOException, InterruptedException { } @Test - @Disabled - // todo this times out void testChunkedRoute() throws IOException, InterruptedException { HttpRequest request = HttpRequest.newBuilder() .GET() diff --git a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/HeadersTest.java b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/HeadersTest.java new file mode 100644 index 00000000000..b99f2432549 --- /dev/null +++ b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/HeadersTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2023 Oracle and/or its affiliates. + * + * Licensed 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 io.helidon.nima.tests.integration.http2.client; + +import java.time.Duration; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeoutException; + +import io.helidon.common.http.Headers; +import io.helidon.common.http.Http; +import io.helidon.logging.common.LogConfig; +import io.helidon.nima.http2.webclient.Http2; +import io.helidon.nima.http2.webclient.Http2ClientResponse; +import io.helidon.nima.webclient.WebClient; + +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerResponse; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +public class HeadersTest { + + private static final Duration TIMEOUT = Duration.ofSeconds(10); + private static final String DATA = "Helidon!!!".repeat(10); + private static final Vertx vertx = Vertx.vertx(); + private static final ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor(); + private static HttpServer server; + private static int port; + + @BeforeAll + static void beforeAll() throws ExecutionException, InterruptedException, TimeoutException { + LogConfig.configureRuntime(); + server = vertx.createHttpServer() + .requestHandler(req -> { + HttpServerResponse res = req.response(); + switch (req.path()) { + case "/trailer" -> { + res.putHeader("test", "before"); + res.write(DATA); + res.putTrailer("Trailer-header", "trailer-test"); + res.end(); + } + case "/cont" -> { + for (int i = 0; i < 500; i++) { + res.headers().add("test-header-" + i, DATA); + } + res.write(DATA); + res.end(); + } + default -> res.setStatusCode(404).end(); + } + }) + .listen(0) + .toCompletionStage() + .toCompletableFuture() + .get(TIMEOUT.toMillis(), MILLISECONDS); + + port = server.actualPort(); + } + + @Test + @Disabled//FIXME: trailer headers are not implemented yet + void trailerHeader() { + try (Http2ClientResponse res = WebClient.builder(Http2.PROTOCOL) + .baseUri("http://localhost:" + port + "/") + .build() + .method(Http.Method.GET) + .path("/trailer") + .priorKnowledge(true) + .request()) { + Headers h = res.headers(); + assertThat(h.first(Http.Header.create("test")).orElse(null), is("before")); + assertThat(res.as(String.class), is(DATA)); + assertThat(h.first(Http.Header.create("Trailer-header")).orElse(null), is("trailer-test")); + } + } + + @Test + void continuation() { + try (Http2ClientResponse res = WebClient.builder(Http2.PROTOCOL) + .baseUri("http://localhost:" + port + "/") + .build() + .method(Http.Method.GET) + .path("/cont") + .priorKnowledge(true) + .request()) { + + Headers h = res.headers(); + for (int i = 0; i < 500; i++) { + String name = "test-header-" + i; + assertThat("Headers " + name, h.first(Http.Header.create(name)).orElse(null), is(DATA)); + } + + assertThat(res.as(String.class), is(DATA)); + } + } + + @AfterAll + static void afterAll() { + server.close(); + vertx.close(); + exec.shutdown(); + try { + if (!exec.awaitTermination(TIMEOUT.toMillis(), MILLISECONDS)) { + exec.shutdownNow(); + } + } catch (InterruptedException e) { + exec.shutdownNow(); + } + } +} diff --git a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/Http2ClientTest.java b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/Http2ClientTest.java index f748d81d55a..cf2be1b77cd 100644 --- a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/Http2ClientTest.java +++ b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/Http2ClientTest.java @@ -42,11 +42,10 @@ @ServerTest class Http2ClientTest { - public static final String MESSAGE = "Hello World!"; + static final String MESSAGE = "Hello World!"; private static final String TEST_HEADER_NAME = "custom_header"; private static final String TEST_HEADER_VALUE = "as!fd"; private static final HeaderValue TEST_HEADER = Header.create(Header.create(TEST_HEADER_NAME), TEST_HEADER_VALUE); - private final Http1Client http1Client; private final Http2Client tlsClient; private final Http2Client plainClient; diff --git a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/PostTest.java b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/PostTest.java index d8b03551d37..9ffb51334ac 100644 --- a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/PostTest.java +++ b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/PostTest.java @@ -68,7 +68,7 @@ class PostTest { static void startServer() { server = WebServer.builder() .host("localhost") - .port(8080) + .port(-1) .addRouting(HttpRouting.builder() .route(Http.Method.POST, "/string", Handler.create(String.class, Routes::string)) .route(Http.Method.POST, "/bytes", Handler.create(byte[].class, Routes::bytes)) diff --git a/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java index 14d846e1166..bcbd38255d1 100644 --- a/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java +++ b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java @@ -34,6 +34,7 @@ import io.helidon.common.reactive.BufferedEmittingPublisher; import io.helidon.common.reactive.Multi; +import io.helidon.nima.http2.WindowSize; import io.helidon.nima.http2.webclient.Http2Client; import io.helidon.nima.http2.webserver.DefaultHttp2Config; import io.helidon.nima.http2.webserver.Http2ConnectionProvider; @@ -68,11 +69,10 @@ static void setUpServer(WebServer.Builder serverBuilder) { serverBuilder .addConnectionProvider(Http2ConnectionProvider.builder() .http2Config(DefaultHttp2Config.builder() - .maxWindowSize(65537) + .maxWindowSize(WindowSize.DEFAULT_WIN_SIZE) ) .build()) .defaultSocket(builder -> builder - .port(-1) .host("localhost") ) .routing(router -> router @@ -101,7 +101,7 @@ static void setUpServer(WebServer.Builder serverBuilder) { .route(Http2Route.route(GET, "/flow-control", (req, res) -> { res.send(EXPECTED); })) - ); + ).port(-1); } FlowControlTest(WebServer server) { @@ -116,6 +116,7 @@ void flowControlWebClientInOut() throws ExecutionException, InterruptedException var client = Http2Client.builder() .priorKnowledge(true) + .initialWindowSize(WindowSize.DEFAULT_WIN_SIZE) .baseUri("http://localhost:" + server.port()) .build(); @@ -154,7 +155,7 @@ void flowControlWebClientInOut() throws ExecutionException, InterruptedException // Wait a bit if more than allowed is sent Thread.sleep(300); // Depends on the win update strategy, can't be full 100k - assertThat(sentData.get(), lessThan(99_000L)); + assertThat(sentData.get(), is(70_000L)); // Let server ask for the rest of the data flowControlServerLatch.complete(null); String response = responded.get(TIMEOUT_SEC, TimeUnit.SECONDS); diff --git a/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/Http2ServerTest.java b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/Http2ServerTest.java index ef5be6bdb20..34f125237d7 100644 --- a/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/Http2ServerTest.java +++ b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/Http2ServerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2023 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,6 @@ import io.helidon.nima.webserver.http.ServerRequest; import io.helidon.nima.webserver.http.ServerResponse; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static io.helidon.common.http.Http.Method.GET; @@ -179,10 +178,4 @@ void testAppProtocol2() throws IOException, InterruptedException { assertThat(response.body(), is("paramValue")); System.clearProperty("jdk.internal.httpclient.disableHostnameVerification"); } - - @Test - @Disabled("No way to test using JDK HTTP Client, Should be part of HTTP/2 client tests") - void testPriorKnowledge() { - - } } diff --git a/nima/tests/integration/http2/server/src/test/resources/logging-test.properties b/nima/tests/integration/http2/server/src/test/resources/logging-test.properties index a15b86667a7..e408b5015a1 100644 --- a/nima/tests/integration/http2/server/src/test/resources/logging-test.properties +++ b/nima/tests/integration/http2/server/src/test/resources/logging-test.properties @@ -16,10 +16,9 @@ handlers=java.util.logging.ConsoleHandler java.util.logging.ConsoleHandler.level=FINEST java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter -java.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS %5$s%6$s%n -#java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +#java.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS %5$s%6$s%n +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n # Global logging level. Can be overridden by specific loggers -io.helidon.nima.http2.WindowSizeImpl.level=ALL -io.helidon.nima.http2.FlowControl.level=ALL +#io.helidon.nima.http2.FlowControl.level=ALL .level=INFO io.helidon.nima.level=INFO From e7467744cd4e9a5f3cc3488c506d46dcf713b51f Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Mon, 3 Apr 2023 18:49:12 +0200 Subject: [PATCH 07/17] HTTP/2 Client - review issues 1 --- .../helidon/common/uri/UriQueryWriteable.java | 6 +- .../common/uri/UriQueryWriteableImpl.java | 19 +-- dependencies/pom.xml | 6 - .../nima/http2/ConnectionFlowControl.java | 116 ++++++++++++++---- .../helidon/nima/http2/FlowControlImpl.java | 29 +++-- .../io/helidon/nima/http2/Http2Headers.java | 1 + .../io/helidon/nima/http2/Http2Settings.java | 17 +-- .../helidon/nima/http2/Http2StreamWriter.java | 6 +- .../io/helidon/nima/http2/WindowSizeImpl.java | 57 +++++---- .../http2/webclient/ClientRequestImpl.java | 23 ++-- .../http2/webclient/ConnectionContext.java | 3 +- .../nima/http2/webclient/Http2Client.java | 4 +- .../webclient/Http2ClientConnection.java | 8 +- .../nima/http2/webclient/Http2ClientImpl.java | 4 +- .../http2/webclient/Http2ClientRequest.java | 8 ++ .../http2/webclient/Http2WebClientTest.java | 68 +++++----- .../nima/http2/webserver/Http2Config.java | 29 ++--- .../nima/http2/webserver/Http2Connection.java | 106 +++++++--------- .../http2/webserver/ConnectionConfigTest.java | 27 +--- .../src/test/resources/application.yaml | 3 - .../http2/client/ClientFlowControlTest.java | 5 +- .../http2/webserver/FlowControlTest.java | 42 ++++--- pom.xml | 6 + 23 files changed, 327 insertions(+), 266 deletions(-) diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java index b5551c83ccd..7de85f74cf1 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteable.java @@ -42,13 +42,13 @@ static UriQueryWriteable create() { UriQueryWriteable set(String name, String... value); /** - * Add a new query parameter or add values to existing. + * Add a new query parameter or add a value to existing. * * @param name name of the parameter - * @param value additional value(s) of the parameter + * @param value additional value of the parameter * @return this instance */ - UriQueryWriteable add(String name, String... value); + UriQueryWriteable add(String name, String value); /** * Set a query parameter with values, if not already defined. diff --git a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java index d7509a879c4..64b98cbffa3 100644 --- a/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java +++ b/common/uri/src/main/java/io/helidon/common/uri/UriQueryWriteableImpl.java @@ -146,21 +146,14 @@ public UriQueryWriteable set(String name, String... values) { } @Override - public UriQueryWriteable add(String name, String... values) { + public UriQueryWriteable add(String name, String value) { String encodedName = UriEncoding.encodeUri(name); + String encodedValue = UriEncoding.encodeUri(value); - List decodedValues = new ArrayList<>(values.length); - List encodedValues = new ArrayList<>(values.length); - - for (String value : values) { - decodedValues.add(value); - encodedValues.add(UriEncoding.encodeUri(value)); - } - - rawQueryParams.computeIfAbsent(encodedName, it -> new ArrayList<>(values.length)) - .addAll(encodedValues); - decodedQueryParams.computeIfAbsent(name, it -> new ArrayList<>(values.length)) - .addAll(decodedValues); + rawQueryParams.computeIfAbsent(encodedName, it -> new ArrayList<>(1)) + .add(encodedValue); + decodedQueryParams.computeIfAbsent(name, it -> new ArrayList<>(1)) + .add(value); return this; } diff --git a/dependencies/pom.xml b/dependencies/pom.xml index 7064f1619ab..76725507d8a 100644 --- a/dependencies/pom.xml +++ b/dependencies/pom.xml @@ -146,7 +146,6 @@ 2.0 1.4.2 2.0.4 - 4.3.7 5.0.SP3 5.1.0.Final 2.0.4 @@ -1386,11 +1385,6 @@ mssql-jdbc ${version.lib.mssql-jdbc} - - io.vertx - vertx-core - ${version.lib.vertx-core} - diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java index 610d891fd66..9d49c21e079 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java @@ -16,8 +16,11 @@ package io.helidon.nima.http2; +import java.time.Duration; import java.util.function.BiConsumer; +import io.helidon.common.Builder; + import static java.lang.System.Logger.Level.DEBUG; /** @@ -29,10 +32,30 @@ public class ConnectionFlowControl { private final Type type; private final BiConsumer windowUpdateWriter; + private final Duration timeout; private final WindowSize.Inbound inboundConnectionWindowSize; private final WindowSize.Outbound outboundConnectionWindowSize; - private int maxFrameSize = WindowSize.DEFAULT_MAX_FRAME_SIZE; - private int initialWindowSize = WindowSize.DEFAULT_WIN_SIZE; + + private volatile int maxFrameSize = WindowSize.DEFAULT_MAX_FRAME_SIZE; + private volatile int initialWindowSize = WindowSize.DEFAULT_WIN_SIZE; + + private ConnectionFlowControl(Type type, + int initialWindowSize, + int maxFrameSize, + BiConsumer windowUpdateWriter, + Duration timeout) { + this.type = type; + this.windowUpdateWriter = windowUpdateWriter; + this.timeout = timeout; + this.inboundConnectionWindowSize = + WindowSize.createInbound(type, + 0, + initialWindowSize, + maxFrameSize, + windowUpdateWriter); + outboundConnectionWindowSize = + WindowSize.createOutbound(type, 0, this); + } /** * Create connection HTTP/2 flow-control for server side. @@ -40,8 +63,8 @@ public class ConnectionFlowControl { * @param windowUpdateWriter method called for sending WINDOW_UPDATE frames to the client. * @return Connection HTTP/2 flow-control */ - public static ConnectionFlowControl createServer(BiConsumer windowUpdateWriter){ - return new ConnectionFlowControl(Type.SERVER, windowUpdateWriter); + public static ConnectionFlowControlBuilder serverBuilder(BiConsumer windowUpdateWriter) { + return new ConnectionFlowControlBuilder(Type.SERVER, windowUpdateWriter); } /** @@ -50,22 +73,8 @@ public static ConnectionFlowControl createServer(BiConsumer windowUpdateWriter){ - return new ConnectionFlowControl(Type.CLIENT, windowUpdateWriter); - } - - private ConnectionFlowControl(Type type, BiConsumer windowUpdateWriter) { - this.type = type; - this.windowUpdateWriter = windowUpdateWriter; - //FIXME: configurable max frame size? - this.inboundConnectionWindowSize = - WindowSize.createInbound(type, - 0, - WindowSize.DEFAULT_WIN_SIZE, - WindowSize.DEFAULT_MAX_FRAME_SIZE, - windowUpdateWriter); - outboundConnectionWindowSize = - WindowSize.createOutbound(type, 0, this); + public static ConnectionFlowControlBuilder clientBuilder(BiConsumer windowUpdateWriter) { + return new ConnectionFlowControlBuilder(Type.CLIENT, windowUpdateWriter); } /** @@ -90,6 +99,7 @@ public long incrementOutboundConnectionWindowSize(int increment) { /** * Decrement inbound connection flow control window, called when DATA frame is received. + * * @param decrement received DATA frame size in bytes * @return inbound window size after decrement */ @@ -114,7 +124,9 @@ public void resetMaxFrameSize(int maxFrameSize) { * @param initialWindowSize INIT_WINDOW_SIZE received */ public void resetInitialWindowSize(int initialWindowSize) { - LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR *: Recv INIT_WINDOW_SIZE %s", type, initialWindowSize)); + if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { + LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR *: Recv INIT_WINDOW_SIZE %s", type, initialWindowSize)); + } this.initialWindowSize = initialWindowSize; } @@ -147,7 +159,69 @@ int initialWindowSize() { return initialWindowSize; } + Duration timeout() { + return timeout; + } + enum Type { SERVER, CLIENT; } + + /** + * Connection flow control builder. + */ + public static class ConnectionFlowControlBuilder implements Builder { + + private static final Duration DEFAULT_TIMEOUT = Duration.ofMillis(100); + private final Type type; + private final BiConsumer windowUpdateWriter; + private int initialWindowSize = WindowSize.DEFAULT_WIN_SIZE; + private int maxFrameSize = WindowSize.DEFAULT_MAX_FRAME_SIZE; + private Duration blockTimeout = DEFAULT_TIMEOUT; + + ConnectionFlowControlBuilder(Type type, BiConsumer windowUpdateWriter) { + this.type = type; + this.windowUpdateWriter = windowUpdateWriter; + } + + /** + * Outbound flow control INITIAL_WINDOW_SIZE setting for new HTTP/2 connections. + * + * @param initialWindowSize units of octets + * @return updated builder + */ + public ConnectionFlowControlBuilder initialWindowSize(int initialWindowSize) { + this.initialWindowSize = initialWindowSize; + return this; + } + + /** + * Initial MAX_FRAME_SIZE setting for new HTTP/2 connections. + * Maximum size of data frames in bytes we are prepared to accept from the other size. + * Default value is 2^14(16_384). + * + * @param maxFrameSize data frame size in bytes between 2^14(16_384) and 2^24-1(16_777_215) + * @return updated client + */ + public ConnectionFlowControlBuilder maxFrameSize(int maxFrameSize) { + this.maxFrameSize = maxFrameSize; + return this; + } + + /** + * Timeout for blocking between windows size check iterations. + * + * @param timeout duration + * @return updated builder + */ + public ConnectionFlowControlBuilder blockTimeout(Duration timeout) { + this.blockTimeout = timeout; + return this; + } + + @Override + public ConnectionFlowControl build() { + return new ConnectionFlowControl(type, initialWindowSize, maxFrameSize, windowUpdateWriter, blockTimeout); + } + } } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java index d36707a8b39..f6b3bfabcf8 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java @@ -103,17 +103,25 @@ WindowSize streamWindowSize() { @Override public void decrementWindowSize(int decrement) { long strRemaining = streamWindowSize().decrementWindowSize(decrement); - LOGGER_INBOUND.log(DEBUG, () -> String.format("%s IFC STR %d: -%d(%d)", type, streamId(), decrement, strRemaining)); + if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { + LOGGER_INBOUND.log(DEBUG, String.format("%s IFC STR %d: -%d(%d)", type, streamId(), decrement, strRemaining)); + } long connRemaining = connectionWindowSize().decrementWindowSize(decrement); - LOGGER_INBOUND.log(DEBUG, () -> String.format("%s IFC STR 0: -%d(%d)", type, decrement, connRemaining)); + if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { + LOGGER_INBOUND.log(DEBUG, String.format("%s IFC STR 0: -%d(%d)", type, decrement, connRemaining)); + } } @Override public void incrementWindowSize(int increment) { long strRemaining = streamWindowSize.incrementWindowSize(increment); - LOGGER_INBOUND.log(DEBUG, () -> String.format("%s IFC STR %d: +%d(%d)", type, streamId(), increment, strRemaining)); + if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { + LOGGER_INBOUND.log(DEBUG, String.format("%s IFC STR %d: +%d(%d)", type, streamId(), increment, strRemaining)); + } long conRemaining = connectionWindowSize.incrementWindowSize(increment); - LOGGER_INBOUND.log(DEBUG, () -> String.format("%s IFC STR 0: +%d(%d)", type, increment, conRemaining)); + if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { + LOGGER_INBOUND.log(DEBUG, String.format("%s IFC STR 0: +%d(%d)", type, increment, conRemaining)); + } } } @@ -145,16 +153,23 @@ WindowSize streamWindowSize() { public void decrementWindowSize(int decrement) { long strRemaining = streamWindowSize().decrementWindowSize(decrement); - LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR %d: -%d(%d)", type, streamId(), decrement, strRemaining)); + if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { + LOGGER_OUTBOUND.log(DEBUG, String.format("%s OFC STR %d: -%d(%d)", type, streamId(), decrement, strRemaining)); + } long connRemaining = connectionWindowSize().decrementWindowSize(decrement); - LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR 0: -%d(%d)", type, decrement, connRemaining)); + if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { + LOGGER_OUTBOUND.log(DEBUG, String.format("%s OFC STR 0: -%d(%d)", type, decrement, connRemaining)); + + } } @Override public long incrementStreamWindowSize(int increment) { long remaining = streamWindowSize.incrementWindowSize(increment); - LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR %d: +%d(%d)", type, streamId(), increment, remaining)); + if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { + LOGGER_OUTBOUND.log(DEBUG, String.format("%s OFC STR %d: +%d(%d)", type, streamId(), increment, remaining)); + } connectionFlowControl.outbound().triggerUpdate(); return remaining; } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Headers.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Headers.java index e14cc43e9e0..e9ed7ac5513 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Headers.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Headers.java @@ -779,6 +779,7 @@ enum StaticHeader implements IndexedHeaderRecord { for (StaticHeader predefinedHeader : StaticHeader.values()) { BY_INDEX.put(predefinedHeader.index(), predefinedHeader); maxIndex = Math.max(maxIndex, predefinedHeader.index); + // Indexed headers may be referenced either with or without value, so we need to store them in both tables if (predefinedHeader.hasValue()) { BY_NAME_VALUE.computeIfAbsent(predefinedHeader.headerName().lowerCase(), it -> new HashMap<>()) .put(predefinedHeader.value(), predefinedHeader); diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Settings.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Settings.java index fc6cdcbee1d..8aa31644446 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Settings.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2Settings.java @@ -25,6 +25,8 @@ import io.helidon.common.buffers.BufferData; import io.helidon.common.socket.SocketContext; +import static java.lang.System.Logger.Level.DEBUG; + /** * HTTP settings frame. */ @@ -37,7 +39,7 @@ public final class Http2Settings implements Http2Frame } /** - * Create emtpy settings frame. + * Create empty settings frame. * * @return settings frame */ @@ -82,6 +84,10 @@ public static Http2Settings create(BufferData frame) { return new Http2Settings(values); } + private static String toString(String setting, int code, Object value) { + return String.format("[%s (0x%02x):%s]", setting, code, value); + } + @SuppressWarnings("unchecked") @Override public Http2FrameData toFrameData(Http2Settings settings, int streamId, Http2Flag.SettingsFlags flags) { @@ -90,8 +96,9 @@ public Http2FrameData toFrameData(Http2Settings settings, int streamId, Http2Fla values.values().forEach(it -> { Object value = it.value(); Http2Setting setting = (Http2Setting) it.setting(); - LOGGER.log(System.Logger.Level.DEBUG, - () -> String.format(" - Http2Settings %s: %s", it.setting().toString(), it.value().toString())); + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, String.format(" - Http2Settings %s: %s", it.setting().toString(), it.value().toString())); + } setting.write(data, value); }); Http2FrameHeader header = Http2FrameHeader.create(data.available(), @@ -173,10 +180,6 @@ public boolean hasValue(Http2Setting setting) { return values.containsKey(setting.identifier()); } - private static String toString(String setting, int code, Object value) { - return String.format("[%s (0x%02x):%s]", setting, code, value); - } - private record SettingValue(Http2Setting setting, Object value) { @Override public String toString() { diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamWriter.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamWriter.java index 0ec72179b68..34eade9838d 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamWriter.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/Http2StreamWriter.java @@ -23,15 +23,15 @@ public interface Http2StreamWriter { /** * Write a frame. * - * @param frame frame to write + * @param frame frame to write */ void write(Http2FrameData frame); /** * Write a frame with flow control. * - * @param frame - * @param flowControl + * @param frame data frame + * @param flowControl outbound flow control */ void writeData(Http2FrameData frame, FlowControl.Outbound flowControl); diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java index b55e7ddce1a..1ee44d9c0d6 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java @@ -35,8 +35,8 @@ abstract class WindowSizeImpl implements WindowSize { private final ConnectionFlowControl.Type type; private final int streamId; - private int windowSize; private final AtomicInteger remainingWindowSize; + private int windowSize; private WindowSizeImpl(ConnectionFlowControl.Type type, int streamId, int initialWindowSize) { this.type = type; @@ -52,8 +52,10 @@ public void resetWindowSize(int size) { // it maintains by the difference between the new value and the old value remainingWindowSize.updateAndGet(o -> o + size - windowSize); windowSize = size; - LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR %d: Recv INITIAL_WINDOW_SIZE %d(%d)", - type, streamId, windowSize, remainingWindowSize.get())); + if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { + LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR %d: Recv INITIAL_WINDOW_SIZE %d(%d)", + type, streamId, windowSize, remainingWindowSize.get())); + } } @Override @@ -125,17 +127,21 @@ static final class Outbound extends WindowSizeImpl implements WindowSize.Outboun private final AtomicReference> updated = new AtomicReference<>(new CompletableFuture<>()); private final ConnectionFlowControl.Type type; private final int streamId; + private final long timeoutMillis; Outbound(ConnectionFlowControl.Type type, int streamId, ConnectionFlowControl connectionFlowControl) { super(type, streamId, connectionFlowControl.initialWindowSize()); this.type = type; this.streamId = streamId; + this.timeoutMillis = connectionFlowControl.timeout().toMillis(); } @Override public long incrementWindowSize(int increment) { long remaining = super.incrementWindowSize(increment); - LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR %d: +%d(%d)", type, streamId, increment, remaining)); + if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { + LOGGER_OUTBOUND.log(DEBUG, String.format("%s OFC STR %d: +%d(%d)", type, streamId, increment, remaining)); + } triggerUpdate(); return remaining; } @@ -149,11 +155,12 @@ public void triggerUpdate() { public void blockTillUpdate() { while (getRemainingWindowSize() < 1) { try { - //TODO configurable timeout - updated.get().get(500, TimeUnit.MILLISECONDS); + updated.get().get(timeoutMillis, TimeUnit.MILLISECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { - LOGGER_OUTBOUND.log(DEBUG, () -> - String.format("%s OFC STR %d: Window depleted, waiting for update.", type, streamId)); + if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { + LOGGER_OUTBOUND.log(DEBUG, + String.format("%s OFC STR %d: Window depleted, waiting for update.", type, streamId)); + } } } } @@ -209,6 +216,11 @@ public String toString() { abstract static class Strategy { + // Strategy Type to instance mapping array + private static final StrategyConstructor[] CREATORS = new StrategyConstructor[] { + Simple::new, + Bisection::new + }; private final Context context; private final int streamId; private final BiConsumer windowUpdateWriter; @@ -219,6 +231,12 @@ private Strategy(Context context, int streamId, BiConsumer windowUpdateWriter) { + return CREATORS[Type.select(context).ordinal()] + .create(context, streamId, windowUpdateWriter); + } + abstract void windowUpdate(ConnectionFlowControl.Type type, int increment, int i); Context context() { @@ -233,22 +251,6 @@ BiConsumer windowUpdateWriter() { return windowUpdateWriter; } - private interface StrategyConstructor { - Strategy create(Context context, int streamId, BiConsumer windowUpdateWriter); - } - - // Strategy Type to instance mapping array - private static final StrategyConstructor[] CREATORS = new StrategyConstructor[] { - Simple::new, - Bisection::new - }; - - // Strategy implementation factory - private static Strategy create(Context context, int streamId, BiConsumer windowUpdateWriter) { - return CREATORS[Type.select(context).ordinal()] - .create(context, streamId, windowUpdateWriter); - } - private enum Type { /** * Simple WINDOW_UPDATE strategy. @@ -268,6 +270,10 @@ private static Type select(Context context) { } + private interface StrategyConstructor { + Strategy create(Context context, int streamId, BiConsumer windowUpdateWriter); + } + private record Context( int maxFrameSize, int initialWindowSize) { @@ -297,9 +303,8 @@ void windowUpdate(ConnectionFlowControl.Type type, int streamId, int increment) */ private static final class Bisection extends Strategy { - private int delayedIncrement; - private final int watermark; + private int delayedIncrement; private Bisection(Context context, int streamId, BiConsumer windowUpdateWriter) { super(context, streamId, windowUpdateWriter); diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java index aa5a8bcc344..dbcde109033 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ClientRequestImpl.java @@ -46,23 +46,23 @@ class ClientRequestImpl implements Http2ClientRequest { static final HeaderValue USER_AGENT_HEADER = Header.create(Header.USER_AGENT, "Helidon Nima " + Version.VERSION); //todo Gracefully close connections in channel cache private static final Map CHANNEL_CACHE = new ConcurrentHashMap<>(); - private WritableHeaders explicitHeaders = WritableHeaders.create(); - private final Http2ClientImpl client; - private final ExecutorService executor; private final Http.Method method; private final UriHelper uri; private final UriQueryWriteable query; + private final int initialWindowSize; + private final int maxFrameSize; + private final long maxHeaderListSize; + private final int connectionPrefetch; + + private WritableHeaders explicitHeaders = WritableHeaders.create(); private Tls tls; private int priority; private boolean priorKnowledge; - private long initialWindowSize; - private int maxFrameSize; - private long maxHeaderListSize; - private int connectionPrefetch; private int requestPrefetch = 0; private ClientConnection explicitConnection; + private Duration flowControlTimeout = Duration.ofMillis(100); private Duration timeout = Duration.ofSeconds(10); ClientRequestImpl(Http2ClientImpl client, @@ -115,7 +115,7 @@ public Http2ClientRequest pathParam(String name, String value) { @Override public Http2ClientRequest queryParam(String name, String... values) { - query.add(name, values); + query.set(name, values); return this; } @@ -218,6 +218,12 @@ public Http2ClientRequest timeout(Duration timeout) { return this; } + @Override + public Http2ClientRequest flowControlTimeout(Duration timeout) { + this.flowControlTimeout = timeout; + return this; + } + UriHelper uriHelper() { return uri; } @@ -285,6 +291,7 @@ private Http2ClientStream newStream(UriHelper uri) { maxHeaderListSize, connectionPrefetch, requestPrefetch, + flowControlTimeout, timeout)); } catch (UpgradeRedirectException e) { return newStream(UriHelper.create(URI.create(e.redirectUri()), UriQueryWriteable.create())); diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionContext.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionContext.java index dbad3f0124a..2b5ecc354ce 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionContext.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/ConnectionContext.java @@ -20,10 +20,11 @@ record ConnectionContext(int priority, boolean priorKnowledge, - long initialWindowSize, + int initialWindowSize, int maxFrameSize, long maxHeaderListSize, int connectionPrefetch, int requestPrefetch, + Duration flowControlTimeout, Duration timeout) { } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2Client.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2Client.java index aedf09a9bbb..010e164c5dd 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2Client.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2Client.java @@ -42,7 +42,7 @@ class Http2ClientBuilder extends WebClient.Builder priorKnowledgeClient = LazyValue.create(() -> Http2Client.builder() + .priorKnowledge(true) + .baseUri("http://localhost:" + plainPort + "/versionspecific") + .build()); + private static final LazyValue upgradeClient = LazyValue.create(() -> Http2Client.builder() + .baseUri("http://localhost:" + plainPort + "/versionspecific") + .build()); private static int tlsPort; + private static final LazyValue tlsClient = LazyValue.create(() -> Http2Client.builder() + .baseUri("https://localhost:" + tlsPort + "/versionspecific") + .tls(Tls.builder() + .enabled(true) + .trustAll(true) + .endpointIdentificationAlgorithm(Tls.ENDPOINT_IDENTIFICATION_NONE) + .build()) + .build()); Http2WebClientTest(WebServer server) { this.plainPort = server.port(); @@ -91,8 +104,7 @@ static void setUpServer(WebServer.Builder serverBuilder) { ) .addConnectionProvider(Http2ConnectionProvider.builder() .http2Config(DefaultHttp2Config.builder() - .flowControlEnabled(true) - .maxStreamWindowSize(10)) + .initialWindowSize(10)) .build()) .socket("https", builder -> builder.port(-1) @@ -130,28 +142,12 @@ static void setUpServer(WebServer.Builder serverBuilder) { Thread.sleep(10); } } catch (IOException | InterruptedException e) { - e.printStackTrace(); + throw new RuntimeException(e); } })) ); } - private static final LazyValue priorKnowledgeClient = LazyValue.create(() -> Http2Client.builder() - .priorKnowledge(true) - .baseUri("http://localhost:" + plainPort + "/versionspecific") - .build()); - private static final LazyValue upgradeClient = LazyValue.create(() -> Http2Client.builder() - .baseUri("http://localhost:" + plainPort + "/versionspecific") - .build()); - private static final LazyValue tlsClient = LazyValue.create(() -> Http2Client.builder() - .baseUri("https://localhost:" + tlsPort + "/versionspecific") - .tls(Tls.builder() - .enabled(true) - .trustAll(true) - .endpointIdentificationAlgorithm(Tls.ENDPOINT_IDENTIFICATION_NONE) - .build()) - .build()); - private static Stream clientTypes() { return Stream.of( Arguments.of("priorKnowledge", priorKnowledgeClient), @@ -160,6 +156,14 @@ private static Stream clientTypes() { ); } + @AfterAll + static void afterAll() throws InterruptedException { + executorService.shutdown(); + if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + } + } + @ParameterizedTest(name = "{0}") @MethodSource("clientTypes") void clientGet(String name, LazyValue client) { @@ -239,7 +243,9 @@ void multiplexParallelStreamsGet(String clientType, LazyValue clien InputStream is = response.inputStream(); for (int i = 0; ; i++) { byte[] bytes = is.readNBytes("0BAF000".getBytes().length); - if (bytes.length == 0) break; + if (bytes.length == 0) { + break; + } String message = new String(bytes); assertThat(message, is(String.format(id + "BAF%03d", i))); } @@ -256,12 +262,4 @@ void multiplexParallelStreamsGet(String clientType, LazyValue clien , CompletableFuture.runAsync(() -> callable.accept(4), executorService) ).get(5, TimeUnit.MINUTES); } - - @AfterAll - static void afterAll() throws InterruptedException { - executorService.shutdown(); - if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) { - executorService.shutdownNow(); - } - } } diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java index 07601a43edc..0dcd3a5d796 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java @@ -16,6 +16,8 @@ package io.helidon.nima.http2.webserver; +import java.time.Duration; + import io.helidon.builder.Builder; import io.helidon.builder.config.ConfigBean; import io.helidon.common.http.RequestedUriDiscoveryContext; @@ -59,16 +61,6 @@ public interface Http2Config { @ConfiguredOption("8192") long maxConcurrentStreams(); - /** - * This setting indicates whether flow control is turned on or off. Value of {@code true} turns flow control on - * and value of {@code false} turns flow control off. - * Default value is {@code true}. - * - * @return whether flow control is enabled - */ - @ConfiguredOption("true") - boolean flowControlEnabled(); - /** * This setting indicates the sender's maximum window size in bytes for connection-level flow control. * Default and maximum value is 231-1 = 2147483647 bytes. This setting affects the window size @@ -78,21 +70,16 @@ public interface Http2Config { * * @return maximum window size in bytes */ - @ConfiguredOption("2147483647") - int maxWindowSize(); + @ConfiguredOption("65635") + int initialWindowSize(); /** - * This setting indicates the sender's maximum window size in bytes for stream-level flow control. - * Value of {@code 0} is reserved to use the same value as connection-level value. - * Default value is {@code 0}. This setting affects the window size of all streams. - * Any value greater than 2147483647 causes an error. Any value greater than {@code 0} and smaller than initial - * window size causes an error. - * See RFC 9113 section 6.9.1 for details. + * Outbound flow control blocking timeout. * - * @return maximum stream-level window size in bytes + * @return duration */ - @ConfiguredOption("0") - int maxStreamWindowSize(); + @ConfiguredOption("java.time.Duration.ofMillis(100)") + Duration flowControlTimeout(); /** * Whether to send error message over HTTP to client. diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java index a6920614315..1e561bd4826 100755 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java @@ -86,7 +86,6 @@ public class Http2Connection implements ServerConnection, InterruptableTask subProviders; private final DataReader reader; - private final Http2Settings serverSettings; private final boolean sendErrorDetails; private final ConnectionFlowControl flowControl; @@ -124,24 +123,26 @@ public class Http2Connection implements ServerConnection, InterruptableTask 0 -// ? http2Config.maxStreamWindowSize() -// : http2Config.maxWindowSize(); -// } else { -// // Pass NOOP when flow control is turned off (but we still have to send WINDOW_UPDATE frames) -// this.inboundWindowSize = WindowSize.createInboundNoop(this::writeWindowUpdateFrame); -// this.inboundInitialWindowSize = WindowSize.DEFAULT_WIN_SIZE; -// } - // Flow control is initialized by RFC 9113 default values - this.flowControl = ConnectionFlowControl.createServer(this::writeWindowUpdateFrame); + this.flowControl = ConnectionFlowControl.serverBuilder(this::writeWindowUpdateFrame) + .initialWindowSize(http2Config.initialWindowSize()) + .blockTimeout(http2Config.flowControlTimeout()) + .maxFrameSize(http2Config.maxFrameSize()) + .build(); + } + + private static void settingsUpdate(Http2Config config, Http2Settings.Builder builder) { + applySetting(builder, config.maxFrameSize(), Http2Setting.MAX_FRAME_SIZE); + applySetting(builder, config.maxHeaderListSize(), Http2Setting.MAX_HEADER_LIST_SIZE); + applySetting(builder, config.maxConcurrentStreams(), Http2Setting.MAX_CONCURRENT_STREAMS); + applySetting(builder, config.initialWindowSize(), Http2Setting.INITIAL_WINDOW_SIZE); + } + + // Add value to the builder only when differs from default + private static void applySetting(Http2Settings.Builder builder, long value, Http2Setting settings) { + if (value != settings.defaultValue()) { + builder.add(settings, value); + } } @Override @@ -287,20 +288,6 @@ Http2Settings serverSettings() { return serverSettings; } - private static void settingsUpdate(Http2Config config, Http2Settings.Builder builder) { - applySetting(builder, config.maxFrameSize(), Http2Setting.MAX_FRAME_SIZE); - applySetting(builder, config.maxHeaderListSize(), Http2Setting.MAX_HEADER_LIST_SIZE); - applySetting(builder, config.maxConcurrentStreams(), Http2Setting.MAX_CONCURRENT_STREAMS); - applySetting(builder, config.maxWindowSize(), Http2Setting.INITIAL_WINDOW_SIZE); - } - - // Add value to the builder only when differs from default - private static void applySetting(Http2Settings.Builder builder, long value, Http2Setting settings) { - if (value != settings.defaultValue()) { - builder.add(settings, value); - } - } - private void doHandle() throws InterruptedException { while (state != State.FINISHED) { @@ -326,22 +313,22 @@ private void doHandle() throws InterruptedException { private void dispatchHandler() { switch (state) { - case CONTINUATION -> doContinuation(); - case WRITE_SERVER_SETTINGS -> writeServerSettings(); - case WINDOW_UPDATE -> readWindowUpdateFrame(); - case SETTINGS -> doSettings(); - case ACK_SETTINGS -> ackSettings(); - case DATA -> dataFrame(); - case HEADERS -> doHeaders(); - case PRIORITY -> doPriority(); - case READ_PUSH_PROMISE -> throw new Http2Exception(Http2ErrorCode.REFUSED_STREAM, "Push promise not supported"); - case PING -> pingFrame(); - case SEND_PING_ACK -> writePingAck(); - case GO_AWAY -> - // todo we may need to do graceful shutdown to process the last stream - goAwayFrame(); - case RST_STREAM -> rstStream(); - default -> unknownFrame(); + case CONTINUATION -> doContinuation(); + case WRITE_SERVER_SETTINGS -> writeServerSettings(); + case WINDOW_UPDATE -> readWindowUpdateFrame(); + case SETTINGS -> doSettings(); + case ACK_SETTINGS -> ackSettings(); + case DATA -> dataFrame(); + case HEADERS -> doHeaders(); + case PRIORITY -> doPriority(); + case READ_PUSH_PROMISE -> throw new Http2Exception(Http2ErrorCode.REFUSED_STREAM, "Push promise not supported"); + case PING -> pingFrame(); + case SEND_PING_ACK -> writePingAck(); + case GO_AWAY -> + // todo we may need to do graceful shutdown to process the last stream + goAwayFrame(); + case RST_STREAM -> rstStream(); + default -> unknownFrame(); } } @@ -525,7 +512,6 @@ private void dataFrame() { } } - if (frameHeader.flags(Http2FrameTypes.DATA).padded()) { BufferData frameData = inProgressFrame(); int padLength = frameData.read(); @@ -727,19 +713,19 @@ private StreamContext stream(int streamId) { // as a stream error (section 5.4.2) of type PROTOCOL_ERROR or REFUSED_STREAM. if (streams.size() > maxClientConcurrentStreams) { throw new Http2Exception(Http2ErrorCode.REFUSED_STREAM, - "Maximum concurrent streams limit " + maxClientConcurrentStreams + " exceeded"); + "Maximum concurrent streams limit " + maxClientConcurrentStreams + " exceeded"); } // Pass NOOP when flow control is turned off -// FlowControl.Inbound.Builder inboundFlowControlBuilder = http2Config.flowControlEnabled() -// ? FlowControl.builderInbound(FlowControl.Type.SERVER) -// .streamId(streamId) -// .connectionWindowSize(inboundWindowSize) -// .streamWindowSize(inboundInitialWindowSize) -// .streamMaxFrameSize(http2Config.maxFrameSize()) -// // Pass NOOP when flow control is turned off (but we still have to send WINDOW_UPDATE frames) -// : FlowControl.builderInbound(FlowControl.Type.SERVER) -// .connectionWindowSize(inboundWindowSize) -// .noop(); + // FlowControl.Inbound.Builder inboundFlowControlBuilder = http2Config.flowControlEnabled() + // ? FlowControl.builderInbound(FlowControl.Type.SERVER) + // .streamId(streamId) + // .connectionWindowSize(inboundWindowSize) + // .streamWindowSize(inboundInitialWindowSize) + // .streamMaxFrameSize(http2Config.maxFrameSize()) + // // Pass NOOP when flow control is turned off (but we still have to send WINDOW_UPDATE frames) + // : FlowControl.builderInbound(FlowControl.Type.SERVER) + // .connectionWindowSize(inboundWindowSize) + // .noop(); streamContext = new StreamContext(streamId, new Http2Stream(ctx, routing, diff --git a/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java b/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java index e029fc1296d..4360ca47201 100644 --- a/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java +++ b/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java @@ -99,40 +99,17 @@ void testConfigValidatePath() { assertThat(http2Config.validatePath(), is(false)); } - // Verify that HTTP/2 flow control enabled is properly configured from configuration file - @Test - void testFlowControlEnabled() { - // This will pick up application.yaml from the classpath as default configuration file - TestProvider provider = new TestProvider(); - WebServer.builder().addConnectionProvider(provider).build(); - assertThat(provider.isConfig(), is(true)); - Http2Config http2Config = provider.config(); - assertThat(http2Config.flowControlEnabled(), is(false)); - } - // Verify that HTTP/2 maximum connection-level window size is properly configured from configuration file @Test - void testMaxWindowSize() { + void testInitialWindowSize() { // This will pick up application.yaml from the classpath as default configuration file TestProvider provider = new TestProvider(); WebServer.builder().addConnectionProvider(provider).build(); assertThat(provider.isConfig(), is(true)); Http2Config http2Config = provider.config(); - assertThat(http2Config.maxWindowSize(), is(32767)); + assertThat(http2Config.initialWindowSize(), is(32767)); } - // Verify that HTTP/2 maximum stream-level window size is properly configured from configuration file - @Test - void testMaxStreamWindowSize() { - // This will pick up application.yaml from the classpath as default configuration file - TestProvider provider = new TestProvider(); - WebServer.builder().addConnectionProvider(provider).build(); - assertThat(provider.isConfig(), is(true)); - Http2Config http2Config = provider.config(); - assertThat(http2Config.maxStreamWindowSize(), is(16383)); - } - - private static class TestProvider implements ServerConnectionProvider { private Http2Config http2Config = null; diff --git a/nima/http2/webserver/src/test/resources/application.yaml b/nima/http2/webserver/src/test/resources/application.yaml index 8fc07bcf13d..a243905461d 100644 --- a/nima/http2/webserver/src/test/resources/application.yaml +++ b/nima/http2/webserver/src/test/resources/application.yaml @@ -23,8 +23,5 @@ server: max-frame-size: 8192 max-header-list-size: 4096 max-concurrent-streams: 16384 - flow-control-enabled: false initial-window-size: 8192 - max-window-size: 32767 - max-stream-window-size: 16383 validate-path: false diff --git a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java index e50f16bc99a..ccf2cc6b002 100644 --- a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java +++ b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java @@ -45,6 +45,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import static java.lang.System.Logger.Level.DEBUG; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.hamcrest.MatcherAssert.assertThat; @@ -75,12 +76,12 @@ static void beforeAll() throws ExecutionException, InterruptedException, Timeout case "/in" -> { for (int i = 0; i < 5; i++) { req.response().write(DATA_10_K) - .andThen(event -> LOGGER.log(System.Logger.Level.DEBUG, "Vertx server sent " + + .andThen(event -> LOGGER.log(DEBUG, "Vertx server sent " + inboundServerSentData.addAndGet(BYTES_10_K.length))); } for (int i = 5; i < 10; i++) { req.response().write(DATA_10_K.toUpperCase()) - .andThen(event -> LOGGER.log(System.Logger.Level.DEBUG, "Vertx server sent " + + .andThen(event -> LOGGER.log(DEBUG, "Vertx server sent " + inboundServerSentData.addAndGet(BYTES_10_K_UP_CASE.length))); } req.end(); diff --git a/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java index bcbd38255d1..fb449e2efc1 100644 --- a/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java +++ b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java @@ -46,6 +46,8 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; +import static java.lang.System.Logger.Level.DEBUG; + import static io.helidon.common.http.Http.Method.GET; import static io.helidon.common.http.Http.Method.PUT; import static org.hamcrest.MatcherAssert.assertThat; @@ -56,20 +58,26 @@ public class FlowControlTest { private static final System.Logger LOGGER = System.getLogger(FlowControlTest.class.getName()); - private static volatile CompletableFuture flowControlServerLatch = new CompletableFuture<>(); - private static volatile CompletableFuture flowControlClientLatch = new CompletableFuture<>(); private static final ExecutorService exec = Executors.newCachedThreadPool(); private static final String DATA_10_K = "Helidon!!!".repeat(1_000); private static final String EXPECTED = DATA_10_K.repeat(5) + DATA_10_K.toUpperCase().repeat(5); private static final int TIMEOUT_SEC = 150; + + private static volatile CompletableFuture flowControlServerLatch = new CompletableFuture<>(); + private static volatile CompletableFuture flowControlClientLatch = new CompletableFuture<>(); + private final WebServer server; + FlowControlTest(WebServer server) { + this.server = server; + } + @SetUpServer static void setUpServer(WebServer.Builder serverBuilder) { serverBuilder .addConnectionProvider(Http2ConnectionProvider.builder() .http2Config(DefaultHttp2Config.builder() - .maxWindowSize(WindowSize.DEFAULT_WIN_SIZE) + .initialWindowSize(WindowSize.DEFAULT_WIN_SIZE) ) .build()) .defaultSocket(builder -> builder @@ -101,11 +109,15 @@ static void setUpServer(WebServer.Builder serverBuilder) { .route(Http2Route.route(GET, "/flow-control", (req, res) -> { res.send(EXPECTED); })) - ).port(-1); + ).port(8080); } - FlowControlTest(WebServer server) { - this.server = server; + @AfterAll + static void afterAll() throws InterruptedException { + exec.shutdown(); + if (!exec.awaitTermination(TIMEOUT_SEC, TimeUnit.SECONDS)) { + exec.shutdownNow(); + } } @Test @@ -131,14 +143,14 @@ void flowControlWebClientInOut() throws ExecutionException, InterruptedException out -> { for (int i = 0; i < 5; i++) { byte[] bytes = DATA_10_K.getBytes(); - LOGGER.log(System.Logger.Level.DEBUG, + LOGGER.log(DEBUG, () -> String.format("CL: Sending %d bytes", bytes.length)); out.write(bytes); sentData.updateAndGet(o -> o + bytes.length); } for (int i = 0; i < 5; i++) { byte[] bytes = DATA_10_K.toUpperCase().getBytes(); - LOGGER.log(System.Logger.Level.DEBUG, + LOGGER.log(DEBUG, () -> String.format("CL: Sending %d bytes", bytes.length)); out.write(bytes); sentData.updateAndGet(o -> o + bytes.length); @@ -200,7 +212,7 @@ void flowControlHttpClientInOut() throws ExecutionException, InterruptedExceptio try { HttpResponse response = cl.send(HttpRequest.newBuilder() - .timeout(Duration.ofSeconds(50)) + .timeout(Duration.ofSeconds(5)) .uri(URI.create("http://localhost:" + server.port() + "/flow-control")) .PUT(HttpRequest.BodyPublishers.fromPublisher( Multi.create(publisher) @@ -218,12 +230,12 @@ void flowControlHttpClientInOut() throws ExecutionException, InterruptedExceptio for (int i = 0; i < 5; i++) { byte[] bytes = DATA_10_K.getBytes(); - LOGGER.log(System.Logger.Level.DEBUG, () -> String.format("CL: Sending %d bytes", bytes.length)); + LOGGER.log(DEBUG, () -> String.format("CL: Sending %d bytes", bytes.length)); publisher.emit(ByteBuffer.wrap(bytes)); } for (int i = 0; i < 5; i++) { byte[] bytes = DATA_10_K.toUpperCase().getBytes(); - LOGGER.log(System.Logger.Level.DEBUG, () -> String.format("CL: Sending %d bytes", bytes.length)); + LOGGER.log(DEBUG, () -> String.format("CL: Sending %d bytes", bytes.length)); publisher.emit(ByteBuffer.wrap(bytes)); } @@ -249,12 +261,4 @@ void name() { flowControlServerLatch.complete(null); new CompletableFuture().join(); } - - @AfterAll - static void afterAll() throws InterruptedException { - exec.shutdown(); - if (!exec.awaitTermination(TIMEOUT_SEC, TimeUnit.SECONDS)) { - exec.shutdownNow(); - } - } } diff --git a/pom.xml b/pom.xml index f9aab8d0c3b..eddd4a5d75a 100644 --- a/pom.xml +++ b/pom.xml @@ -82,6 +82,7 @@ 3.1.6 3.0.0.Final 1.23 + 4.3.7 2.26.3 3.10 4.8.154 @@ -987,6 +988,11 @@ reactive-streams-tck-flow ${version.lib.reactivestreams} + + io.vertx + vertx-core + ${version.lib.vertx-core} + org.apache.maven.wagon wagon-provider-api From 491f5a717e57940561022eda884a53a8f080e711 Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Mon, 3 Apr 2023 18:59:29 +0200 Subject: [PATCH 08/17] HTTP/2 Client - review issues 2 --- .../io/helidon/nima/http2/WindowSizeImpl.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java index 1ee44d9c0d6..3bc8e4859e2 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java @@ -291,7 +291,9 @@ private Simple(Context context, int streamId, BiConsumer String.format("%s IFC STR %d: Send WINDOW_UPDATE %s", type, streamId, increment)); + if (LOGGER_INBOUND.isLoggable(DEBUG)) { + LOGGER_INBOUND.log(DEBUG, String.format("%s IFC STR %d: Send WINDOW_UPDATE %s", type, streamId, increment)); + } windowUpdateWriter().accept(streamId(), new Http2WindowUpdate(increment)); } @@ -314,12 +316,16 @@ private Bisection(Context context, int streamId, BiConsumer String.format("%s IFC STR %d: Deferred WINDOW_UPDATE %d, total %d, watermark %d", - type, streamId, increment, delayedIncrement, watermark)); + if (LOGGER_INBOUND.isLoggable(DEBUG)) { + LOGGER_INBOUND.log(DEBUG, String.format("%s IFC STR %d: Deferred WINDOW_UPDATE %d, total %d, watermark %d", + type, streamId, increment, delayedIncrement, watermark)); + } delayedIncrement += increment; if (delayedIncrement > watermark) { - LOGGER_INBOUND.log(DEBUG, () -> String.format("%s IFC STR %d: Send WINDOW_UPDATE %d, watermark %d", - type, streamId, delayedIncrement, watermark)); + if (LOGGER_INBOUND.isLoggable(DEBUG)) { + LOGGER_INBOUND.log(DEBUG, String.format("%s IFC STR %d: Send WINDOW_UPDATE %d, watermark %d", + type, streamId, delayedIncrement, watermark)); + } windowUpdateWriter().accept(streamId(), new Http2WindowUpdate(delayedIncrement)); delayedIncrement = 0; } From 3dbbbcb7dd5eb19a24c046f8cd91bd38ab868732 Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Mon, 3 Apr 2023 19:28:45 +0200 Subject: [PATCH 09/17] HTTP/2 Client - review issues 3 --- .../http2/webclient/Http2ClientStream.java | 37 +++++++++---------- pom.xml | 2 +- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java index 1d08416e2a6..6eb17926cbc 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java @@ -59,15 +59,13 @@ class Http2ClientStream implements Http2Stream { private final Http2ClientConnection connection; private final Http2Settings serverSettings; private final SocketContext ctx; - private final ConnectionContext connectionContext; private final LockingStreamIdSequence streamIdSeq; private final Http2FrameListener sendListener = new Http2LoggingFrameListener("cl-send"); private final Http2FrameListener recvListener = new Http2LoggingFrameListener("cl-recv"); - - // todo configure private final Http2Settings settings = Http2Settings.create(); private final List continuationData = new ArrayList<>(); private final StreamBuffer buffer; + private Http2StreamState state = Http2StreamState.IDLE; private Http2Headers currentHeaders; private StreamFlowControl flowControl; @@ -81,7 +79,6 @@ class Http2ClientStream implements Http2Stream { this.connection = connection; this.serverSettings = serverSettings; this.ctx = ctx; - this.connectionContext = connectionContext; this.streamIdSeq = streamIdSeq; this.buffer = new StreamBuffer(streamId, connectionContext.timeout()); } @@ -137,22 +134,22 @@ private Http2FrameData readOne() { endOfHeaders); switch (frameData.header().type()) { - case DATA: - data(frameData.header(), frameData.data()); - return frameData; - case HEADERS, CONTINUATION: - continuationData.add(frameData); - if (endOfHeaders) { - var requestHuffman = new Http2HuffmanDecoder(); - Http2Headers http2Headers = Http2Headers.create(this, - connection.getInboundDynamicTable(), - requestHuffman, - continuationData.toArray(new Http2FrameData[0])); - this.headers(http2Headers, endOfStream); - } - break; - default: - LOGGER.log(DEBUG, "Dropping frame " + frameData.header() + " expected header or data."); + case DATA: + data(frameData.header(), frameData.data()); + return frameData; + case HEADERS, CONTINUATION: + continuationData.add(frameData); + if (endOfHeaders) { + var requestHuffman = new Http2HuffmanDecoder(); + Http2Headers http2Headers = Http2Headers.create(this, + connection.getInboundDynamicTable(), + requestHuffman, + continuationData.toArray(new Http2FrameData[0])); + this.headers(http2Headers, endOfStream); + } + break; + default: + LOGGER.log(DEBUG, "Dropping frame " + frameData.header() + " expected header or data."); } } return null; diff --git a/pom.xml b/pom.xml index eddd4a5d75a..a5a58418d7f 100644 --- a/pom.xml +++ b/pom.xml @@ -82,7 +82,7 @@ 3.1.6 3.0.0.Final 1.23 - 4.3.7 + 4.3.8 2.26.3 3.10 4.8.154 From ed047aa6aab8da1ebf29bd67c04a0637b3ac415b Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Mon, 3 Apr 2023 20:12:56 +0200 Subject: [PATCH 10/17] HTTP/2 Client - review issues 4 --- .../io/helidon/nima/http2/webserver/ConnectionConfigTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java b/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java index 4360ca47201..b2b008ab37a 100644 --- a/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java +++ b/nima/http2/webserver/src/test/java/io/helidon/nima/http2/webserver/ConnectionConfigTest.java @@ -107,7 +107,7 @@ void testInitialWindowSize() { WebServer.builder().addConnectionProvider(provider).build(); assertThat(provider.isConfig(), is(true)); Http2Config http2Config = provider.config(); - assertThat(http2Config.initialWindowSize(), is(32767)); + assertThat(http2Config.initialWindowSize(), is(8192)); } private static class TestProvider implements ServerConnectionProvider { From 0d0852c6f42e2f30e5f98cb7450f7ffc10f98cd8 Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Mon, 3 Apr 2023 22:13:39 +0200 Subject: [PATCH 11/17] HTTP/2 Client - review issues 5 --- nima/http2/http2/pom.xml | 5 + .../nima/http2/ConnectionFlowControl.java | 2 +- .../helidon/nima/http2/FlowControlImpl.java | 9 +- .../io/helidon/nima/http2/WindowSizeImpl.java | 4 +- .../helidon/nima/http2/Http2HeadersTest.java | 77 +----- .../nima/http2/MaxFrameSizeSplitTest.java | 66 ++--- .../webclient/Http2ClientConnection.java | 137 +++++----- .../nima/http2/webclient/Http2ClientImpl.java | 2 +- .../http2/webclient/Http2ClientStream.java | 240 +++++++++--------- .../http2/webclient/Http2WebClientTest.java | 2 +- .../test/resources/logging-test.properties | 4 - .../http2/client/ClientFlowControlTest.java | 30 +-- .../integration/http2/client/HeadersTest.java | 59 ++--- .../http2/webserver/FlowControlTest.java | 10 +- .../webserver/Http2WebServerStopIdleTest.java | 2 +- .../test/resources/logging-test.properties | 2 - 16 files changed, 285 insertions(+), 366 deletions(-) diff --git a/nima/http2/http2/pom.xml b/nima/http2/http2/pom.xml index 02ef0700952..36b5495c78c 100644 --- a/nima/http2/http2/pom.xml +++ b/nima/http2/http2/pom.xml @@ -56,6 +56,11 @@ helidon-logging-jul test + + org.mockito + mockito-core + test + diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java index 9d49c21e079..e26bdd2b248 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/ConnectionFlowControl.java @@ -125,7 +125,7 @@ public void resetMaxFrameSize(int maxFrameSize) { */ public void resetInitialWindowSize(int initialWindowSize) { if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { - LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR *: Recv INIT_WINDOW_SIZE %s", type, initialWindowSize)); + LOGGER_OUTBOUND.log(DEBUG, String.format("%s OFC STR *: Recv INIT_WINDOW_SIZE %s", type, initialWindowSize)); } this.initialWindowSize = initialWindowSize; } diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java index f6b3bfabcf8..14f5d29c75f 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/FlowControlImpl.java @@ -50,10 +50,6 @@ public int getRemainingWindowSize() { ); } - protected int streamId() { - return this.streamId; - } - @Override public String toString() { return "FlowControlImpl{" @@ -63,6 +59,10 @@ public String toString() { + '}'; } + protected int streamId() { + return this.streamId; + } + static class Inbound extends FlowControlImpl implements FlowControl.Inbound { private final WindowSize.Inbound connectionWindowSize; @@ -151,6 +151,7 @@ WindowSize streamWindowSize() { return streamWindowSize; } + @Override public void decrementWindowSize(int decrement) { long strRemaining = streamWindowSize().decrementWindowSize(decrement); if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { diff --git a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java index 3bc8e4859e2..58f27cde1e0 100644 --- a/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java +++ b/nima/http2/http2/src/main/java/io/helidon/nima/http2/WindowSizeImpl.java @@ -53,8 +53,8 @@ public void resetWindowSize(int size) { remainingWindowSize.updateAndGet(o -> o + size - windowSize); windowSize = size; if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { - LOGGER_OUTBOUND.log(DEBUG, () -> String.format("%s OFC STR %d: Recv INITIAL_WINDOW_SIZE %d(%d)", - type, streamId, windowSize, remainingWindowSize.get())); + LOGGER_OUTBOUND.log(DEBUG, String.format("%s OFC STR %d: Recv INITIAL_WINDOW_SIZE %d(%d)", + type, streamId, windowSize, remainingWindowSize.get())); } } diff --git a/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java b/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java index ba9fccd9c09..d2c3e247a4a 100644 --- a/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java +++ b/nima/http2/http2/src/test/java/io/helidon/nima/http2/Http2HeadersTest.java @@ -28,6 +28,7 @@ import io.helidon.nima.http2.Http2Headers.HeaderRecord; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; @@ -256,80 +257,12 @@ private Http2Headers headers(String hexEncoded, DynamicTable dynamicTable) { Http2FrameTypes.HEADERS, Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS), 1); - return Http2Headers.create(stream(), + + Http2Stream stream = Mockito.mock(Http2Stream.class); + + return Http2Headers.create(stream, dynamicTable, new Http2HuffmanDecoder(), new Http2FrameData(header, data)); } - - private Http2Stream stream() { - return new Http2Stream() { - - @Override - public void rstStream(Http2RstStream rstStream) { - - } - - @Override - public void windowUpdate(Http2WindowUpdate windowUpdate) { - - } - - @Override - public void headers(Http2Headers headers, boolean endOfStream) { - - } - - @Override - public void data(Http2FrameHeader header, BufferData data) { - - } - - @Override - public void priority(Http2Priority http2Priority) { - - } - - @Override - public int streamId() { - return 1; - } - - @Override - public Http2StreamState streamState() { - return Http2StreamState.IDLE; - } - - @Override - public StreamFlowControl flowControl() { - return null; - } - }; - } - - private static class DevNullWriter implements Http2StreamWriter { - - @Override - public void write(Http2FrameData frame) { - } - - @Override - public void writeData(Http2FrameData frame, FlowControl.Outbound flowControl) { - - } - - @Override - public int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlags flags, FlowControl.Outbound flowControl) { - return 0; - } - - @Override - public int writeHeaders(Http2Headers headers, - int streamId, - Http2Flag.HeaderFlags flags, - Http2FrameData dataFrame, - FlowControl.Outbound flowControl) { - return 0; - } - } } \ No newline at end of file diff --git a/nima/http2/http2/src/test/java/io/helidon/nima/http2/MaxFrameSizeSplitTest.java b/nima/http2/http2/src/test/java/io/helidon/nima/http2/MaxFrameSizeSplitTest.java index 6b07d46648b..badecf1fa0a 100644 --- a/nima/http2/http2/src/test/java/io/helidon/nima/http2/MaxFrameSizeSplitTest.java +++ b/nima/http2/http2/src/test/java/io/helidon/nima/http2/MaxFrameSizeSplitTest.java @@ -25,7 +25,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import static java.lang.System.Logger.Level.DEBUG; @@ -45,38 +44,33 @@ static void beforeAll() { LogConfig.configureRuntime(); } - private static Stream splitMultiple() { - return Stream.of( - Arguments.of(17, 1, 16), - Arguments.of(16, 1, 16), - Arguments.of(15, 2, 1), - Arguments.of(14, 2, 2), - Arguments.of(13, 2, 3), - Arguments.of(12, 2, 4), - Arguments.of(11, 2, 5), - Arguments.of(10, 2, 6), - Arguments.of(9, 2, 7), - Arguments.of(8, 2, 8), - Arguments.of(7, 3, 2), - Arguments.of(6, 3, 4), - Arguments.of(5, 4, 1), - Arguments.of(4, 4, 4), - Arguments.of(3, 6, 1), - Arguments.of(2, 8, 2), - Arguments.of(1, 16, 1) + private static Stream splitMultiple() { + return Stream.of(new SplitTest(17, 1, 16), + new SplitTest(16, 1, 16), + new SplitTest(15, 2, 1), + new SplitTest(14, 2, 2), + new SplitTest(13, 2, 3), + new SplitTest(12, 2, 4), + new SplitTest(11, 2, 5), + new SplitTest(10, 2, 6), + new SplitTest(9, 2, 7), + new SplitTest(8, 2, 8), + new SplitTest(7, 3, 2), + new SplitTest(6, 3, 4), + new SplitTest(5, 4, 1), + new SplitTest(4, 4, 4), + new SplitTest(3, 6, 1), + new SplitTest(2, 8, 2), + new SplitTest(1, 16, 1) ); } @ParameterizedTest @MethodSource - void splitMultiple(int sizeOfFrames, - int numberOfFrames, - int sizeOfLastFrame) { - LOGGER.log(DEBUG, "Splitting " + Arrays.toString(TEST_DATA) + " to frames of max size " + sizeOfFrames); - + void splitMultiple(SplitTest args) { Http2FrameData frameData = createFrameData(TEST_DATA); - Http2FrameData[] split = frameData.split(sizeOfFrames); - assertThat("Unexpected number of frames", split.length, is(numberOfFrames)); + Http2FrameData[] split = frameData.split(args.sizeOfFrames()); + assertThat("Unexpected number of frames", split.length, is(args.numberOfFrames())); BufferData joined = Stream.of(split) .collect(() -> BufferData.create(TEST_DATA.length), @@ -89,9 +83,9 @@ void splitMultiple(int sizeOfFrames, is(TEST_STRING)); // Reload data depleted by previous test - split = createFrameData(TEST_DATA).split(sizeOfFrames); + split = createFrameData(TEST_DATA).split(args.sizeOfFrames()); - for (int i = 0; i < numberOfFrames - 1; i++) { + for (int i = 0; i < args.numberOfFrames() - 1; i++) { Http2FrameData frame = split[i]; assertThat("Only last frame can have endOfStream flag", frame.header().flags(Http2FrameTypes.DATA).endOfStream(), @@ -99,17 +93,17 @@ void splitMultiple(int sizeOfFrames, byte[] bytes = toBytes(frame); LOGGER.log(DEBUG, i + ". frame: " + Arrays.toString(bytes)); - assertThat("Unexpected size of frame " + i, bytes.length, is(sizeOfFrames)); + assertThat("Unexpected size of frame " + i, bytes.length, is(args.sizeOfFrames())); } - Http2FrameData lastFrame = split[numberOfFrames - 1]; + Http2FrameData lastFrame = split[args.numberOfFrames() - 1]; assertThat("Last frame is missing endOfStream flag", lastFrame.header().flags(Http2FrameTypes.DATA).endOfStream(), is(true)); byte[] bytes = toBytes(lastFrame); - LOGGER.log(DEBUG, numberOfFrames - 1 + ". frame: " + Arrays.toString(bytes)); - assertThat("Unexpected size of the last frame", bytes.length, is(sizeOfLastFrame)); + LOGGER.log(DEBUG, args.numberOfFrames() - 1 + ". frame: " + Arrays.toString(bytes)); + assertThat("Unexpected size of the last frame", bytes.length, is(args.sizeOfLastFrame())); } private Http2FrameData createFrameData(byte[] data) { @@ -129,4 +123,10 @@ private byte[] toBytes(BufferData data) { data.read(b); return b; } + + private record SplitTest(int sizeOfFrames, + int numberOfFrames, + int sizeOfLastFrame) { + + } } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java index 6cb90fd8952..6ba8c32c75a 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientConnection.java @@ -72,12 +72,7 @@ class Http2ClientConnection { private static final System.Logger LOGGER = System.getLogger(Http2ClientConnection.class.getName()); - - private final Http2FrameListener sendListener = new Http2LoggingFrameListener("cl-send"); - private final Http2FrameListener recvListener = new Http2LoggingFrameListener("cl-recv"); - private static final int FRAME_HEADER_LENGTH = 9; - private static final String UPGRADE_REQ_MASK = """ %s %s HTTP/1.1\r Host: %s:%s\r @@ -87,7 +82,8 @@ class Http2ClientConnection { """; private static final byte[] PRIOR_KNOWLEDGE_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(StandardCharsets.UTF_8); - + private final Http2FrameListener sendListener = new Http2LoggingFrameListener("cl-send"); + private final Http2FrameListener recvListener = new Http2LoggingFrameListener("cl-recv"); private final ExecutorService executor; private final SocketOptions socketOptions; private final ConnectionKey connectionKey; @@ -96,18 +92,16 @@ class Http2ClientConnection { private final Map streams = new HashMap<>(); private final ConnectionFlowControl connectionFlowControl; private final ConnectionContext connectionContext; - + private final Http2Headers.DynamicTable inboundDynamicTable = + Http2Headers.DynamicTable.create(Http2Setting.HEADER_TABLE_SIZE.defaultValue()); private Http2Settings serverSettings = Http2Settings.builder() .build(); - private String channelId; private Socket socket; private PlainSocket helidonSocket; private Http2ConnectionWriter writer; private DataReader reader; private DataWriter dataWriter; - private final Http2Headers.DynamicTable inboundDynamicTable = - Http2Headers.DynamicTable.create(Http2Setting.HEADER_TABLE_SIZE.defaultValue()); private Future handleTask; Http2ClientConnection(ExecutorService executor, @@ -127,8 +121,16 @@ class Http2ClientConnection { .build(); } - void writeWindowsUpdate(int streamId, Http2WindowUpdate windowUpdateFrame) { - writer.write(windowUpdateFrame.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create())); + Http2ConnectionWriter writer() { + return writer; + } + + Http2Headers.DynamicTable getInboundDynamicTable() { + return this.inboundDynamicTable; + } + + ConnectionFlowControl flowControl() { + return this.connectionFlowControl; } Http2ClientConnection connect() { @@ -148,6 +150,48 @@ Http2ClientConnection connect() { return this; } + Http2ClientStream stream(int streamId) { + return streams.get(streamId); + } + + Http2ClientStream createStream(int priority) { + //FIXME: priority + return new Http2ClientStream(this, + serverSettings, + helidonSocket, + connectionContext, + streamIdSeq); + } + + void addStream(int streamId, Http2ClientStream stream) { + this.streams.put(streamId, stream); + } + + void removeStream(int streamId) { + this.streams.remove(streamId); + } + + Http2ClientStream tryStream(int priority) { + try { + return createStream(priority); + } catch (IllegalStateException | UncheckedIOException e) { + return null; + } + } + + void close() { + try { + handleTask.cancel(true); + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void writeWindowsUpdate(int streamId, Http2WindowUpdate windowUpdateFrame) { + writer.write(windowUpdateFrame.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create())); + } + private void handle() { this.reader.ensureAvailable(); BufferData frameHeaderBuffer = this.reader.readBuffer(FRAME_HEADER_LENGTH); @@ -196,7 +240,7 @@ private void handle() { } // §6.5.3 Settings Synchronization ackSettings(); - //FIXME: Other settings + //FIXME: Max number of concurrent streams return; case WINDOW_UPDATE: @@ -218,7 +262,6 @@ private void handle() { writer.write(frame.toFrameData(serverSettings, 0, Http2Flag.NoFlags.create())); } - } else { stream(streamId) .windowUpdate(windowUpdate); @@ -267,44 +310,6 @@ private void handle() { } - Http2ClientStream stream(int streamId) { - return streams.get(streamId); - } - - Http2ClientStream createStream(int priority) { - //FIXME: priority - return new Http2ClientStream(this, - serverSettings, - helidonSocket, - connectionContext, - streamIdSeq); - } - - void addStream(int streamId, Http2ClientStream stream){ - this.streams.put(streamId, stream); - } - - void removeStream(int streamId) { - this.streams.remove(streamId); - } - - Http2ClientStream tryStream(int priority) { - try { - return createStream(priority); - } catch (IllegalStateException | UncheckedIOException e) { - return null; - } - } - - void close() { - try { - handleTask.cancel(true); - socket.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - private void doConnect() throws IOException { boolean useTls = "https".equals(connectionKey.scheme()) && connectionKey.tls() != null; @@ -373,7 +378,7 @@ private void goAway(int streamId, Http2ErrorCode errorCode, String msg) { writer.write(frame.toFrameData(http2Settings, 0, Http2Flag.NoFlags.create())); } - private void sendPreface(boolean sendSettings){ + private void sendPreface(boolean sendSettings) { dataWriter.writeNow(BufferData.create(PRIOR_KNOWLEDGE_PREFACE)); if (sendSettings) { // §3.5 Preface bytes must be followed by setting frame @@ -412,13 +417,13 @@ private void httpUpgrade() { String message = reader.readLine(); WritableHeaders headers = Http1HeadersParser.readHeaders(reader, 8192, false); switch (status) { - case "101": - break; - case "301": - throw new UpgradeRedirectException(headers.get(Http.Header.LOCATION).value()); - default: - close(); - throw new IllegalStateException("Upgrade to HTTP/2 failed: " + message); + case "101": + break; + case "301": + throw new UpgradeRedirectException(headers.get(Http.Header.LOCATION).value()); + default: + close(); + throw new IllegalStateException("Upgrade to HTTP/2 failed: " + message); } } @@ -485,16 +490,4 @@ private String certsToString(Certificate[] peerCertificates) { return String.join(", ", certs); } - - public Http2ConnectionWriter getWriter() { - return writer; - } - - public Http2Headers.DynamicTable getInboundDynamicTable() { - return this.inboundDynamicTable; - } - - ConnectionFlowControl flowControl(){ - return this.connectionFlowControl; - } } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientImpl.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientImpl.java index 3bc7e167cd0..def89bde5e4 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientImpl.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientImpl.java @@ -62,7 +62,7 @@ boolean priorKnowledge() { return priorKnowledge; } - public int maxFrameSize() { + int maxFrameSize() { return this.maxFrameSize; } } diff --git a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java index 6eb17926cbc..088aa8b173a 100644 --- a/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java +++ b/nima/http2/webclient/src/main/java/io/helidon/nima/http2/webclient/Http2ClientStream.java @@ -83,6 +83,78 @@ class Http2ClientStream implements Http2Stream { this.buffer = new StreamBuffer(streamId, connectionContext.timeout()); } + @Override + public int streamId() { + return streamId; + } + + @Override + public Http2StreamState streamState() { + return state; + } + + @Override + public void headers(Http2Headers headers, boolean endOfStream) { + currentHeaders = headers; + } + + @Override + public void rstStream(Http2RstStream rstStream) { + if (state == Http2StreamState.IDLE) { + throw new Http2Exception(Http2ErrorCode.PROTOCOL, + "Received RST_STREAM for stream " + + streamId + " in IDLE state"); + } + this.state = Http2StreamState.checkAndGetState(this.state, + Http2FrameType.RST_STREAM, + false, + false, + false); + + throw new RuntimeException("Reset of " + streamId + " stream received!"); + } + + @Override + public void windowUpdate(Http2WindowUpdate windowUpdate) { + this.state = Http2StreamState.checkAndGetState(this.state, + Http2FrameType.WINDOW_UPDATE, + false, + false, + false); + + int increment = windowUpdate.windowSizeIncrement(); + + //6.9/2 + if (increment == 0) { + Http2RstStream frame = new Http2RstStream(Http2ErrorCode.PROTOCOL); + connection.writer().write(frame.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create())); + } + //6.9.1/3 + if (flowControl.outbound().incrementStreamWindowSize(increment) > WindowSize.MAX_WIN_SIZE) { + Http2RstStream frame = new Http2RstStream(Http2ErrorCode.FLOW_CONTROL); + connection.writer().write(frame.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create())); + } + + flowControl() + .outbound() + .incrementStreamWindowSize(increment); + } + + @Override + public void data(Http2FrameHeader header, BufferData data) { + flowControl.inbound().incrementWindowSize(header.length()); + } + + @Override + public void priority(Http2Priority http2Priority) { + //FIXME: priority + } + + @Override + public StreamFlowControl flowControl() { + return flowControl; + } + void cancel() { Http2RstStream rstStream = new Http2RstStream(Http2ErrorCode.CANCEL); Http2FrameData frameData = rstStream.toFrameData(settings, streamId, Http2Flag.NoFlags.create()); @@ -115,46 +187,6 @@ void push(Http2FrameData frameData) { buffer.push(frameData); } - private Http2FrameData readOne() { - Http2FrameData frameData = buffer.poll(); - - if (frameData != null) { - - recvListener.frameHeader(ctx, frameData.header()); - recvListener.frame(ctx, frameData.data()); - - int flags = frameData.header().flags(); - boolean endOfStream = (flags & Http2Flag.END_OF_STREAM) == Http2Flag.END_OF_STREAM; - boolean endOfHeaders = (flags & Http2Flag.END_OF_HEADERS) == Http2Flag.END_OF_HEADERS; - - this.state = Http2StreamState.checkAndGetState(this.state, - frameData.header().type(), - false, - endOfStream, - endOfHeaders); - - switch (frameData.header().type()) { - case DATA: - data(frameData.header(), frameData.data()); - return frameData; - case HEADERS, CONTINUATION: - continuationData.add(frameData); - if (endOfHeaders) { - var requestHuffman = new Http2HuffmanDecoder(); - Http2Headers http2Headers = Http2Headers.create(this, - connection.getInboundDynamicTable(), - requestHuffman, - continuationData.toArray(new Http2FrameData[0])); - this.headers(http2Headers, endOfStream); - } - break; - default: - LOGGER.log(DEBUG, "Dropping frame " + frameData.header() + " expected header or data."); - } - } - return null; - } - BufferData read(int i) { while (state == Http2StreamState.HALF_CLOSED_LOCAL) { Http2FrameData frameData = readOne(); @@ -183,7 +215,7 @@ void write(Http2Headers http2Headers, boolean endOfStream) { this.flowControl = connection.flowControl().createStreamFlowControl(streamId); this.connection.addStream(streamId, this); // First call to the server-starting stream, needs to be increasing sequence of odd numbers - connection.getWriter().writeHeaders(http2Headers, streamId, flags, flowControl.outbound()); + connection.writer().writeHeaders(http2Headers, streamId, flags, flowControl.outbound()); } finally { streamIdSeq.unlock(); } @@ -200,16 +232,6 @@ void writeData(BufferData entityBytes, boolean endOfStream) { splitAndWrite(frameData); } - private void splitAndWrite(Http2FrameData frameData) { - int maxFrameSize = this.serverSettings.value(Http2Setting.MAX_FRAME_SIZE).intValue(); - - // Split to frames if bigger than max frame size - Http2FrameData[] frames = frameData.split(maxFrameSize); - for (Http2FrameData frame : frames) { - write(frame, frame.header().flags(Http2FrameTypes.DATA).endOfStream()); - } - } - Http2Headers readHeaders() { while (currentHeaders == null) { Http2FrameData frameData = readOne(); @@ -224,86 +246,64 @@ ClientOutputStream outputStream() { return new ClientOutputStream(); } - private void write(Http2FrameData frameData, boolean endOfStream) { - this.state = Http2StreamState.checkAndGetState(this.state, - frameData.header().type(), - true, - endOfStream, - false); - connection.getWriter().writeData(frameData, - flowControl().outbound()); - } + private Http2FrameData readOne() { + Http2FrameData frameData = buffer.poll(); - @Override - public void rstStream(Http2RstStream rstStream) { - if (state == Http2StreamState.IDLE) { - throw new Http2Exception(Http2ErrorCode.PROTOCOL, - "Received RST_STREAM for stream " - + streamId + " in IDLE state"); - } - this.state = Http2StreamState.checkAndGetState(this.state, - Http2FrameType.RST_STREAM, - false, - false, - false); + if (frameData != null) { - throw new RuntimeException("Reset of " + streamId + " stream received!"); - } + recvListener.frameHeader(ctx, frameData.header()); + recvListener.frame(ctx, frameData.data()); - @Override - public void windowUpdate(Http2WindowUpdate windowUpdate) { - this.state = Http2StreamState.checkAndGetState(this.state, - Http2FrameType.WINDOW_UPDATE, - false, - false, - false); + int flags = frameData.header().flags(); + boolean endOfStream = (flags & Http2Flag.END_OF_STREAM) == Http2Flag.END_OF_STREAM; + boolean endOfHeaders = (flags & Http2Flag.END_OF_HEADERS) == Http2Flag.END_OF_HEADERS; - int increment = windowUpdate.windowSizeIncrement(); + this.state = Http2StreamState.checkAndGetState(this.state, + frameData.header().type(), + false, + endOfStream, + endOfHeaders); - //6.9/2 - if (increment == 0) { - Http2RstStream frame = new Http2RstStream(Http2ErrorCode.PROTOCOL); - connection.getWriter().write(frame.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create())); - } - //6.9.1/3 - if (flowControl.outbound().incrementStreamWindowSize(increment) > WindowSize.MAX_WIN_SIZE) { - Http2RstStream frame = new Http2RstStream(Http2ErrorCode.FLOW_CONTROL); - connection.getWriter().write(frame.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create())); + switch (frameData.header().type()) { + case DATA: + data(frameData.header(), frameData.data()); + return frameData; + case HEADERS, CONTINUATION: + continuationData.add(frameData); + if (endOfHeaders) { + var requestHuffman = new Http2HuffmanDecoder(); + Http2Headers http2Headers = Http2Headers.create(this, + connection.getInboundDynamicTable(), + requestHuffman, + continuationData.toArray(new Http2FrameData[0])); + this.headers(http2Headers, endOfStream); + } + break; + default: + LOGGER.log(DEBUG, "Dropping frame " + frameData.header() + " expected header or data."); + } } - - flowControl() - .outbound() - .incrementStreamWindowSize(increment); - } - - @Override - public void headers(Http2Headers headers, boolean endOfStream) { - currentHeaders = headers; - } - - @Override - public void data(Http2FrameHeader header, BufferData data) { - flowControl.inbound().incrementWindowSize(header.length()); - } - - @Override - public void priority(Http2Priority http2Priority) { - //FIXME: priority + return null; } - @Override - public int streamId() { - return streamId; - } + private void splitAndWrite(Http2FrameData frameData) { + int maxFrameSize = this.serverSettings.value(Http2Setting.MAX_FRAME_SIZE).intValue(); - @Override - public Http2StreamState streamState() { - return state; + // Split to frames if bigger than max frame size + Http2FrameData[] frames = frameData.split(maxFrameSize); + for (Http2FrameData frame : frames) { + write(frame, frame.header().flags(Http2FrameTypes.DATA).endOfStream()); + } } - @Override - public StreamFlowControl flowControl() { - return flowControl; + private void write(Http2FrameData frameData, boolean endOfStream) { + this.state = Http2StreamState.checkAndGetState(this.state, + frameData.header().type(), + true, + endOfStream, + false); + connection.writer().writeData(frameData, + flowControl().outbound()); } class ClientOutputStream extends OutputStream { diff --git a/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/Http2WebClientTest.java b/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/Http2WebClientTest.java index 3cb99d25886..191379ba321 100644 --- a/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/Http2WebClientTest.java +++ b/nima/http2/webclient/src/test/java/io/helidon/nima/http2/webclient/Http2WebClientTest.java @@ -148,7 +148,7 @@ static void setUpServer(WebServer.Builder serverBuilder) { ); } - private static Stream clientTypes() { + static Stream clientTypes() { return Stream.of( Arguments.of("priorKnowledge", priorKnowledgeClient), Arguments.of("upgrade", upgradeClient), diff --git a/nima/http2/webclient/src/test/resources/logging-test.properties b/nima/http2/webclient/src/test/resources/logging-test.properties index ebb86e90e96..f9bd523dd3a 100644 --- a/nima/http2/webclient/src/test/resources/logging-test.properties +++ b/nima/http2/webclient/src/test/resources/logging-test.properties @@ -16,7 +16,6 @@ # Send messages to the console handlers=java.util.logging.ConsoleHandler - java.util.logging.ConsoleHandler.level=INFO java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter # java.util.logging.SimpleFormatter.format = [%1$tc] %5$s %6$s%n @@ -24,8 +23,5 @@ java.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS %5$s%6$s%n #java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s %5$s%6$s%n io.helidon.nima.level=INFO -io.helidon.nima.http2.WindowSizeImpl.level=ALL -io.helidon.nima.http2.FlowControl.level=ALL -io.helidon.nima.http2.FlowControlImpl.level=ALL # Global logging level. Can be overridden by specific loggers .level=INFO diff --git a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java index ccf2cc6b002..1263207ebfb 100644 --- a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java +++ b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/ClientFlowControlTest.java @@ -51,7 +51,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -public class ClientFlowControlTest { +class ClientFlowControlTest { private static final System.Logger LOGGER = System.getLogger(ClientFlowControlTest.class.getName()); private static final Duration TIMEOUT = Duration.ofSeconds(10); @@ -106,6 +106,20 @@ static void beforeAll() throws ExecutionException, InterruptedException, Timeout .build(); } + @AfterAll + static void afterAll() { + server.close(); + vertx.close(); + exec.shutdown(); + try { + if (!exec.awaitTermination(TIMEOUT.toMillis(), MILLISECONDS)) { + exec.shutdownNow(); + } + } catch (InterruptedException e) { + exec.shutdownNow(); + } + } + @Test void clientOutbound() throws InterruptedException, ExecutionException, TimeoutException { outboundTestServerRequestRef = new CompletableFuture<>(); @@ -206,18 +220,4 @@ private void awaitSize(String msg, AtomicLong actualSize, long expected) } assertThat(msg, actualSize.get(), is(expected)); } - - @AfterAll - static void afterAll() { - server.close(); - vertx.close(); - exec.shutdown(); - try { - if (!exec.awaitTermination(TIMEOUT.toMillis(), MILLISECONDS)) { - exec.shutdownNow(); - } - } catch (InterruptedException e) { - exec.shutdownNow(); - } - } } diff --git a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/HeadersTest.java b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/HeadersTest.java index b99f2432549..1d93aa5d603 100644 --- a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/HeadersTest.java +++ b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/HeadersTest.java @@ -42,7 +42,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; -public class HeadersTest { +class HeadersTest { private static final Duration TIMEOUT = Duration.ofSeconds(10); private static final String DATA = "Helidon!!!".repeat(10); @@ -58,20 +58,20 @@ static void beforeAll() throws ExecutionException, InterruptedException, Timeout .requestHandler(req -> { HttpServerResponse res = req.response(); switch (req.path()) { - case "/trailer" -> { - res.putHeader("test", "before"); - res.write(DATA); - res.putTrailer("Trailer-header", "trailer-test"); - res.end(); - } - case "/cont" -> { - for (int i = 0; i < 500; i++) { - res.headers().add("test-header-" + i, DATA); - } - res.write(DATA); - res.end(); + case "/trailer" -> { + res.putHeader("test", "before"); + res.write(DATA); + res.putTrailer("Trailer-header", "trailer-test"); + res.end(); + } + case "/cont" -> { + for (int i = 0; i < 500; i++) { + res.headers().add("test-header-" + i, DATA); } - default -> res.setStatusCode(404).end(); + res.write(DATA); + res.end(); + } + default -> res.setStatusCode(404).end(); } }) .listen(0) @@ -82,8 +82,23 @@ static void beforeAll() throws ExecutionException, InterruptedException, Timeout port = server.actualPort(); } + @AfterAll + static void afterAll() { + server.close(); + vertx.close(); + exec.shutdown(); + try { + if (!exec.awaitTermination(TIMEOUT.toMillis(), MILLISECONDS)) { + exec.shutdownNow(); + } + } catch (InterruptedException e) { + exec.shutdownNow(); + } + } + @Test - @Disabled//FIXME: trailer headers are not implemented yet + //FIXME: trailer headers are not implemented yet + @Disabled void trailerHeader() { try (Http2ClientResponse res = WebClient.builder(Http2.PROTOCOL) .baseUri("http://localhost:" + port + "/") @@ -118,18 +133,4 @@ void continuation() { assertThat(res.as(String.class), is(DATA)); } } - - @AfterAll - static void afterAll() { - server.close(); - vertx.close(); - exec.shutdown(); - try { - if (!exec.awaitTermination(TIMEOUT.toMillis(), MILLISECONDS)) { - exec.shutdownNow(); - } - } catch (InterruptedException e) { - exec.shutdownNow(); - } - } } diff --git a/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java index fb449e2efc1..7606a94cdd1 100644 --- a/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java +++ b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/FlowControlTest.java @@ -55,7 +55,7 @@ import static org.hamcrest.Matchers.lessThan; @ServerTest -public class FlowControlTest { +class FlowControlTest { private static final System.Logger LOGGER = System.getLogger(FlowControlTest.class.getName()); private static final ExecutorService exec = Executors.newCachedThreadPool(); @@ -253,12 +253,4 @@ void flowControlHttpClientInOut() throws ExecutionException, InterruptedExceptio assertThat(sentData.get(), is(100_000L)); assertThat(response, is(EXPECTED)); } - - // @Test - void name() { - flowControlServerLatch = new CompletableFuture<>(); - flowControlClientLatch = new CompletableFuture<>(); - flowControlServerLatch.complete(null); - new CompletableFuture().join(); - } } diff --git a/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/Http2WebServerStopIdleTest.java b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/Http2WebServerStopIdleTest.java index 810ebd5cb4d..8377564bbec 100644 --- a/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/Http2WebServerStopIdleTest.java +++ b/nima/tests/integration/http2/server/src/test/java/io/helidon/nima/tests/integration/http2/webserver/Http2WebServerStopIdleTest.java @@ -79,6 +79,6 @@ void stopWhenIdleExpectTimelyStopHttp2() throws IOException, InterruptedExceptio long startMillis = System.currentTimeMillis(); webServer.stop(); int stopExecutionTimeInMillis = (int) (System.currentTimeMillis() - startMillis); - assertThat(stopExecutionTimeInMillis, is(lessThan(500))); + assertThat(stopExecutionTimeInMillis, is(lessThan(550))); } } diff --git a/nima/tests/integration/http2/server/src/test/resources/logging-test.properties b/nima/tests/integration/http2/server/src/test/resources/logging-test.properties index e408b5015a1..aff4b62fc13 100644 --- a/nima/tests/integration/http2/server/src/test/resources/logging-test.properties +++ b/nima/tests/integration/http2/server/src/test/resources/logging-test.properties @@ -16,9 +16,7 @@ handlers=java.util.logging.ConsoleHandler java.util.logging.ConsoleHandler.level=FINEST java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter -#java.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS %5$s%6$s%n java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n # Global logging level. Can be overridden by specific loggers -#io.helidon.nima.http2.FlowControl.level=ALL .level=INFO io.helidon.nima.level=INFO From 872bf9aa50412733c5f47ea0c4f032863ffdded7 Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Tue, 4 Apr 2023 08:21:01 +0200 Subject: [PATCH 12/17] HTTP/2 Client - review issues 6 --- .../http2/client/Http2ClientTest.java | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/Http2ClientTest.java b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/Http2ClientTest.java index cf2be1b77cd..18b01b42ea2 100644 --- a/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/Http2ClientTest.java +++ b/nima/tests/integration/http2/client/src/test/java/io/helidon/nima/tests/integration/http2/client/Http2ClientTest.java @@ -42,7 +42,7 @@ @ServerTest class Http2ClientTest { - static final String MESSAGE = "Hello World!"; + private static final String MESSAGE = "Hello World!"; private static final String TEST_HEADER_NAME = "custom_header"; private static final String TEST_HEADER_VALUE = "as!fd"; private static final HeaderValue TEST_HEADER = Header.create(Header.create(TEST_HEADER_NAME), TEST_HEADER_VALUE); @@ -94,44 +94,51 @@ static void router(HttpRouting.Builder router) { @Test void testHttp1() { // make sure the HTTP/1 route is not working + try (Http1ClientResponse response = http1Client + .get("/") + .request()) { - Http1ClientResponse response = http1Client.get("/") - .request(); - - assertThat(response.status(), is(Http.Status.NOT_FOUND_404)); + assertThat(response.status(), is(Http.Status.NOT_FOUND_404)); + } } @Test void testUpgrade() { - Http2ClientResponse response = plainClient.get("/") - .request(); - - assertThat(response.status(), is(Http.Status.OK_200)); - assertThat(response.as(String.class), is(MESSAGE)); - assertThat(TEST_HEADER + " header must be present in response", - response.headers().contains(TEST_HEADER), is(true)); + try (Http2ClientResponse response = plainClient + .get("/") + .request()) { + + assertThat(response.status(), is(Http.Status.OK_200)); + assertThat(response.as(String.class), is(MESSAGE)); + assertThat(TEST_HEADER + " header must be present in response", + response.headers().contains(TEST_HEADER), is(true)); + } } @Test void testAppProtocol() { - Http2ClientResponse response = tlsClient.get("/") - .request(); - - assertThat(response.status(), is(Http.Status.OK_200)); - assertThat(response.as(String.class), is(MESSAGE)); - assertThat(TEST_HEADER + " header must be present in response", - response.headers().contains(TEST_HEADER), is(true)); + try (Http2ClientResponse response = tlsClient + .get("/") + .request()) { + + assertThat(response.status(), is(Http.Status.OK_200)); + assertThat(response.as(String.class), is(MESSAGE)); + assertThat(TEST_HEADER + " header must be present in response", + response.headers().contains(TEST_HEADER), is(true)); + } } @Test void testPriorKnowledge() { - Http2ClientResponse response = tlsClient.get("/") + try (Http2ClientResponse response = tlsClient + .get("/") .priorKnowledge(true) - .request(); + .request()) { - assertThat(response.status(), is(Http.Status.OK_200)); - assertThat(response.as(String.class), is(MESSAGE)); - assertThat(TEST_HEADER + " header must be present in response", - response.headers().contains(TEST_HEADER), is(true)); + assertThat(response.status(), is(Http.Status.OK_200)); + assertThat(response.as(String.class), is(MESSAGE)); + assertThat(TEST_HEADER + " header must be present in response", + response.headers().contains(TEST_HEADER), is(true)); + } } } From f87eb8246e25173ff769b43f624270a56b9ee437 Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Tue, 4 Apr 2023 08:30:49 +0200 Subject: [PATCH 13/17] HTTP/2 Client - review issues 7 --- .../server/src/main/java/module-info.java | 17 ------------- .../server/src/test/java/module-info.java | 25 ------------------- 2 files changed, 42 deletions(-) delete mode 100644 nima/tests/integration/http2/server/src/main/java/module-info.java delete mode 100644 nima/tests/integration/http2/server/src/test/java/module-info.java diff --git a/nima/tests/integration/http2/server/src/main/java/module-info.java b/nima/tests/integration/http2/server/src/main/java/module-info.java deleted file mode 100644 index 187e694572b..00000000000 --- a/nima/tests/integration/http2/server/src/main/java/module-info.java +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed 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. - */ -module helidon.nima.tests.integration.http2.webserver { -} \ No newline at end of file diff --git a/nima/tests/integration/http2/server/src/test/java/module-info.java b/nima/tests/integration/http2/server/src/test/java/module-info.java deleted file mode 100644 index 59320d44af8..00000000000 --- a/nima/tests/integration/http2/server/src/test/java/module-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2023 Oracle and/or its affiliates. - * - * Licensed 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. - */ -open module helidon.nima.tests.integration.http2.webserver { - requires java.logging; - requires java.net.http; - requires hamcrest.all; - requires org.junit.jupiter.api; - requires io.helidon.nima.http2.webserver; - requires io.helidon.nima.testing.junit5.webserver; - requires io.helidon.common.reactive; - requires io.helidon.nima.http2.webclient; -} \ No newline at end of file From 981a9d72b05d26c102c1b8837be1fb483ea8db51 Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Tue, 4 Apr 2023 08:44:59 +0200 Subject: [PATCH 14/17] HTTP/2 Client - review issues 8 --- .../nima/http2/webserver/Http2Connection.java | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java index 1e561bd4826..8e8ab3922fa 100755 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java @@ -709,23 +709,12 @@ private StreamContext stream(int streamId) { } } - // MAX_CONCURRENT_STREAMS limit check - according to RFC 9113 section 5.1.2 endpoint MUST treat this - // as a stream error (section 5.4.2) of type PROTOCOL_ERROR or REFUSED_STREAM. + // 5.1.2 MAX_CONCURRENT_STREAMS limit check - stream error of type PROTOCOL_ERROR or REFUSED_STREAM if (streams.size() > maxClientConcurrentStreams) { throw new Http2Exception(Http2ErrorCode.REFUSED_STREAM, "Maximum concurrent streams limit " + maxClientConcurrentStreams + " exceeded"); } - // Pass NOOP when flow control is turned off - // FlowControl.Inbound.Builder inboundFlowControlBuilder = http2Config.flowControlEnabled() - // ? FlowControl.builderInbound(FlowControl.Type.SERVER) - // .streamId(streamId) - // .connectionWindowSize(inboundWindowSize) - // .streamWindowSize(inboundInitialWindowSize) - // .streamMaxFrameSize(http2Config.maxFrameSize()) - // // Pass NOOP when flow control is turned off (but we still have to send WINDOW_UPDATE frames) - // : FlowControl.builderInbound(FlowControl.Type.SERVER) - // .connectionWindowSize(inboundWindowSize) - // .noop(); + streamContext = new StreamContext(streamId, new Http2Stream(ctx, routing, From e02c3fecc32928cf78388753428603f423821bad Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Tue, 4 Apr 2023 12:53:15 +0200 Subject: [PATCH 15/17] HTTP/2 Client - review issues 9 --- .../nima/http2/webserver/Http2Config.java | 8 +++----- .../nima/http2/webserver/Http2Connection.java | 3 ++- .../http2/webserver/ConnectionConfigTest.java | 20 +++++++++++++++++++ .../src/test/resources/application.yaml | 1 + 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java index 0dcd3a5d796..5f3f8e2936b 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java @@ -16,8 +16,6 @@ package io.helidon.nima.http2.webserver; -import java.time.Duration; - import io.helidon.builder.Builder; import io.helidon.builder.config.ConfigBean; import io.helidon.common.http.RequestedUriDiscoveryContext; @@ -74,12 +72,12 @@ public interface Http2Config { int initialWindowSize(); /** - * Outbound flow control blocking timeout. + * Outbound flow control blocking timeout in milliseconds. * * @return duration */ - @ConfiguredOption("java.time.Duration.ofMillis(100)") - Duration flowControlTimeout(); + @ConfiguredOption("100") + long flowControlTimeout(); /** * Whether to send error message over HTTP to client. diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java index 8e8ab3922fa..dcb143f5205 100755 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java @@ -16,6 +16,7 @@ package io.helidon.nima.http2.webserver; +import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -126,7 +127,7 @@ public class Http2Connection implements ServerConnection, InterruptableTask Date: Thu, 6 Apr 2023 12:02:29 +0200 Subject: [PATCH 16/17] HTTP/2 Client - review issues 10 --- .../nima/http2/webserver/Http2Config.java | 20 ++++++++++++++++--- .../nima/http2/webserver/Http2Connection.java | 3 +-- .../http2/webserver/ConnectionConfigTest.java | 3 ++- .../src/test/resources/application.yaml | 2 +- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java index 5f3f8e2936b..a7347a18e66 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java @@ -16,6 +16,8 @@ package io.helidon.nima.http2.webserver; +import java.time.Duration; + import io.helidon.builder.Builder; import io.helidon.builder.config.ConfigBean; import io.helidon.common.http.RequestedUriDiscoveryContext; @@ -72,12 +74,24 @@ public interface Http2Config { int initialWindowSize(); /** - * Outbound flow control blocking timeout in milliseconds. + * Outbound flow control blocking timeout configured as {@link java.time.Duration} + * or text in ISO-8601 format. + * Blocking timeout defines an interval to wait for the outbound window size changes(incoming window updates) + * before the next blocking iteration. + * Default value is {@code PT0.1S}. + * + * + * Examples: + * + * + * + *
PT0.1S100 milliseconds
PT0.5S500 milliseconds
PT2S2 seconds
* * @return duration + * @see ISO_8601 Durations */ - @ConfiguredOption("100") - long flowControlTimeout(); + @ConfiguredOption("java.time.Duration.ofMillis(100L)") + Duration flowControlTimeout(); /** * Whether to send error message over HTTP to client. diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java index dcb143f5205..8e8ab3922fa 100755 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Connection.java @@ -16,7 +16,6 @@ package io.helidon.nima.http2.webserver; -import java.time.Duration; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -127,7 +126,7 @@ public class Http2Connection implements ServerConnection, InterruptableTask Date: Thu, 6 Apr 2023 13:00:48 +0200 Subject: [PATCH 17/17] HTTP/2 Client - review issues 11 --- .../main/java/io/helidon/nima/http2/webserver/Http2Config.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java index a7347a18e66..305b80ada7a 100644 --- a/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java +++ b/nima/http2/webserver/src/main/java/io/helidon/nima/http2/webserver/Http2Config.java @@ -81,7 +81,7 @@ public interface Http2Config { * Default value is {@code PT0.1S}. * * - * Examples: + * * * *
ISO_8601 format examples:
PT0.1S100 milliseconds
PT0.5S500 milliseconds
PT2S2 seconds