View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License, version 2.0 (the
5    * "License"); you may not use this file except in compliance with the License. You may obtain a
6    * copy of the License at:
7    *
8    * https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software distributed under the License
11   * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12   * or implied. See the License for the specific language governing permissions and limitations under
13   * the License.
14   */
15  
16  package io.netty.handler.codec.http2;
17  
18  import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
19  import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_WINDOW_SIZE;
20  import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_INITIAL_WINDOW_SIZE;
21  import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_INITIAL_WINDOW_SIZE;
22  import static io.netty.handler.codec.http2.Http2Error.FLOW_CONTROL_ERROR;
23  import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
24  import static io.netty.handler.codec.http2.Http2Exception.connectionError;
25  import static io.netty.handler.codec.http2.Http2Exception.streamError;
26  import static io.netty.util.internal.ObjectUtil.checkNotNull;
27  import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;
28  import static java.lang.Math.max;
29  import static java.lang.Math.min;
30  import io.netty.buffer.ByteBuf;
31  import io.netty.channel.ChannelHandlerContext;
32  import io.netty.handler.codec.http2.Http2Exception.CompositeStreamException;
33  import io.netty.handler.codec.http2.Http2Exception.StreamException;
34  import io.netty.util.internal.PlatformDependent;
35  
36  /**
37   * Basic implementation of {@link Http2LocalFlowController}.
38   * <p>
39   * This class is <strong>NOT</strong> thread safe. The assumption is all methods must be invoked from a single thread.
40   * Typically this thread is the event loop thread for the {@link ChannelHandlerContext} managed by this class.
41   */
42  public class DefaultHttp2LocalFlowController implements Http2LocalFlowController {
43      /**
44       * The default ratio of window size to initial window size below which a {@code WINDOW_UPDATE}
45       * is sent to expand the window.
46       */
47      public static final float DEFAULT_WINDOW_UPDATE_RATIO = 0.5f;
48  
49      private final Http2Connection connection;
50      private final Http2Connection.PropertyKey stateKey;
51      private Http2FrameWriter frameWriter;
52      private ChannelHandlerContext ctx;
53      private float windowUpdateRatio;
54      private int initialWindowSize = DEFAULT_WINDOW_SIZE;
55  
56      public DefaultHttp2LocalFlowController(Http2Connection connection) {
57          this(connection, DEFAULT_WINDOW_UPDATE_RATIO, false);
58      }
59  
60      /**
61       * Constructs a controller with the given settings.
62       *
63       * @param connection the connection state.
64       * @param windowUpdateRatio the window percentage below which to send a {@code WINDOW_UPDATE}.
65       * @param autoRefillConnectionWindow if {@code true}, effectively disables the connection window
66       * in the flow control algorithm as they will always refill automatically without requiring the
67       * application to consume the bytes. When enabled, the maximum bytes you must be prepared to
68       * queue is proportional to {@code maximum number of concurrent streams * the initial window
69       * size per stream}
70       * (<a href="https://tools.ietf.org/html/rfc7540#section-6.5.2">SETTINGS_MAX_CONCURRENT_STREAMS</a>
71       * <a href="https://tools.ietf.org/html/rfc7540#section-6.5.2">SETTINGS_INITIAL_WINDOW_SIZE</a>).
72       */
73      public DefaultHttp2LocalFlowController(Http2Connection connection,
74                                             float windowUpdateRatio,
75                                             boolean autoRefillConnectionWindow) {
76          this.connection = checkNotNull(connection, "connection");
77          windowUpdateRatio(windowUpdateRatio);
78  
79          // Add a flow state for the connection.
80          stateKey = connection.newKey();
81          FlowState connectionState = autoRefillConnectionWindow ?
82                  new AutoRefillState(connection.connectionStream(), initialWindowSize) :
83                  new DefaultState(connection.connectionStream(), initialWindowSize);
84          connection.connectionStream().setProperty(stateKey, connectionState);
85  
86          // Register for notification of new streams.
87          connection.addListener(new Http2ConnectionAdapter() {
88              @Override
89              public void onStreamAdded(Http2Stream stream) {
90                  // Unconditionally used the reduced flow control state because it requires no object allocation
91                  // and the DefaultFlowState will be allocated in onStreamActive.
92                  stream.setProperty(stateKey, REDUCED_FLOW_STATE);
93              }
94  
95              @Override
96              public void onStreamActive(Http2Stream stream) {
97                  // Need to be sure the stream's initial window is adjusted for SETTINGS
98                  // frames which may have been exchanged while it was in IDLE
99                  stream.setProperty(stateKey, new DefaultState(stream, initialWindowSize));
100             }
101 
102             @Override
103             public void onStreamClosed(Http2Stream stream) {
104                 try {
105                     // When a stream is closed, consume any remaining bytes so that they
106                     // are restored to the connection window.
107                     FlowState state = state(stream);
108                     int unconsumedBytes = state.unconsumedBytes();
109                     if (ctx != null && unconsumedBytes > 0) {
110                         if (consumeAllBytes(state, unconsumedBytes)) {
111                             // As the user has no real control on when this callback is used we should better
112                             // call flush() if we produced any window update to ensure we not stale.
113                             ctx.flush();
114                         }
115                     }
116                 } catch (Http2Exception e) {
117                     PlatformDependent.throwException(e);
118                 } finally {
119                     // Unconditionally reduce the amount of memory required for flow control because there is no
120                     // object allocation costs associated with doing so and the stream will not have any more
121                     // local flow control state to keep track of anymore.
122                     stream.setProperty(stateKey, REDUCED_FLOW_STATE);
123                 }
124             }
125         });
126     }
127 
128     @Override
129     public DefaultHttp2LocalFlowController frameWriter(Http2FrameWriter frameWriter) {
130         this.frameWriter = checkNotNull(frameWriter, "frameWriter");
131         return this;
132     }
133 
134     @Override
135     public void channelHandlerContext(ChannelHandlerContext ctx) {
136         this.ctx = checkNotNull(ctx, "ctx");
137     }
138 
139     @Override
140     public void initialWindowSize(int newWindowSize) throws Http2Exception {
141         assert ctx == null || ctx.executor().inEventLoop();
142         int delta = newWindowSize - initialWindowSize;
143         initialWindowSize = newWindowSize;
144 
145         WindowUpdateVisitor visitor = new WindowUpdateVisitor(delta);
146         connection.forEachActiveStream(visitor);
147         visitor.throwIfError();
148     }
149 
150     @Override
151     public int initialWindowSize() {
152         return initialWindowSize;
153     }
154 
155     @Override
156     public int windowSize(Http2Stream stream) {
157         return state(stream).windowSize();
158     }
159 
160     @Override
161     public int initialWindowSize(Http2Stream stream) {
162         return state(stream).initialWindowSize();
163     }
164 
165     @Override
166     public void incrementWindowSize(Http2Stream stream, int delta) throws Http2Exception {
167         assert ctx != null && ctx.executor().inEventLoop();
168         FlowState state = state(stream);
169         // Just add the delta to the stream-specific initial window size so that the next time the window
170         // expands it will grow to the new initial size.
171         state.incrementInitialStreamWindow(delta);
172         state.writeWindowUpdateIfNeeded();
173     }
174 
175     @Override
176     public boolean consumeBytes(Http2Stream stream, int numBytes) throws Http2Exception {
177         assert ctx != null && ctx.executor().inEventLoop();
178         checkPositiveOrZero(numBytes, "numBytes");
179         if (numBytes == 0) {
180             return false;
181         }
182 
183         if (stream != null) {
184             if (stream.id() == CONNECTION_STREAM_ID) {
185                 throw new UnsupportedOperationException("Returning bytes for the connection window is not supported");
186             }
187 
188             // Streams automatically consume all remaining bytes when they are closed, and streams that are not
189             // active yet (IDLE or RESERVED_*) never have bytes accounted to them, so just ignore those.
190             if (isActive(stream)) {
191                 return consumeAllBytes(state(stream), numBytes);
192             }
193         }
194         return false;
195     }
196 
197     private boolean consumeAllBytes(FlowState state, int numBytes) throws Http2Exception {
198         return connectionState().consumeBytes(numBytes) | state.consumeBytes(numBytes);
199     }
200 
201     @Override
202     public int unconsumedBytes(Http2Stream stream) {
203         return state(stream).unconsumedBytes();
204     }
205 
206     private static void checkValidRatio(float ratio) {
207         if (Double.compare(ratio, 0.0) <= 0 || Double.compare(ratio, 1.0) >= 0) {
208             throw new IllegalArgumentException("Invalid ratio: " + ratio);
209         }
210     }
211 
212     /**
213      * The window update ratio is used to determine when a window update must be sent. If the ratio
214      * of bytes processed since the last update has meet or exceeded this ratio then a window update will
215      * be sent. This is the global window update ratio that will be used for new streams.
216      * @param ratio the ratio to use when checking if a {@code WINDOW_UPDATE} is determined necessary for new streams.
217      * @throws IllegalArgumentException If the ratio is out of bounds (0, 1).
218      */
219     public void windowUpdateRatio(float ratio) {
220         assert ctx == null || ctx.executor().inEventLoop();
221         checkValidRatio(ratio);
222         windowUpdateRatio = ratio;
223     }
224 
225     /**
226      * The window update ratio is used to determine when a window update must be sent. If the ratio
227      * of bytes processed since the last update has meet or exceeded this ratio then a window update will
228      * be sent. This is the global window update ratio that will be used for new streams.
229      */
230     public float windowUpdateRatio() {
231         return windowUpdateRatio;
232     }
233 
234     /**
235      * The window update ratio is used to determine when a window update must be sent. If the ratio
236      * of bytes processed since the last update has meet or exceeded this ratio then a window update will
237      * be sent. This window update ratio will only be applied to {@code streamId}.
238      * <p>
239      * Note it is the responsibly of the caller to ensure that the
240      * initial {@code SETTINGS} frame is sent before this is called. It would
241      * be considered a {@link Http2Error#PROTOCOL_ERROR} if a {@code WINDOW_UPDATE}
242      * was generated by this method before the initial {@code SETTINGS} frame is sent.
243      * @param stream the stream for which {@code ratio} applies to.
244      * @param ratio the ratio to use when checking if a {@code WINDOW_UPDATE} is determined necessary.
245      * @throws Http2Exception If a protocol-error occurs while generating {@code WINDOW_UPDATE} frames
246      */
247     public void windowUpdateRatio(Http2Stream stream, float ratio) throws Http2Exception {
248         assert ctx != null && ctx.executor().inEventLoop();
249         checkValidRatio(ratio);
250         FlowState state = state(stream);
251         state.windowUpdateRatio(ratio);
252         state.writeWindowUpdateIfNeeded();
253     }
254 
255     /**
256      * The window update ratio is used to determine when a window update must be sent. If the ratio
257      * of bytes processed since the last update has meet or exceeded this ratio then a window update will
258      * be sent. This window update ratio will only be applied to {@code streamId}.
259      * @throws Http2Exception If no stream corresponding to {@code stream} could be found.
260      */
261     public float windowUpdateRatio(Http2Stream stream) throws Http2Exception {
262         return state(stream).windowUpdateRatio();
263     }
264 
265     @Override
266     public void receiveFlowControlledFrame(Http2Stream stream, ByteBuf data, int padding,
267             boolean endOfStream) throws Http2Exception {
268         assert ctx != null && ctx.executor().inEventLoop();
269         int dataLength = data.readableBytes() + padding;
270 
271         // Apply the connection-level flow control
272         FlowState connectionState = connectionState();
273         connectionState.receiveFlowControlledFrame(dataLength);
274 
275         if (stream != null && isActive(stream)) {
276             // Apply the stream-level flow control
277             FlowState state = state(stream);
278             state.endOfStream(endOfStream);
279             state.receiveFlowControlledFrame(dataLength);
280         } else if (dataLength > 0) {
281             // Immediately consume the bytes for the connection window.
282             connectionState.consumeBytes(dataLength);
283         }
284     }
285 
286     private FlowState connectionState() {
287         return connection.connectionStream().getProperty(stateKey);
288     }
289 
290     private FlowState state(Http2Stream stream) {
291         return stream.getProperty(stateKey);
292     }
293 
294     /**
295      * Returns {@code true} if the stream is active and so has its own flow control state. Streams that are
296      * {@code IDLE}, {@code RESERVED_*} or {@code CLOSED} use the {@link #REDUCED_FLOW_STATE}.
297      */
298     private static boolean isActive(Http2Stream stream) {
299         switch (stream.state()) {
300             case OPEN:
301             case HALF_CLOSED_LOCAL:
302             case HALF_CLOSED_REMOTE:
303                 return true;
304             default:
305                 return false;
306         }
307     }
308 
309     private static boolean isClosed(Http2Stream stream) {
310         return stream.state() == Http2Stream.State.CLOSED;
311     }
312 
313     /**
314      * Flow control state that does autorefill of the flow control window when the data is
315      * received.
316      */
317     private final class AutoRefillState extends DefaultState {
318         AutoRefillState(Http2Stream stream, int initialWindowSize) {
319             super(stream, initialWindowSize);
320         }
321 
322         @Override
323         public void receiveFlowControlledFrame(int dataLength) throws Http2Exception {
324             super.receiveFlowControlledFrame(dataLength);
325             // Need to call the super to consume the bytes, since this.consumeBytes does nothing.
326             super.consumeBytes(dataLength);
327         }
328 
329         @Override
330         public boolean consumeBytes(int numBytes) throws Http2Exception {
331             // Do nothing, since the bytes are already consumed upon receiving the data.
332             return false;
333         }
334     }
335 
336     /**
337      * Flow control window state for an individual stream.
338      */
339     private class DefaultState implements FlowState {
340         private final Http2Stream stream;
341 
342         /**
343          * The actual flow control window that is decremented as soon as {@code DATA} arrives.
344          */
345         private int window;
346 
347         /**
348          * A view of {@link #window} that is used to determine when to send {@code WINDOW_UPDATE}
349          * frames. Decrementing this window for received {@code DATA} frames is delayed until the
350          * application has indicated that the data has been fully processed. This prevents sending
351          * a {@code WINDOW_UPDATE} until the number of processed bytes drops below the threshold.
352          */
353         private int processedWindow;
354 
355         /**
356          * This is what is used to determine how many bytes need to be returned relative to {@link #processedWindow}.
357          * Each stream has their own initial window size.
358          */
359         private int initialStreamWindowSize;
360 
361         /**
362          * This is used to determine when {@link #processedWindow} is sufficiently far away from
363          * {@link #initialStreamWindowSize} such that a {@code WINDOW_UPDATE} should be sent.
364          * Each stream has their own window update ratio.
365          */
366         private float streamWindowUpdateRatio;
367 
368         private int lowerBound;
369         private boolean endOfStream;
370 
371         DefaultState(Http2Stream stream, int initialWindowSize) {
372             this.stream = stream;
373             window(initialWindowSize);
374             streamWindowUpdateRatio = windowUpdateRatio;
375         }
376 
377         @Override
378         public void window(int initialWindowSize) {
379             assert ctx == null || ctx.executor().inEventLoop();
380             window = processedWindow = initialStreamWindowSize = initialWindowSize;
381         }
382 
383         @Override
384         public int windowSize() {
385             return window;
386         }
387 
388         @Override
389         public int initialWindowSize() {
390             return initialStreamWindowSize;
391         }
392 
393         @Override
394         public void endOfStream(boolean endOfStream) {
395             this.endOfStream = endOfStream;
396         }
397 
398         @Override
399         public float windowUpdateRatio() {
400             return streamWindowUpdateRatio;
401         }
402 
403         @Override
404         public void windowUpdateRatio(float ratio) {
405             assert ctx == null || ctx.executor().inEventLoop();
406             streamWindowUpdateRatio = ratio;
407         }
408 
409         @Override
410         public void incrementInitialStreamWindow(int delta) {
411             // Clip the delta so that the resulting initialStreamWindowSize falls within the allowed range.
412             int newValue = (int) min(MAX_INITIAL_WINDOW_SIZE,
413                     max(MIN_INITIAL_WINDOW_SIZE, initialStreamWindowSize + (long) delta));
414             delta = newValue - initialStreamWindowSize;
415 
416             initialStreamWindowSize += delta;
417         }
418 
419         @Override
420         public void incrementFlowControlWindows(int delta) throws Http2Exception {
421             if (delta > 0 && window > MAX_INITIAL_WINDOW_SIZE - delta) {
422                 throw streamError(stream.id(), FLOW_CONTROL_ERROR,
423                         "Flow control window overflowed for stream: %d", stream.id());
424             }
425 
426             window += delta;
427             processedWindow += delta;
428             lowerBound = min(delta, 0);
429         }
430 
431         @Override
432         public void receiveFlowControlledFrame(int dataLength) throws Http2Exception {
433             assert dataLength >= 0;
434 
435             // Apply the delta. Even if we throw an exception we want to have taken this delta into account.
436             window -= dataLength;
437 
438             // Window size can become negative if we sent a SETTINGS frame that reduces the
439             // size of the transfer window after the peer has written data frames.
440             // The value is bounded by the length that SETTINGS frame decrease the window.
441             // This difference is stored for the connection when writing the SETTINGS frame
442             // and is cleared once we send a WINDOW_UPDATE frame.
443             if (window < lowerBound) {
444                 throw streamError(stream.id(), FLOW_CONTROL_ERROR,
445                         "Flow control window exceeded for stream: %d", stream.id());
446             }
447         }
448 
449         private void returnProcessedBytes(int delta) throws Http2Exception {
450             if (processedWindow - delta < window) {
451                 throw streamError(stream.id(), INTERNAL_ERROR,
452                         "Attempting to return too many bytes for stream %d", stream.id());
453             }
454             processedWindow -= delta;
455         }
456 
457         @Override
458         public boolean consumeBytes(int numBytes) throws Http2Exception {
459             // Return the bytes processed and update the window.
460             returnProcessedBytes(numBytes);
461             return writeWindowUpdateIfNeeded();
462         }
463 
464         @Override
465         public int unconsumedBytes() {
466             return processedWindow - window;
467         }
468 
469         @Override
470         public boolean writeWindowUpdateIfNeeded() throws Http2Exception {
471             if (endOfStream || initialStreamWindowSize <= 0 ||
472                     // If the stream is already closed there is no need to try to write a window update for it.
473                     isClosed(stream)) {
474                 return false;
475             }
476 
477             int threshold = (int) (initialStreamWindowSize * streamWindowUpdateRatio);
478             if (processedWindow <= threshold) {
479                 writeWindowUpdate();
480                 return true;
481             }
482             return false;
483         }
484 
485         /**
486          * Called to perform a window update for this stream (or connection). Updates the window size back
487          * to the size of the initial window and sends a window update frame to the remote endpoint.
488          */
489         private void writeWindowUpdate() throws Http2Exception {
490             // Expand the window for this stream back to the size of the initial window.
491             int deltaWindowSize = initialStreamWindowSize - processedWindow;
492             try {
493                 incrementFlowControlWindows(deltaWindowSize);
494             } catch (Throwable t) {
495                 throw connectionError(INTERNAL_ERROR, t,
496                         "Attempting to return too many bytes for stream %d", stream.id());
497             }
498 
499             // Send a window update for the stream/connection.
500             frameWriter.writeWindowUpdate(ctx, stream.id(), deltaWindowSize, ctx.newPromise());
501         }
502     }
503 
504     /**
505      * The local flow control state for a single stream that is not in a state where flow controlled frames cannot
506      * be exchanged.
507      */
508     private static final FlowState REDUCED_FLOW_STATE = new FlowState() {
509 
510         @Override
511         public int windowSize() {
512             return 0;
513         }
514 
515         @Override
516         public int initialWindowSize() {
517             return 0;
518         }
519 
520         @Override
521         public void window(int initialWindowSize) {
522             throw new UnsupportedOperationException();
523         }
524 
525         @Override
526         public void incrementInitialStreamWindow(int delta) {
527             // This operation needs to be supported during the initial settings exchange when
528             // the peer has not yet acknowledged this peer being activated.
529         }
530 
531         @Override
532         public boolean writeWindowUpdateIfNeeded() throws Http2Exception {
533             throw new UnsupportedOperationException();
534         }
535 
536         @Override
537         public boolean consumeBytes(int numBytes) throws Http2Exception {
538             return false;
539         }
540 
541         @Override
542         public int unconsumedBytes() {
543             return 0;
544         }
545 
546         @Override
547         public float windowUpdateRatio() {
548             throw new UnsupportedOperationException();
549         }
550 
551         @Override
552         public void windowUpdateRatio(float ratio) {
553             throw new UnsupportedOperationException();
554         }
555 
556         @Override
557         public void receiveFlowControlledFrame(int dataLength) throws Http2Exception {
558             throw new UnsupportedOperationException();
559         }
560 
561         @Override
562         public void incrementFlowControlWindows(int delta) throws Http2Exception {
563             // This operation needs to be supported during the initial settings exchange when
564             // the peer has not yet acknowledged this peer being activated.
565         }
566 
567         @Override
568         public void endOfStream(boolean endOfStream) {
569             throw new UnsupportedOperationException();
570         }
571     };
572 
573     /**
574      * An abstraction which provides specific extensions used by local flow control.
575      */
576     private interface FlowState {
577 
578         int windowSize();
579 
580         int initialWindowSize();
581 
582         void window(int initialWindowSize);
583 
584         /**
585          * Increment the initial window size for this stream.
586          * @param delta The amount to increase the initial window size by.
587          */
588         void incrementInitialStreamWindow(int delta);
589 
590         /**
591          * Updates the flow control window for this stream if it is appropriate.
592          *
593          * @return true if {@code WINDOW_UPDATE} was written, false otherwise.
594          */
595         boolean writeWindowUpdateIfNeeded() throws Http2Exception;
596 
597         /**
598          * Indicates that the application has consumed {@code numBytes} from the connection or stream and is
599          * ready to receive more data.
600          *
601          * @param numBytes the number of bytes to be returned to the flow control window.
602          * @return true if {@code WINDOW_UPDATE} was written, false otherwise.
603          * @throws Http2Exception
604          */
605         boolean consumeBytes(int numBytes) throws Http2Exception;
606 
607         int unconsumedBytes();
608 
609         float windowUpdateRatio();
610 
611         void windowUpdateRatio(float ratio);
612 
613         /**
614          * A flow control event has occurred and we should decrement the amount of available bytes for this stream.
615          * @param dataLength The amount of data to for which this stream is no longer eligible to use for flow control.
616          * @throws Http2Exception If too much data is used relative to how much is available.
617          */
618         void receiveFlowControlledFrame(int dataLength) throws Http2Exception;
619 
620         /**
621          * Increment the windows which are used to determine many bytes have been processed.
622          * @param delta The amount to increment the window by.
623          * @throws Http2Exception if integer overflow occurs on the window.
624          */
625         void incrementFlowControlWindows(int delta) throws Http2Exception;
626 
627         void endOfStream(boolean endOfStream);
628     }
629 
630     /**
631      * Provides a means to iterate over all active streams and increment the flow control windows.
632      */
633     private final class WindowUpdateVisitor implements Http2StreamVisitor {
634         private CompositeStreamException compositeException;
635         private final int delta;
636 
637         WindowUpdateVisitor(int delta) {
638             this.delta = delta;
639         }
640 
641         @Override
642         public boolean visit(Http2Stream stream) throws Http2Exception {
643             try {
644                 // Increment flow control window first so state will be consistent if overflow is detected.
645                 FlowState state = state(stream);
646                 state.incrementFlowControlWindows(delta);
647                 state.incrementInitialStreamWindow(delta);
648             } catch (StreamException e) {
649                 if (compositeException == null) {
650                     compositeException = new CompositeStreamException(e.error(), 4);
651                 }
652                 compositeException.add(e);
653             }
654             return true;
655         }
656 
657         public void throwIfError() throws CompositeStreamException {
658             if (compositeException != null) {
659                 throw compositeException;
660             }
661         }
662     }
663 }