Package io.netty.handler.codec.http2

Source Code of io.netty.handler.codec.http2.DefaultHttp2OutboundFlowController$OutboundFlowState$Frame

/*
* 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.CONNECTION_STREAM_ID;
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.STREAM_CLOSED;
import static io.netty.handler.codec.http2.Http2Exception.format;
import static io.netty.handler.codec.http2.Http2Exception.protocolError;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static java.lang.Math.max;
import static java.lang.Math.min;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.ChannelPromiseAggregator;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Queue;

/**
* Basic implementation of {@link Http2OutboundFlowController}.
*/
public class DefaultHttp2OutboundFlowController implements Http2OutboundFlowController {
    /**
     * A comparators that sorts priority nodes in ascending order by the amount of priority data available for its
     * subtree.
     */
    private static final Comparator<Http2Stream> DATA_WEIGHT = new Comparator<Http2Stream>() {
        @Override
        public int compare(Http2Stream o1, Http2Stream o2) {
            final long result = ((long) state(o1).priorityBytes()) * o1.weight() -
                                ((long) state(o2).priorityBytes()) * o2.weight();
            return result > 0 ? 1 : (result < 0 ? -1 : 0);
        }
    };

    private final Http2Connection connection;
    private final Http2FrameWriter frameWriter;
    private int initialWindowSize = DEFAULT_WINDOW_SIZE;
    private ChannelHandlerContext ctx;

    public DefaultHttp2OutboundFlowController(Http2Connection connection, Http2FrameWriter frameWriter) {
        this.connection = checkNotNull(connection, "connection");
        this.frameWriter = checkNotNull(frameWriter, "frameWriter");

        // Add a flow state for the connection.
        connection.connectionStream().outboundFlow(new OutboundFlowState(connection.connectionStream()));

        // Register for notification of new streams.
        connection.addListener(new Http2ConnectionAdapter() {
            @Override
            public void streamAdded(Http2Stream stream) {
                // Just add a new flow state to the stream.
                stream.outboundFlow(new OutboundFlowState(stream));
            }

            @Override
            public void streamHalfClosed(Http2Stream stream) {
                if (!stream.localSideOpen()) {
                    // Any pending frames can never be written, clear and
                    // write errors for any pending frames.
                    state(stream).clear();
                }
            }

            @Override
            public void streamInactive(Http2Stream stream) {
                // Any pending frames can never be written, clear and
                // write errors for any pending frames.
                state(stream).clear();
            }

            @Override
            public void priorityTreeParentChanged(Http2Stream stream, Http2Stream oldParent) {
                Http2Stream parent = stream.parent();
                if (parent != null) {
                    state(parent).incrementPriorityBytes(state(stream).priorityBytes());
                }
            }

            @Override
            public void priorityTreeParentChanging(Http2Stream stream, Http2Stream newParent) {
                Http2Stream parent = stream.parent();
                if (parent != null) {
                    state(parent).incrementPriorityBytes(-state(stream).priorityBytes());
                }
            }
        });
    }

    @Override
    public void initialOutboundWindowSize(int newWindowSize) throws Http2Exception {
        if (newWindowSize < 0) {
            throw new IllegalArgumentException("Invalid initial window size: " + newWindowSize);
        }

        int delta = newWindowSize - initialWindowSize;
        initialWindowSize = newWindowSize;
        for (Http2Stream stream : connection.activeStreams()) {
            // Verify that the maximum value is not exceeded by this change.
            OutboundFlowState state = state(stream);
            state.incrementStreamWindow(delta);
        }

        if (delta > 0) {
            // The window size increased, send any pending frames for all streams.
            writePendingBytes();
        }
    }

    @Override
    public int initialOutboundWindowSize() {
        return initialWindowSize;
    }

    @Override
    public void updateOutboundWindowSize(int streamId, int delta) throws Http2Exception {
        if (delta <= 0) {
            throw new IllegalArgumentException("delta must be > 0");
        }

        if (streamId == CONNECTION_STREAM_ID) {
            // Update the connection window and write any pending frames for all streams.
            connectionState().incrementStreamWindow(delta);
            writePendingBytes();
        } else {
            // Update the stream window and write any pending frames for the stream.
            OutboundFlowState state = stateOrFail(streamId);
            state.incrementStreamWindow(delta);
            if (state.writeBytes(state.writableWindow()) > 0) {
                flush();
            }
        }
    }

    @Override
    public ChannelFuture writeData(ChannelHandlerContext ctx, int streamId, ByteBuf data,
            int padding, boolean endStream, ChannelPromise promise) {
        checkNotNull(ctx, "ctx");
        checkNotNull(promise, "promise");
        checkNotNull(data, "data");
        if (this.ctx != null && this.ctx != ctx) {
            throw new IllegalArgumentException("Writing data from multiple ChannelHandlerContexts is not supported");
        }
        if (padding < 0) {
            throw new IllegalArgumentException("padding must be >= 0");
        }
        if (streamId <= 0) {
            throw new IllegalArgumentException("streamId must be >= 0");
        }

        // Save the context. We'll use this later when we write pending bytes.
        this.ctx = ctx;

        try {
            OutboundFlowState state = stateOrFail(streamId);

            int window = state.writableWindow();
            boolean framesAlreadyQueued = state.hasFrame();

            OutboundFlowState.Frame frame = state.newFrame(promise, data, padding, endStream);
            if (!framesAlreadyQueued && window >= frame.size()) {
                // Window size is large enough to send entire data frame
                frame.write();
                ctx.flush();
                return promise;
            }

            // Enqueue the frame to be written when the window size permits.
            frame.enqueue();

            if (framesAlreadyQueued || window <= 0) {
                // Stream already has frames pending or is stalled, don't send anything now.
                return promise;
            }

            // Create and send a partial frame up to the window size.
            frame.split(window).write();
            ctx.flush();
        } catch (Http2Exception e) {
            promise.setFailure(e);
        }
        return promise;
    }

    private static OutboundFlowState state(Http2Stream stream) {
        return (OutboundFlowState) stream.outboundFlow();
    }

    private OutboundFlowState connectionState() {
        return state(connection.connectionStream());
    }

    private OutboundFlowState state(int streamId) {
        return state(connection.stream(streamId));
    }

    /**
     * Attempts to get the {@link OutboundFlowState} for the given stream. If not available, raises a
     * {@code PROTOCOL_ERROR}.
     */
    private OutboundFlowState stateOrFail(int streamId) throws Http2Exception {
        OutboundFlowState state = state(streamId);
        if (state == null) {
            throw protocolError("Missing flow control window for stream: %d", streamId);
        }
        return state;
    }

    /**
     * Returns the flow control window for the entire connection.
     */
    private int connectionWindow() {
        return connectionState().window();
    }

    /**
     * Flushes the {@link ChannelHandlerContext} if we've received any data frames.
     */
    private void flush() {
        if (ctx != null) {
            ctx.flush();
        }
    }

    /**
     * Writes as many pending bytes as possible, according to stream priority.
     */
    private void writePendingBytes() throws Http2Exception {

        // Recursively write as many of the total writable bytes as possible.
        Http2Stream connectionStream = connection.connectionStream();
        int totalAllowance = state(connectionStream).priorityBytes();
        writeAllowedBytes(connectionStream, totalAllowance);

        // Optimization: only flush once for all written frames. If it's null, there are no
        // data frames to send anyway.
        flush();
    }

    /**
     * Recursively traverses the priority tree rooted at the given node. Attempts to write the allowed bytes for the
     * streams in this sub tree based on their weighted priorities.
     *
     * @param allowance
     *            an allowed number of bytes that may be written to the streams in this subtree
     */
    private void writeAllowedBytes(Http2Stream stream, int allowance) throws Http2Exception {
        // Write the allowed bytes for this node. If not all of the allowance was used,
        // restore what's left so that it can be propagated to future nodes.
        OutboundFlowState state = state(stream);
        int bytesWritten = state.writeBytes(allowance);
        allowance -= bytesWritten;

        if (allowance <= 0 || stream.isLeaf()) {
            // Nothing left to do in this sub tree.
            return;
        }

        // Clip the remaining connection flow control window by the allowance.
        int remainingWindow = min(allowance, connectionWindow());

        // The total number of unallocated bytes from the children of this node.
        int unallocatedBytes = state.priorityBytes() - state.streamableBytes();

        // Optimization. If the window is big enough to fit all the data. Just write everything
        // and skip the priority algorithm.
        if (unallocatedBytes <= remainingWindow) {
            for (Http2Stream child : stream.children()) {
                writeAllowedBytes(child, state(child).unallocatedPriorityBytes());
            }
            return;
        }

        // Copy and sort the children of this node. They are sorted in ascending order the total
        // priority bytes for the subtree scaled by the weight of the node. The algorithm gives
        // preference to nodes that appear later in the list, since the weight of each node
        // increases in value as the list is iterated. This means that with this node ordering,
        // the most bytes will be written to those nodes with the largest aggregate number of
        // bytes and the highest priority.
        List<Http2Stream> states = new ArrayList<Http2Stream>(stream.children());
        Collections.sort(states, DATA_WEIGHT);

        // Iterate over the children and spread the remaining bytes across them as is appropriate
        // based on the weights. This algorithm loops over all of the children more than once,
        // although it should typically only take a few passes to complete. In each pass we
        // give a node its share of the current remaining bytes. The node's weight and bytes
        // allocated are then decremented from the totals, so that the subsequent
        // nodes split the difference. If after being processed, a node still has writable data,
        // it is added back to the queue for further processing in the next pass.
        int remainingWeight = stream.totalChildWeights();
        int nextTail = 0;
        int unallocatedBytesForNextPass = 0;
        int remainingWeightForNextPass = 0;
        for (int head = 0, tail = states.size();; ++head) {
            if (head >= tail) {
                // We've reached the end one pass of the nodes. Reset the totals based on
                // the nodes that were re-added to the deque since they still have data available.
                unallocatedBytes = unallocatedBytesForNextPass;
                remainingWeight = remainingWeightForNextPass;
                unallocatedBytesForNextPass = 0;
                remainingWeightForNextPass = 0;
                head = 0;
                tail = nextTail;
                nextTail = 0;
            }

            // Get the next state, or break if nothing to do.
            if (head >= tail) {
                break;
            }
            Http2Stream next = states.get(head);
            OutboundFlowState nextState = state(next);
            int weight = next.weight();

            // Determine the value (in bytes) of a single unit of weight.
            double dataToWeightRatio = min(unallocatedBytes, remainingWindow) / (double) remainingWeight;
            unallocatedBytes -= nextState.unallocatedPriorityBytes();
            remainingWeight -= weight;

            if (dataToWeightRatio > 0.0 && nextState.unallocatedPriorityBytes() > 0) {

                // Determine the portion of the current writable data that is assigned to this
                // node.
                int writableChunk = (int) (weight * dataToWeightRatio);

                // Clip the chunk allocated by the total amount of unallocated data remaining in
                // the node.
                int allocatedChunk = min(writableChunk, nextState.unallocatedPriorityBytes());

                // Update the remaining connection window size.
                remainingWindow -= allocatedChunk;

                // Mark these bytes as allocated.
                nextState.allocatePriorityBytes(allocatedChunk);
                if (nextState.unallocatedPriorityBytes() > 0) {
                    // There is still data remaining for this stream. Add it back to the queue
                    // for the next pass.
                    unallocatedBytesForNextPass += nextState.unallocatedPriorityBytes();
                    remainingWeightForNextPass += weight;
                    states.set(nextTail++, next);
                    continue;
                }
            }

            if (nextState.allocatedPriorityBytes() > 0) {
                // Write the allocated data for this stream.
                writeAllowedBytes(next, nextState.allocatedPriorityBytes());

                // We're done with this node. Remark all bytes as unallocated for future
                // invocations.
                nextState.allocatePriorityBytes(0);
            }
        }
    }

    /**
     * The outbound flow control state for a single stream.
     */
    final class OutboundFlowState implements FlowState {
        private final Queue<Frame> pendingWriteQueue;
        private final Http2Stream stream;
        private int window = initialWindowSize;
        private int pendingBytes;
        private int priorityBytes;
        private int allocatedPriorityBytes;

        private OutboundFlowState(Http2Stream stream) {
            this.stream = stream;
            pendingWriteQueue = new ArrayDeque<Frame>(2);
        }

        @Override
        public int window() {
            return window;
        }

        /**
         * Increments the flow control window for this stream by the given delta and returns the new value.
         */
        private int incrementStreamWindow(int delta) throws Http2Exception {
            if (delta > 0 && Integer.MAX_VALUE - delta < window) {
                throw new Http2StreamException(stream.id(), FLOW_CONTROL_ERROR, "Window size overflow for stream: "
                        + stream.id());
            }
            int previouslyStreamable = streamableBytes();
            window += delta;

            // Update this branch of the priority tree if the streamable bytes have changed for this
            // node.
            incrementPriorityBytes(streamableBytes() - previouslyStreamable);
            return window;
        }

        /**
         * Returns the maximum writable window (minimum of the stream and connection windows).
         */
        int writableWindow() {
            return min(window, connectionWindow());
        }

        /**
         * Returns the number of pending bytes for this node that will fit within the {@link #window}. This is used for
         * the priority algorithm to determine the aggregate total for {@link #priorityBytes} at each node. Each node
         * only takes into account it's stream window so that when a change occurs to the connection window, these
         * values need not change (i.e. no tree traversal is required).
         */
        int streamableBytes() {
            return max(0, min(pendingBytes, window));
        }

        /**
         * The aggregate total of all {@link #streamableBytes()} for subtree rooted at this node.
         */
        int priorityBytes() {
            return priorityBytes;
        }

        /**
         * Used by the priority algorithm to allocate bytes to this stream.
         */
        private void allocatePriorityBytes(int bytes) {
            allocatedPriorityBytes += bytes;
        }

        /**
         * Used by the priority algorithm to get the intermediate allocation of bytes to this stream.
         */
        int allocatedPriorityBytes() {
            return allocatedPriorityBytes;
        }

        /**
         * Used by the priority algorithm to determine the number of writable bytes that have not yet been allocated.
         */
        private int unallocatedPriorityBytes() {
            return priorityBytes - allocatedPriorityBytes;
        }

        /**
         * Creates a new frame with the given values but does not add it to the pending queue.
         */
        private Frame newFrame(ChannelPromise promise, ByteBuf data, int padding, boolean endStream) {
            return new Frame(new ChannelPromiseAggregator(promise), data, padding, endStream);
        }

        /**
         * Indicates whether or not there are frames in the pending queue.
         */
        boolean hasFrame() {
            return !pendingWriteQueue.isEmpty();
        }

        /**
         * Returns the the head of the pending queue, or {@code null} if empty.
         */
        Frame peek() {
            return pendingWriteQueue.peek();
        }

        /**
         * Clears the pending queue and writes errors for each remaining frame.
         */
        private void clear() {
            for (;;) {
                Frame frame = pendingWriteQueue.poll();
                if (frame == null) {
                    break;
                }
                frame.writeError(format(STREAM_CLOSED, "Stream closed before write could take place"));
            }
        }

        /**
         * Writes up to the number of bytes from the pending queue. May write less if limited by the writable window, by
         * the number of pending writes available, or because a frame does not support splitting on arbitrary
         * boundaries.
         */
        private int writeBytes(int bytes) throws Http2Exception {
            int bytesWritten = 0;
            if (!stream.localSideOpen()) {
                return bytesWritten;
            }

            int maxBytes = min(bytes, writableWindow());
            while (hasFrame()) {
                Frame pendingWrite = peek();
                if (maxBytes >= pendingWrite.size()) {
                    // Window size is large enough to send entire data frame
                    bytesWritten += pendingWrite.size();
                    pendingWrite.write();
                } else if (maxBytes <= 0) {
                    // No data from the current frame can be written - we're done.
                    // We purposely check this after first testing the size of the
                    // pending frame to properly handle zero-length frame.
                    break;
                } else {
                    // We can send a partial frame
                    Frame partialFrame = pendingWrite.split(maxBytes);
                    bytesWritten += partialFrame.size();
                    partialFrame.write();
                }

                // Update the threshold.
                maxBytes = min(bytes - bytesWritten, writableWindow());
            }
            return bytesWritten;
        }

        /**
         * Recursively increments the priority bytes for this branch in the priority tree starting at the current node.
         */
        private void incrementPriorityBytes(int numBytes) {
            if (numBytes != 0) {
                priorityBytes += numBytes;
                if (!stream.isRoot()) {
                    state(stream.parent()).incrementPriorityBytes(numBytes);
                }
            }
        }

        /**
         * A wrapper class around the content of a data frame.
         */
        private final class Frame {
            final ByteBuf data;
            final boolean endStream;
            final ChannelPromiseAggregator promiseAggregator;
            final ChannelPromise promise;
            int padding;
            boolean enqueued;

            Frame(ChannelPromiseAggregator promiseAggregator, ByteBuf data, int padding,
                    boolean endStream) {
                this.data = data;
                this.padding = padding;
                this.endStream = endStream;
                this.promiseAggregator = promiseAggregator;
                this.promise = ctx.newPromise();
                promiseAggregator.add(promise);
            }

            /**
             * Gets the total size (in bytes) of this frame including the data and padding.
             */
            int size() {
                return data.readableBytes() + padding;
            }

            void enqueue() {
                if (!enqueued) {
                    enqueued = true;
                    pendingWriteQueue.offer(this);

                    // Increment the number of pending bytes for this stream.
                    incrementPendingBytes(size());
                }
            }

            /**
             * Increments the number of pending bytes for this node. If there was any change to the number of bytes that
             * fit into the stream window, then {@link #incrementPriorityBytes} to recursively update this branch of the
             * priority tree.
             */
            private void incrementPendingBytes(int numBytes) {
                int previouslyStreamable = streamableBytes();
                pendingBytes += numBytes;

                int delta = streamableBytes() - previouslyStreamable;
                incrementPriorityBytes(delta);
            }

            /**
             * Writes the frame and decrements the stream and connection window sizes. If the frame
             * is in the pending queue, the written bytes are removed from this branch of the
             * priority tree.
             * <p>
             * Note: this does not flush the {@link ChannelHandlerContext}.
             */
            void write() throws Http2Exception {
                // Using a do/while loop because if the buffer is empty we still need to call
                // the writer once to send the empty frame.
                final Http2FrameSizePolicy frameSizePolicy = frameWriter.configuration().frameSizePolicy();
                do {
                    int bytesToWrite = size();
                    int frameBytes = Math.min(bytesToWrite, frameSizePolicy.maxFrameSize());
                    if (frameBytes == bytesToWrite) {
                        // All the bytes fit into a single HTTP/2 frame, just send it all.
                        connectionState().incrementStreamWindow(-bytesToWrite);
                        incrementStreamWindow(-bytesToWrite);
                        frameWriter.writeData(ctx, stream.id(), data, padding, endStream, promise);
                        decrementPendingBytes(bytesToWrite);
                        if (enqueued) {
                            // It's enqueued - remove it from the head of the pending write queue.
                            pendingWriteQueue.remove();
                        }
                        return;
                    }

                    // Split a chunk that will fit into a single HTTP/2 frame and write it.
                    Frame frame = split(frameBytes);
                    frame.write();
                } while (size() > 0);
            }

            /**
             * Discards this frame, writing an error. If this frame is in the pending queue, the unwritten bytes are
             * removed from this branch of the priority tree.
             */
            void writeError(Http2Exception cause) {
                decrementPendingBytes(size());
                data.release();
                promise.setFailure(cause);
            }

            /**
             * Creates a new frame that is a view of this frame's data. The {@code maxBytes} are
             * first split from the data buffer. If not all the requested bytes are available, the
             * remaining bytes are then split from the padding (if available).
             *
             * @param maxBytes
             *            the maximum number of bytes that is allowed in the created frame.
             * @return the partial frame.
             */
            Frame split(int maxBytes) {
                // TODO: Should padding be spread across chunks or only at the end?

                // The requested maxBytes should always be less than the size of this frame.
                assert maxBytes < size() : "Attempting to split a frame for the full size.";

                // Get the portion of the data buffer to be split. Limit to the readable bytes.
                int dataSplit = min(maxBytes, data.readableBytes());

                // Split any remaining bytes from the padding.
                int paddingSplit = min(maxBytes - dataSplit, padding);

                ByteBuf splitSlice = data.readSlice(dataSplit).retain();
                padding -= paddingSplit;

                Frame frame = new Frame(promiseAggregator, splitSlice, paddingSplit, false);

                int totalBytesSplit = dataSplit + paddingSplit;
                decrementPendingBytes(totalBytesSplit);
                return frame;
            }

            /**
             * If this frame is in the pending queue, decrements the number of pending bytes for the stream.
             */
            void decrementPendingBytes(int bytes) {
                if (enqueued) {
                    incrementPendingBytes(-bytes);
                }
            }
        }
    }
}
TOP

Related Classes of io.netty.handler.codec.http2.DefaultHttp2OutboundFlowController$OutboundFlowState$Frame

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.