All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController Maven / Gradle / Ivy

There is a newer version: 5.0.0.Alpha2
Show newest version
/*
 * Copyright 2014 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License, version 2.0 (the
 * "License"); you may not use this file except in compliance with the License. You may obtain a
 * copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License
 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing permissions and limitations under
 * the License.
 */
package io.netty.handler.codec.http2;

import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_WINDOW_SIZE;
import static io.netty.handler.codec.http2.Http2Error.FLOW_CONTROL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.streamError;
import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_LOCAL;
import static io.netty.handler.codec.http2.Http2Stream.State.IDLE;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static java.lang.Math.max;
import static java.lang.Math.min;

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http2.StreamByteDistributor.Writer;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;

import java.util.ArrayDeque;
import java.util.Deque;

/**
 * Basic implementation of {@link Http2RemoteFlowController}.
 * 

* This class is NOT thread safe. The assumption is all methods must be invoked from a single thread. * Typically this thread is the event loop thread for the {@link ChannelHandlerContext} managed by this class. */ public class DefaultHttp2RemoteFlowController implements Http2RemoteFlowController { private static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultHttp2RemoteFlowController.class); private static final int MIN_WRITABLE_CHUNK = 32 * 1024; private final Http2Connection connection; private final Http2Connection.PropertyKey stateKey; private final StreamByteDistributor streamByteDistributor; private final AbstractState connectionState; private int initialWindowSize = DEFAULT_WINDOW_SIZE; private WritabilityMonitor monitor; private ChannelHandlerContext ctx; public DefaultHttp2RemoteFlowController(Http2Connection connection) { this(connection, (Listener) null); } public DefaultHttp2RemoteFlowController(Http2Connection connection, StreamByteDistributor streamByteDistributor) { this(connection, streamByteDistributor, null); } public DefaultHttp2RemoteFlowController(Http2Connection connection, final Listener listener) { this(connection, new WeightedFairQueueByteDistributor(connection), listener); } public DefaultHttp2RemoteFlowController(Http2Connection connection, StreamByteDistributor streamByteDistributor, final Listener listener) { this.connection = checkNotNull(connection, "connection"); this.streamByteDistributor = checkNotNull(streamByteDistributor, "streamWriteDistributor"); // Add a flow state for the connection. stateKey = connection.newKey(); connectionState = new DefaultState(connection.connectionStream(), initialWindowSize, initialWindowSize > 0 && isChannelWritable()); connection.connectionStream().setProperty(stateKey, connectionState); // Monitor may depend upon connectionState, and so initialize after connectionState listener(listener); // Register for notification of new streams. connection.addListener(new Http2ConnectionAdapter() { @Override public void onStreamAdded(Http2Stream stream) { // If the stream state is not open then the stream is not yet eligible for flow controlled frames and // only requires the ReducedFlowState. Otherwise the full amount of memory is required. stream.setProperty(stateKey, stream.state() == IDLE ? new ReducedState(stream) : new DefaultState(stream, 0, isWritable(DefaultHttp2RemoteFlowController.this.connection.connectionStream()))); } @Override public void onStreamActive(Http2Stream stream) { // If the object was previously created, but later activated then we have to ensure // the full state is allocated and the proper initialWindowSize is used. AbstractState state = state(stream); if (state.getClass() == DefaultState.class) { state.window(initialWindowSize); } else { stream.setProperty(stateKey, new DefaultState(state, initialWindowSize)); } } @Override public void onStreamClosed(Http2Stream stream) { // Any pending frames can never be written, cancel and // write errors for any pending frames. AbstractState state = state(stream); state.cancel(); // If the stream is now eligible for removal, but will persist in the priority tree then we can // decrease the amount of memory required for this stream because no flow controlled frames can // be exchanged on this stream if (stream.prioritizableForTree() != 0) { state = new ReducedState(state); stream.setProperty(stateKey, state); } // Tell the monitor after cancel has been called and after the new state is used. monitor.stateCancelled(state); } @Override public void onStreamHalfClosed(Http2Stream stream) { if (HALF_CLOSED_LOCAL.equals(stream.state())) { /** * When this method is called there should not be any * pending frames left if the API is used correctly. However, * it is possible that a erroneous application can sneak * in a frame even after having already written a frame with the * END_STREAM flag set, as the stream state might not transition * immediately to HALF_CLOSED_LOCAL / CLOSED due to flow control * delaying the write. * * This is to cancel any such illegal writes. */ AbstractState state = state(stream); state.cancel(); monitor.stateCancelled(state); } } }); } /** * {@inheritDoc} *

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

     * Connection Window - Total Queued Bytes > 0 &&
     * Stream Window - Bytes Queued for Stream > 0 &&
     * isChannelWritable()
     * 
*/ private final class ListenerWritabilityMonitor extends WritabilityMonitor { private final Listener listener; private final Http2StreamVisitor checkStreamWritabilityVisitor = new Http2StreamVisitor() { @Override public boolean visit(Http2Stream stream) throws Http2Exception { AbstractState state = state(stream); if (isWritable(state) != state.markWritability()) { notifyWritabilityChanged(state); } return true; } }; ListenerWritabilityMonitor(Listener listener) { this.listener = listener; } @Override public void incrementWindowSize(AbstractState state, int delta) throws Http2Exception { super.incrementWindowSize(state, delta); if (isWritable(state) != state.markWritability()) { if (state == connectionState) { checkAllWritabilityChanged(); } else { notifyWritabilityChanged(state); } } } @Override protected void initialWindowSize(int newWindowSize) throws Http2Exception { super.initialWindowSize(newWindowSize); if (isWritableConnection()) { // If the write operation does not occur we still need to check all streams because they // may have transitioned from writable to not writable. checkAllWritabilityChanged(); } } @Override public void enqueueFrame(AbstractState state, FlowControlled frame) throws Http2Exception { super.enqueueFrame(state, frame); checkConnectionThenStreamWritabilityChanged(state); } @Override public void stateCancelled(AbstractState state) { try { checkConnectionThenStreamWritabilityChanged(state); } catch (Http2Exception e) { logger.error("Caught unexpected exception from checkAllWritabilityChanged", e); } } @Override public void channelWritabilityChange() throws Http2Exception { if (connectionState.markWritability() != isChannelWritable()) { checkAllWritabilityChanged(); } } private void notifyWritabilityChanged(AbstractState state) { state.markWritability(!state.markWritability()); try { listener.writabilityChanged(state.stream); } catch (RuntimeException e) { logger.error("Caught unexpected exception from listener.writabilityChanged", e); } } private void checkConnectionThenStreamWritabilityChanged(AbstractState state) throws Http2Exception { // It is possible that the connection window and/or the individual stream writability could change. if (isWritableConnection() != connectionState.markWritability()) { checkAllWritabilityChanged(); } else if (isWritable(state) != state.markWritability()) { notifyWritabilityChanged(state); } } private void checkAllWritabilityChanged() throws Http2Exception { // Make sure we mark that we have notified as a result of this change. connectionState.markWritability(isWritableConnection()); connection.forEachActiveStream(checkStreamWritabilityVisitor); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy