|
| 1 | +/* |
| 2 | + * Copyright (c) 2023 Oracle and/or its affiliates. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +package io.helidon.nima.http2; |
| 18 | + |
| 19 | +import java.time.Duration; |
| 20 | +import java.util.function.BiConsumer; |
| 21 | + |
| 22 | +import io.helidon.common.Builder; |
| 23 | + |
| 24 | +import static java.lang.System.Logger.Level.DEBUG; |
| 25 | + |
| 26 | +/** |
| 27 | + * HTTP/2 Flow control for connection. |
| 28 | + */ |
| 29 | +public class ConnectionFlowControl { |
| 30 | + |
| 31 | + private static final System.Logger LOGGER_OUTBOUND = System.getLogger(FlowControl.class.getName() + ".ofc"); |
| 32 | + |
| 33 | + private final Type type; |
| 34 | + private final BiConsumer<Integer, Http2WindowUpdate> windowUpdateWriter; |
| 35 | + private final Duration timeout; |
| 36 | + private final WindowSize.Inbound inboundConnectionWindowSize; |
| 37 | + private final WindowSize.Outbound outboundConnectionWindowSize; |
| 38 | + |
| 39 | + private volatile int maxFrameSize = WindowSize.DEFAULT_MAX_FRAME_SIZE; |
| 40 | + private volatile int initialWindowSize = WindowSize.DEFAULT_WIN_SIZE; |
| 41 | + |
| 42 | + private ConnectionFlowControl(Type type, |
| 43 | + int initialWindowSize, |
| 44 | + int maxFrameSize, |
| 45 | + BiConsumer<Integer, Http2WindowUpdate> windowUpdateWriter, |
| 46 | + Duration timeout) { |
| 47 | + this.type = type; |
| 48 | + this.windowUpdateWriter = windowUpdateWriter; |
| 49 | + this.timeout = timeout; |
| 50 | + this.inboundConnectionWindowSize = |
| 51 | + WindowSize.createInbound(type, |
| 52 | + 0, |
| 53 | + initialWindowSize, |
| 54 | + maxFrameSize, |
| 55 | + windowUpdateWriter); |
| 56 | + outboundConnectionWindowSize = |
| 57 | + WindowSize.createOutbound(type, 0, this); |
| 58 | + } |
| 59 | + |
| 60 | + /** |
| 61 | + * Create connection HTTP/2 flow-control for server side. |
| 62 | + * |
| 63 | + * @param windowUpdateWriter method called for sending WINDOW_UPDATE frames to the client. |
| 64 | + * @return Connection HTTP/2 flow-control |
| 65 | + */ |
| 66 | + public static ConnectionFlowControlBuilder serverBuilder(BiConsumer<Integer, Http2WindowUpdate> windowUpdateWriter) { |
| 67 | + return new ConnectionFlowControlBuilder(Type.SERVER, windowUpdateWriter); |
| 68 | + } |
| 69 | + |
| 70 | + /** |
| 71 | + * Create connection HTTP/2 flow-control for client side. |
| 72 | + * |
| 73 | + * @param windowUpdateWriter method called for sending WINDOW_UPDATE frames to the server. |
| 74 | + * @return Connection HTTP/2 flow-control |
| 75 | + */ |
| 76 | + public static ConnectionFlowControlBuilder clientBuilder(BiConsumer<Integer, Http2WindowUpdate> windowUpdateWriter) { |
| 77 | + return new ConnectionFlowControlBuilder(Type.CLIENT, windowUpdateWriter); |
| 78 | + } |
| 79 | + |
| 80 | + /** |
| 81 | + * Create stream specific inbound and outbound flow control. |
| 82 | + * |
| 83 | + * @param streamId stream id |
| 84 | + * @return stream flow control |
| 85 | + */ |
| 86 | + public StreamFlowControl createStreamFlowControl(int streamId) { |
| 87 | + return new StreamFlowControl(type, streamId, this, windowUpdateWriter); |
| 88 | + } |
| 89 | + |
| 90 | + /** |
| 91 | + * Increment outbound connection flow control window, called when WINDOW_UPDATE is received. |
| 92 | + * |
| 93 | + * @param increment number of bytes other side has requested on top of actual demand |
| 94 | + * @return outbound window size after increment |
| 95 | + */ |
| 96 | + public long incrementOutboundConnectionWindowSize(int increment) { |
| 97 | + return outboundConnectionWindowSize.incrementWindowSize(increment); |
| 98 | + } |
| 99 | + |
| 100 | + /** |
| 101 | + * Decrement inbound connection flow control window, called when DATA frame is received. |
| 102 | + * |
| 103 | + * @param decrement received DATA frame size in bytes |
| 104 | + * @return inbound window size after decrement |
| 105 | + */ |
| 106 | + public long decrementInboundConnectionWindowSize(int decrement) { |
| 107 | + return inboundConnectionWindowSize.decrementWindowSize(decrement); |
| 108 | + } |
| 109 | + |
| 110 | + /** |
| 111 | + * Reset MAX_FRAME_SIZE for all streams, existing and future ones. |
| 112 | + * |
| 113 | + * @param maxFrameSize to split data frames according to when larger |
| 114 | + */ |
| 115 | + public void resetMaxFrameSize(int maxFrameSize) { |
| 116 | + this.maxFrameSize = maxFrameSize; |
| 117 | + } |
| 118 | + |
| 119 | + /** |
| 120 | + * Reset an initial window size value for outbound flow control windows of a new streams. |
| 121 | + * Don't forget to call stream.flowControl().outbound().resetStreamWindowSize(...) for each stream |
| 122 | + * to align window size of existing streams. |
| 123 | + * |
| 124 | + * @param initialWindowSize INIT_WINDOW_SIZE received |
| 125 | + */ |
| 126 | + public void resetInitialWindowSize(int initialWindowSize) { |
| 127 | + if (LOGGER_OUTBOUND.isLoggable(DEBUG)) { |
| 128 | + LOGGER_OUTBOUND.log(DEBUG, String.format("%s OFC STR *: Recv INIT_WINDOW_SIZE %s", type, initialWindowSize)); |
| 129 | + } |
| 130 | + this.initialWindowSize = initialWindowSize; |
| 131 | + } |
| 132 | + |
| 133 | + /** |
| 134 | + * Connection outbound flow control window, |
| 135 | + * decrements when DATA are sent and increments when WINDOW_UPDATE or INIT_WINDOW_SIZE is received. |
| 136 | + * Blocks sending when window is depleted. |
| 137 | + * |
| 138 | + * @return connection outbound flow control window |
| 139 | + */ |
| 140 | + public WindowSize.Outbound outbound() { |
| 141 | + return outboundConnectionWindowSize; |
| 142 | + } |
| 143 | + |
| 144 | + /** |
| 145 | + * Connection inbound window is always manipulated by respective stream flow control, |
| 146 | + * therefore package private is enough. |
| 147 | + * |
| 148 | + * @return connection inbound flow control window |
| 149 | + */ |
| 150 | + WindowSize.Inbound inbound() { |
| 151 | + return inboundConnectionWindowSize; |
| 152 | + } |
| 153 | + |
| 154 | + int maxFrameSize() { |
| 155 | + return maxFrameSize; |
| 156 | + } |
| 157 | + |
| 158 | + int initialWindowSize() { |
| 159 | + return initialWindowSize; |
| 160 | + } |
| 161 | + |
| 162 | + Duration timeout() { |
| 163 | + return timeout; |
| 164 | + } |
| 165 | + |
| 166 | + enum Type { |
| 167 | + SERVER, CLIENT; |
| 168 | + } |
| 169 | + |
| 170 | + /** |
| 171 | + * Connection flow control builder. |
| 172 | + */ |
| 173 | + public static class ConnectionFlowControlBuilder implements Builder<ConnectionFlowControlBuilder, ConnectionFlowControl> { |
| 174 | + |
| 175 | + private static final Duration DEFAULT_TIMEOUT = Duration.ofMillis(100); |
| 176 | + private final Type type; |
| 177 | + private final BiConsumer<Integer, Http2WindowUpdate> windowUpdateWriter; |
| 178 | + private int initialWindowSize = WindowSize.DEFAULT_WIN_SIZE; |
| 179 | + private int maxFrameSize = WindowSize.DEFAULT_MAX_FRAME_SIZE; |
| 180 | + private Duration blockTimeout = DEFAULT_TIMEOUT; |
| 181 | + |
| 182 | + ConnectionFlowControlBuilder(Type type, BiConsumer<Integer, Http2WindowUpdate> windowUpdateWriter) { |
| 183 | + this.type = type; |
| 184 | + this.windowUpdateWriter = windowUpdateWriter; |
| 185 | + } |
| 186 | + |
| 187 | + /** |
| 188 | + * Outbound flow control INITIAL_WINDOW_SIZE setting for new HTTP/2 connections. |
| 189 | + * |
| 190 | + * @param initialWindowSize units of octets |
| 191 | + * @return updated builder |
| 192 | + */ |
| 193 | + public ConnectionFlowControlBuilder initialWindowSize(int initialWindowSize) { |
| 194 | + this.initialWindowSize = initialWindowSize; |
| 195 | + return this; |
| 196 | + } |
| 197 | + |
| 198 | + /** |
| 199 | + * Initial MAX_FRAME_SIZE setting for new HTTP/2 connections. |
| 200 | + * Maximum size of data frames in bytes we are prepared to accept from the other size. |
| 201 | + * Default value is 2^14(16_384). |
| 202 | + * |
| 203 | + * @param maxFrameSize data frame size in bytes between 2^14(16_384) and 2^24-1(16_777_215) |
| 204 | + * @return updated client |
| 205 | + */ |
| 206 | + public ConnectionFlowControlBuilder maxFrameSize(int maxFrameSize) { |
| 207 | + this.maxFrameSize = maxFrameSize; |
| 208 | + return this; |
| 209 | + } |
| 210 | + |
| 211 | + /** |
| 212 | + * Timeout for blocking between windows size check iterations. |
| 213 | + * |
| 214 | + * @param timeout duration |
| 215 | + * @return updated builder |
| 216 | + */ |
| 217 | + public ConnectionFlowControlBuilder blockTimeout(Duration timeout) { |
| 218 | + this.blockTimeout = timeout; |
| 219 | + return this; |
| 220 | + } |
| 221 | + |
| 222 | + @Override |
| 223 | + public ConnectionFlowControl build() { |
| 224 | + return new ConnectionFlowControl(type, initialWindowSize, maxFrameSize, windowUpdateWriter, blockTimeout); |
| 225 | + } |
| 226 | + } |
| 227 | +} |
0 commit comments