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

org.eclipse.jetty.http2.HTTP2Session Maven / Gradle / Ivy

The newest version!
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//

package org.eclipse.jetty.http2;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.FailureFrame;
import org.eclipse.jetty.http2.frames.Frame;
import org.eclipse.jetty.http2.frames.FrameType;
import org.eclipse.jetty.http2.frames.GoAwayFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.frames.PingFrame;
import org.eclipse.jetty.http2.frames.PrefaceFrame;
import org.eclipse.jetty.http2.frames.PriorityFrame;
import org.eclipse.jetty.http2.frames.PushPromiseFrame;
import org.eclipse.jetty.http2.frames.ResetFrame;
import org.eclipse.jetty.http2.frames.SettingsFrame;
import org.eclipse.jetty.http2.frames.StreamFrame;
import org.eclipse.jetty.http2.frames.WindowUpdateFrame;
import org.eclipse.jetty.http2.generator.Generator;
import org.eclipse.jetty.http2.hpack.HpackEncoder;
import org.eclipse.jetty.http2.hpack.HpackException;
import org.eclipse.jetty.http2.internal.HTTP2Flusher;
import org.eclipse.jetty.http2.parser.Parser;
import org.eclipse.jetty.io.ByteBufferPool;
import org.eclipse.jetty.io.CyclicTimeouts;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.util.AtomicBiInteger;
import org.eclipse.jetty.util.Atomics;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.CountingCallback;
import org.eclipse.jetty.util.MathUtils;
import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.Promise;
import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.eclipse.jetty.util.annotation.ManagedObject;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.component.DumpableCollection;
import org.eclipse.jetty.util.thread.AutoLock;
import org.eclipse.jetty.util.thread.Invocable;
import org.eclipse.jetty.util.thread.Scheduler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ManagedObject
public abstract class HTTP2Session extends ContainerLifeCycle implements Session, Parser.Listener
{
    private static final Logger LOG = LoggerFactory.getLogger(HTTP2Session.class);

    private final Map streams = new ConcurrentHashMap<>();
    private final AtomicLong streamsOpened = new AtomicLong();
    private final AtomicLong streamsClosed = new AtomicLong();
    private final StreamsState streamsState = new StreamsState();
    private final AtomicInteger localStreamIds = new AtomicInteger();
    private final AtomicInteger lastRemoteStreamId = new AtomicInteger();
    private final AtomicInteger localStreamCount = new AtomicInteger();
    private final AtomicBiInteger remoteStreamCount = new AtomicBiInteger();
    private final AtomicInteger sendWindow = new AtomicInteger();
    private final AtomicInteger recvWindow = new AtomicInteger();
    private final AtomicLong bytesWritten = new AtomicLong();
    private final EndPoint endPoint;
    private final Parser parser;
    private final Generator generator;
    private final Session.Listener listener;
    private final FlowControlStrategy flowControl;
    private final HTTP2Flusher flusher;
    private final StreamTimeouts streamTimeouts;
    private int maxLocalStreams;
    private int maxRemoteStreams;
    private long streamIdleTimeout;
    private int initialSessionRecvWindow;
    private int writeThreshold;
    private int maxEncoderTableCapacity;
    private boolean pushEnabled;
    private boolean connectProtocolEnabled;

    public HTTP2Session(Scheduler scheduler, EndPoint endPoint, Parser parser, Generator generator, Session.Listener listener, FlowControlStrategy flowControl, int initialStreamId)
    {
        this.endPoint = endPoint;
        this.parser = parser;
        this.generator = generator;
        this.listener = listener;
        this.flowControl = flowControl;
        this.flusher = new HTTP2Flusher(this);
        this.streamTimeouts = new StreamTimeouts(scheduler);
        this.maxLocalStreams = -1;
        this.maxRemoteStreams = -1;
        this.localStreamIds.set(initialStreamId);
        this.sendWindow.set(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
        this.recvWindow.set(FlowControlStrategy.DEFAULT_WINDOW_SIZE);
        this.writeThreshold = 32 * 1024;
        this.pushEnabled = true; // SPEC: by default, push is enabled.
        installBean(flowControl);
        installBean(flusher);
    }

    @Override
    protected void doStop() throws Exception
    {
        super.doStop();
        streamsState.halt("stop");
    }

    @ManagedAttribute(value = "The flow control strategy", readonly = true)
    public FlowControlStrategy getFlowControlStrategy()
    {
        return flowControl;
    }

    @ManagedAttribute(value = "The total number of streams opened", readonly = true)
    public long getStreamsOpened()
    {
        return streamsOpened.get();
    }

    @ManagedAttribute(value = "The total number of streams closed", readonly = true)
    public long getStreamsClosed()
    {
        return streamsClosed.get();
    }

    @ManagedAttribute("The maximum number of concurrent local streams")
    public int getMaxLocalStreams()
    {
        return maxLocalStreams;
    }

    public void setMaxLocalStreams(int maxLocalStreams)
    {
        this.maxLocalStreams = maxLocalStreams;
    }

    @ManagedAttribute("The maximum number of concurrent remote streams")
    public int getMaxRemoteStreams()
    {
        return maxRemoteStreams;
    }

    public void setMaxRemoteStreams(int maxRemoteStreams)
    {
        this.maxRemoteStreams = maxRemoteStreams;
    }

    @ManagedAttribute("The stream's idle timeout")
    public long getStreamIdleTimeout()
    {
        return streamIdleTimeout;
    }

    public void setStreamIdleTimeout(long streamIdleTimeout)
    {
        this.streamIdleTimeout = streamIdleTimeout;
    }

    @ManagedAttribute("The initial size of session's flow control receive window")
    public int getInitialSessionRecvWindow()
    {
        return initialSessionRecvWindow;
    }

    public void setInitialSessionRecvWindow(int initialSessionRecvWindow)
    {
        this.initialSessionRecvWindow = initialSessionRecvWindow;
    }

    @ManagedAttribute("The number of bytes that trigger a TCP write")
    public int getWriteThreshold()
    {
        return writeThreshold;
    }

    public void setWriteThreshold(int writeThreshold)
    {
        this.writeThreshold = writeThreshold;
    }

    @ManagedAttribute("The HPACK encoder dynamic table maximum capacity")
    public int getMaxEncoderTableCapacity()
    {
        return maxEncoderTableCapacity;
    }

    public void setMaxEncoderTableCapacity(int maxEncoderTableCapacity)
    {
        this.maxEncoderTableCapacity = maxEncoderTableCapacity;
    }

    public EndPoint getEndPoint()
    {
        return endPoint;
    }

    public Parser getParser()
    {
        return parser;
    }

    public Generator getGenerator()
    {
        return generator;
    }

    public long getBytesWritten()
    {
        return bytesWritten.get();
    }

    @Override
    public void onData(DataFrame frame)
    {
        // This method should never be called, the one below should.
        throw new UnsupportedOperationException();
    }

    public void onData(Stream.Data data)
    {
        if (LOG.isDebugEnabled())
            LOG.debug("Received {} on {}", data, this);

        DataFrame frame = data.frame();
        int streamId = frame.getStreamId();
        HTTP2Stream stream = getStream(streamId);

        // SPEC: the session window must be updated even if the stream is null.
        // The flow control length includes the padding bytes.
        int flowControlLength = frame.flowControlLength();
        flowControl.onDataReceived(this, stream, flowControlLength);

        if (stream != null)
        {
            if (getRecvWindow() < 0)
            {
                onSessionFailure(ErrorCode.FLOW_CONTROL_ERROR.code, "session_window_exceeded", Callback.NOOP);
            }
            else
            {
                if (stream.updateRecvWindow(0) < 0)
                {
                    // It's a bad client, it does not deserve to be
                    // treated gently by just resetting the stream.
                    onSessionFailure(ErrorCode.FLOW_CONTROL_ERROR.code, "stream_window_exceeded", Callback.NOOP);
                }
                else
                {
                    stream.process(data);
                }
            }
        }
        else
        {
            if (LOG.isDebugEnabled())
                LOG.debug("Stream #{} not found on {}", streamId, this);
            // We must enlarge the session flow control window,
            // otherwise other requests will be stalled.
            dataConsumed(null, flowControlLength);
            if (isStreamClosed(streamId))
                reset(null, new ResetFrame(streamId, ErrorCode.STREAM_CLOSED_ERROR.code), Callback.NOOP);
            else
                onSessionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_data_frame", Callback.NOOP);
        }
    }

    void dataConsumed(HTTP2Stream stream, int length)
    {
        notIdle();
        flowControl.onDataConsumed(this, stream, length);
    }

    private boolean isStreamClosed(int streamId)
    {
        return isLocalStream(streamId) ? isLocalStreamClosed(streamId) : isRemoteStreamClosed(streamId);
    }

    private boolean isLocalStream(int streamId)
    {
        return (streamId & 1) == (localStreamIds.get() & 1);
    }

    protected boolean isLocalStreamClosed(int streamId)
    {
        return streamId <= localStreamIds.get();
    }

    protected boolean isRemoteStreamClosed(int streamId)
    {
        return streamId <= getLastRemoteStreamId();
    }

    @Override
    public abstract void onHeaders(HeadersFrame frame);

    @Override
    public void onPriority(PriorityFrame frame)
    {
        if (LOG.isDebugEnabled())
            LOG.debug("Received {} on {}", frame, this);
    }

    @Override
    public void onReset(ResetFrame frame)
    {
        int streamId = frame.getStreamId();
        HTTP2Stream stream = getStream(streamId);

        if (LOG.isDebugEnabled())
            LOG.debug("Received {} for {} on {}", frame, stream, this);

        if (stream != null)
        {
            stream.process(frame, new OnResetCallback());
        }
        else
        {
            onResetForUnknownStream(frame);
        }
    }

    protected void onResetForUnknownStream(ResetFrame frame)
    {
        if (isStreamClosed(frame.getStreamId()))
            notifyReset(this, frame);
        else
            onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_rst_stream_frame");
    }

    @Override
    public void onSettings(SettingsFrame frame)
    {
        // SPEC: SETTINGS frame MUST be replied.
        onSettings(frame, true);
    }

    public void onSettings(SettingsFrame frame, boolean reply)
    {
        if (LOG.isDebugEnabled())
            LOG.debug("Received {} on {}", frame, this);

        if (frame.isReply())
            return;

        Map settings = frame.getSettings();
        configure(settings, false);
        notifySettings(this, frame);

        if (reply)
        {
            SettingsFrame replyFrame = new SettingsFrame(Collections.emptyMap(), true);
            settings(replyFrame, Callback.NOOP);
        }
    }

    private void configure(Map settings, boolean local)
    {
        for (Map.Entry entry : settings.entrySet())
        {
            int key = entry.getKey();
            int value = entry.getValue();
            switch (key)
            {
                case SettingsFrame.HEADER_TABLE_SIZE ->
                {
                    if (LOG.isDebugEnabled())
                        LOG.debug("Updating HPACK {} max table capacity to {} for {}", local ? "decoder" : "encoder", value, this);
                    if (local)
                    {
                        parser.getHpackDecoder().setMaxTableCapacity(value);
                    }
                    else
                    {
                        HpackEncoder hpackEncoder = generator.getHpackEncoder();
                        hpackEncoder.setMaxTableCapacity(value);
                        hpackEncoder.setTableCapacity(Math.min(value, getMaxEncoderTableCapacity()));
                    }
                }
                case SettingsFrame.ENABLE_PUSH ->
                {
                    boolean enabled = value == 1;
                    if (LOG.isDebugEnabled())
                        LOG.debug("{} push for {}", enabled ? "Enabling" : "Disabling", this);
                    pushEnabled = enabled;
                }
                case SettingsFrame.MAX_CONCURRENT_STREAMS ->
                {
                    if (LOG.isDebugEnabled())
                        LOG.debug("Updating max {} concurrent streams to {} for {}", local ? "remote" : "local", value, this);
                    if (local)
                        maxRemoteStreams = value;
                    else
                        maxLocalStreams = value;
                }
                case SettingsFrame.INITIAL_WINDOW_SIZE ->
                {
                    if (LOG.isDebugEnabled())
                        LOG.debug("Updating initial stream window size to {} for {}", value, this);
                    flowControl.updateInitialStreamWindow(this, value, local);
                }
                case SettingsFrame.MAX_FRAME_SIZE ->
                {
                    if (LOG.isDebugEnabled())
                        LOG.debug("Updating {} max frame size to {} for {}", local ? "parser" : "generator", value, this);
                    if (local)
                        parser.setMaxFrameSize(value);
                    else
                        generator.setMaxFrameSize(value);
                }
                case SettingsFrame.MAX_HEADER_LIST_SIZE ->
                {
                    if (LOG.isDebugEnabled())
                        LOG.debug("Updating {} max header list size to {} for {}", local ? "decoder" : "encoder", value, this);
                    if (local)
                        parser.getHpackDecoder().setMaxHeaderListSize(value);
                    else
                        generator.getHpackEncoder().setMaxHeaderListSize(value);
                }
                case SettingsFrame.ENABLE_CONNECT_PROTOCOL ->
                {
                    boolean enabled = value == 1;
                    if (LOG.isDebugEnabled())
                        LOG.debug("{} CONNECT protocol for {}", enabled ? "Enabling" : "Disabling", this);
                    connectProtocolEnabled = enabled;
                }
                default ->
                {
                    if (LOG.isDebugEnabled())
                        LOG.debug("Unknown setting {}:{} for {}", key, value, this);
                }
            }
        }
    }

    @Override
    public void onPing(PingFrame frame)
    {
        if (LOG.isDebugEnabled())
            LOG.debug("Received {} on {}", frame, this);

        if (frame.isReply())
        {
            notifyPing(this, frame);
        }
        else
        {
            PingFrame reply = new PingFrame(frame.getPayload(), true);
            control(null, Callback.NOOP, reply);
        }
    }

    /**
     * 

This method is called when receiving a GO_AWAY from the other peer.

* * @param frame the GO_AWAY frame that has been received. * @see #close(int, String, Callback) * @see #onShutdown() * @see #onIdleTimeout() */ @Override public void onGoAway(GoAwayFrame frame) { if (LOG.isDebugEnabled()) LOG.debug("Received {} on {}", frame, this); streamsState.onGoAway(frame); } @Override public void onWindowUpdate(WindowUpdateFrame frame) { if (LOG.isDebugEnabled()) LOG.debug("Received {} on {}", frame, this); int streamId = frame.getStreamId(); int windowDelta = frame.getWindowDelta(); if (streamId > 0) { HTTP2Stream stream = getStream(streamId); if (stream != null) { int streamSendWindow = stream.updateSendWindow(0); if (MathUtils.sumOverflows(streamSendWindow, windowDelta)) { reset(stream, new ResetFrame(streamId, ErrorCode.FLOW_CONTROL_ERROR.code), Callback.NOOP); } else { stream.process(frame, Callback.NOOP); onWindowUpdate(stream, frame); } } else { if (!isStreamClosed(streamId)) onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "unexpected_window_update_frame"); } } else { int sessionSendWindow = updateSendWindow(0); if (MathUtils.sumOverflows(sessionSendWindow, windowDelta)) onConnectionFailure(ErrorCode.FLOW_CONTROL_ERROR.code, "invalid_flow_control_window"); else onWindowUpdate(null, frame); } } public void onWindowUpdate(HTTP2Stream stream, WindowUpdateFrame frame) { // WindowUpdateFrames arrive concurrently with writes. // Increasing (or reducing) the window size concurrently // with writes requires coordination with the flusher, that // decides how many frames to write depending on the available // window sizes. If the window sizes vary concurrently, the // flusher may take non-optimal or wrong decisions. // Here, we "queue" window updates to the flusher, so it will // be the only component responsible for window updates, for // both increments and reductions. flusher.window(stream, frame); } @Override public void onStreamFailure(int streamId, int error, String reason) { Callback callback = Callback.from(() -> reset(getStream(streamId), new ResetFrame(streamId, error), Callback.NOOP)); Throwable failure = toFailure(error, reason); if (LOG.isDebugEnabled()) LOG.debug("Stream #{} failure {}", streamId, this, failure); HTTP2Stream stream = getStream(streamId); if (stream != null) failStream(stream, error, reason, failure, callback); else callback.succeeded(); } @Override public void onConnectionFailure(int error, String reason) { onSessionFailure(error, reason, Callback.NOOP); } private void onSessionFailure(int error, String reason, Callback callback) { streamsState.onSessionFailure(error, reason, callback); } public void onWriteFailure(Throwable failure) { streamsState.onWriteFailure(failure); } protected void abort(String reason, Throwable failure, Callback callback) { if (LOG.isDebugEnabled()) LOG.debug("Session abort {} for {}", reason, this, failure); onFailure(ErrorCode.NO_ERROR.code, reason, failure, callback); } private void onFailure(int error, String reason, Throwable failure, Callback callback) { Collection streams = getStreams(); int count = streams.size(); Callback countCallback = new CountingCallback(callback, count + 1); for (Stream stream : streams) { if (stream.isClosed()) countCallback.succeeded(); else failStream(stream, error, reason, failure, countCallback); } notifyFailure(this, failure, countCallback); } private void failStreams(Predicate matcher, String reason, boolean reset) { int error = ErrorCode.CANCEL_STREAM_ERROR.code; Throwable failure = toFailure(error, reason); for (Stream stream : getStreams()) { if (stream.isClosed()) continue; if (!matcher.test(stream)) continue; if (LOG.isDebugEnabled()) LOG.debug("Failing stream {} of {}", stream, this); failStream(stream, error, reason, failure, Callback.NOOP); if (reset) stream.reset(new ResetFrame(stream.getId(), error), Callback.NOOP); } } private void failStream(Stream stream, int error, String reason, Throwable failure, Callback callback) { ((HTTP2Stream)stream).process(new FailureFrame(error, reason, failure), callback); } private Throwable toFailure(int error, String reason) { return new IOException(String.format("%s/%s", ErrorCode.toString(error, null), reason)); } @Override public void newStream(HeadersFrame frame, Promise promise, Stream.Listener listener) { newStream(new HTTP2Stream.FrameList(frame), promise, listener); } public void newStream(HTTP2Stream.FrameList frames, Promise promise, Stream.Listener listener) { streamsState.newLocalStream(frames, promise, listener); } /** *

Creates a new stream allocating a stream id if the given HEADERS frame does not have one.

* * @param frame the HEADERS frame that triggered the stream creation * allocated stream id, or null if not interested in the modified headers frame * @param listener the listener that gets notified of stream events */ public Stream newUpgradeStream(HeadersFrame frame, Stream.Listener listener, Consumer failFn) { return streamsState.newUpgradeStream(frame, listener, failFn); } protected HTTP2Stream newStream(int streamId, MetaData.Request request, boolean local) { return new HTTP2Stream(this, streamId, request, local); } @Override public int priority(PriorityFrame frame, Callback callback) { return streamsState.priority(frame, callback); } public void push(Stream stream, Promise promise, PushPromiseFrame frame, Stream.Listener listener) { if (!isPushEnabled()) throw new IllegalStateException("Push is disabled"); streamsState.push(frame, new Promise.Wrapper<>(promise) { @Override public void succeeded(Stream pushed) { // Pushed streams are implicitly remotely closed. // They are closed when sending an end-stream DATA frame. HTTP2Stream http2Pushed = (HTTP2Stream)pushed; http2Pushed.process(Stream.Data.eof(pushed.getId())); http2Pushed.updateClose(true, CloseState.Event.RECEIVED); super.succeeded(pushed); } }, listener); } @Override public void settings(SettingsFrame frame, Callback callback) { control(null, callback, frame); } @Override public void ping(PingFrame frame, Callback callback) { if (frame.isReply()) callback.failed(new IllegalArgumentException()); else control(null, callback, frame); } void reset(HTTP2Stream stream, ResetFrame frame, Callback callback) { control(stream, Callback.from(() -> { if (stream != null) { stream.close(); removeStream(stream); } }, callback), frame); } /** *

Invoked internally and by applications to send a GO_AWAY frame to the other peer.

* * @param error the error code * @param reason the reason * @param callback the callback to invoke when the operation is complete * @see #onGoAway(GoAwayFrame) * @see #onShutdown() * @see #onIdleTimeout() */ @Override public boolean close(int error, String reason, Callback callback) { if (LOG.isDebugEnabled()) LOG.debug("Closing {}/{} {}", ErrorCode.toString(error, null), reason, this); return goAway(newGoAwayFrame(error, reason), callback); } @Override public CompletableFuture shutdown() { return streamsState.shutdown(); } public boolean goAway(GoAwayFrame frame, Callback callback) { return streamsState.goAway(frame, callback); } private GoAwayFrame newGoAwayFrame(int error, String reason) { return newGoAwayFrame(getLastRemoteStreamId(), error, reason); } private GoAwayFrame newGoAwayFrame(int lastRemoteStreamId, int error, String reason) { byte[] payload = null; if (reason != null) { // Trim the reason to avoid attack vectors. reason = reason.substring(0, Math.min(reason.length(), 32)); payload = reason.getBytes(StandardCharsets.UTF_8); } return new GoAwayFrame(lastRemoteStreamId, error, payload); } @Override public boolean isClosed() { return getCloseState() != CloseState.NOT_CLOSED; } public CloseState getCloseState() { return streamsState.getCloseState(); } private void control(HTTP2Stream stream, Callback callback, Frame frame) { frames(stream, List.of(frame), callback); } public void frames(HTTP2Stream stream, List frames, Callback callback) { // We want to generate as late as possible to allow re-prioritization; // generation will happen while processing the entries. // The callback needs to be notified only when the last frame completes. int count = frames.size(); if (count > 1) callback = new CountingCallback(callback, count); for (int i = 1; i <= count; ++i) { Frame frame = frames.get(i - 1); Entry entry = newEntry(frame, stream, callback); frame(entry, i == count); } } private Entry newEntry(Frame frame, HTTP2Stream stream, Callback callback) { return frame.getType() == FrameType.DATA ? new DataEntry((DataFrame)frame, stream, callback) : new ControlEntry(frame, stream, callback); } public void data(HTTP2Stream stream, DataFrame frame, Callback callback) { // We want to generate as late as possible to allow re-prioritization. frame(newEntry(frame, stream, callback), true); } private void frame(Entry entry, boolean flush) { if (LOG.isDebugEnabled()) LOG.debug("{} {} on {}", flush ? "Sending" : "Queueing", entry, this); // Ping frames are prepended to process them as soon as possible. boolean queued = entry.hasHighPriority() ? flusher.prepend(entry) : flusher.append(entry); if (queued && flush) { if (entry.stream != null) entry.stream.notIdle(); flusher.iterate(); } } protected HTTP2Stream createLocalStream(int streamId, MetaData.Request request, Consumer failFn) { while (true) { int localCount = localStreamCount.get(); int maxCount = getMaxLocalStreams(); if (maxCount >= 0 && localCount >= maxCount) { IllegalStateException failure = new IllegalStateException("Max local stream count " + maxCount + " exceeded: " + localCount); if (LOG.isDebugEnabled()) LOG.debug("Could not create local stream #{} for {}", streamId, this, failure); failFn.accept(failure); return null; } if (localStreamCount.compareAndSet(localCount, localCount + 1)) break; } HTTP2Stream stream = newStream(streamId, request, true); if (streams.putIfAbsent(streamId, stream) == null) { stream.setIdleTimeout(getStreamIdleTimeout()); flowControl.onStreamCreated(stream); if (LOG.isDebugEnabled()) LOG.debug("Created local {} for {}", stream, this); return stream; } else { localStreamCount.decrementAndGet(); failFn.accept(new IllegalStateException("Duplicate stream " + streamId)); return null; } } protected HTTP2Stream createRemoteStream(int streamId, MetaData.Request request) { // This stream has been seen the server. // Even if the stream cannot be created because this peer is closing, // updating the lastRemoteStreamId ensures that in-flight HEADERS and // DATA frames can be read (and discarded) without causing an error. updateLastRemoteStreamId(streamId); if (!streamsState.newRemoteStream(streamId)) { if (LOG.isDebugEnabled()) LOG.debug("Could not create remote stream #{} for {}", streamId, this); return null; } // SPEC: exceeding max concurrent streams is treated as stream error. while (true) { long encoded = remoteStreamCount.get(); int remoteCount = AtomicBiInteger.getHi(encoded); int remoteClosing = AtomicBiInteger.getLo(encoded); int maxCount = getMaxRemoteStreams(); if (maxCount >= 0 && remoteCount - remoteClosing >= maxCount) { IllegalStateException failure = new IllegalStateException("Max remote stream count " + maxCount + " exceeded: " + remoteCount + "+" + remoteClosing); if (LOG.isDebugEnabled()) LOG.debug("Could not create remote stream #{} for {}", streamId, this, failure); reset(null, new ResetFrame(streamId, ErrorCode.REFUSED_STREAM_ERROR.code), Callback.from(() -> onStreamDestroyed(streamId))); return null; } if (remoteStreamCount.compareAndSet(encoded, remoteCount + 1, remoteClosing)) break; } HTTP2Stream stream = newStream(streamId, request, false); if (streams.putIfAbsent(streamId, stream) == null) { stream.setIdleTimeout(getStreamIdleTimeout()); flowControl.onStreamCreated(stream); if (LOG.isDebugEnabled()) LOG.debug("Created remote {} for {}", stream, this); return stream; } else { remoteStreamCount.addAndGetHi(-1); onStreamDestroyed(streamId); // SPEC: duplicate stream is treated as connection error. onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "duplicate_stream"); return null; } } void updateStreamCount(boolean local, int deltaStreams, int deltaClosing) { if (local) localStreamCount.addAndGet(deltaStreams); else remoteStreamCount.add(deltaStreams, deltaClosing); } public boolean removeStream(Stream stream) { int streamId = stream.getId(); HTTP2Stream removed = streams.remove(streamId); if (removed == null) return false; if (LOG.isDebugEnabled()) LOG.debug("Removed {} {} from {}", stream.isLocal() ? "local" : "remote", stream, this); onStreamClosed(stream); flowControl.onStreamDestroyed(stream); onStreamDestroyed(streamId); return true; } @Override public Collection getStreams() { return new ArrayList<>(streams.values()); } @ManagedAttribute("The number of active streams") public int getStreamCount() { return streamsState.streamCount.intValue(); } @Override public HTTP2Stream getStream(int streamId) { return streams.get(streamId); } @Override public InetSocketAddress getLocalAddress() { SocketAddress local = getLocalSocketAddress(); if (local instanceof InetSocketAddress) return (InetSocketAddress)local; return null; } @Override public SocketAddress getLocalSocketAddress() { return endPoint.getLocalSocketAddress(); } @Override public InetSocketAddress getRemoteAddress() { SocketAddress remote = getRemoteSocketAddress(); if (remote instanceof InetSocketAddress) return (InetSocketAddress)remote; return null; } @Override public SocketAddress getRemoteSocketAddress() { return endPoint.getRemoteSocketAddress(); } @ManagedAttribute(value = "The flow control send window", readonly = true) public int getSendWindow() { return sendWindow.get(); } @ManagedAttribute(value = "The flow control receive window", readonly = true) public int getRecvWindow() { return recvWindow.get(); } public int updateSendWindow(int delta) { return sendWindow.getAndAdd(delta); } public int updateRecvWindow(int delta) { return recvWindow.getAndAdd(delta); } @Override @ManagedAttribute(value = "Whether HTTP/2 push is enabled", readonly = true) public boolean isPushEnabled() { return pushEnabled; } @ManagedAttribute(value = "Whether CONNECT requests supports a protocol", readonly = true) public boolean isConnectProtocolEnabled() { return connectProtocolEnabled; } public void setConnectProtocolEnabled(boolean connectProtocolEnabled) { this.connectProtocolEnabled = connectProtocolEnabled; } /** *

This method is called when the TCP FIN is received from the remote peer.

* * @see #onGoAway(GoAwayFrame) * @see #close(int, String, Callback) * @see #onIdleTimeout() */ public void onShutdown() { streamsState.onShutdown(); } /** *

This method is invoked when the idle timeout expires.

* * @return true if the session should be closed, false otherwise * @see #onGoAway(GoAwayFrame) * @see #close(int, String, Callback) * @see #onShutdown() */ public boolean onIdleTimeout() { return streamsState.onIdleTimeout(); } private void notIdle() { streamsState.idleNanoTime = NanoTime.now(); } public void onFrame(Frame frame) { onConnectionFailure(ErrorCode.PROTOCOL_ERROR.code, "upgrade"); } void scheduleTimeout(HTTP2Stream stream) { streamTimeouts.schedule(stream); } private void onStreamCreated(int streamId) { if (LOG.isDebugEnabled()) LOG.debug("Creating stream #{} for {}", streamId, this); streamsState.onStreamCreated(); } protected final void onStreamOpened(Stream stream) { if (LOG.isDebugEnabled()) LOG.debug("Opened stream {} for {}", stream, this); streamsOpened.incrementAndGet(); } private void onStreamClosed(Stream stream) { if (LOG.isDebugEnabled()) LOG.debug("Closed stream {} for {}", stream, this); streamsClosed.incrementAndGet(); } private void onStreamDestroyed(int streamId) { if (LOG.isDebugEnabled()) LOG.debug("Destroyed stream #{} for {}", streamId, this); streamsState.onStreamDestroyed(); } private void terminate(Throwable cause) { flusher.terminate(cause); streamTimeouts.destroy(); disconnect(); } public void disconnect() { if (LOG.isDebugEnabled()) LOG.debug("Disconnecting {}", this); endPoint.close(); } public boolean isDisconnected() { return !endPoint.isOpen(); } protected int getLastRemoteStreamId() { return lastRemoteStreamId.get(); } protected void updateLastRemoteStreamId(int streamId) { Atomics.updateMax(lastRemoteStreamId, streamId); } protected Stream.Listener notifyNewStream(Stream stream, HeadersFrame frame) { try { return listener.onNewStream(stream, frame); } catch (Throwable x) { LOG.info("Failure while notifying listener {}", listener, x); return null; } } protected void notifySettings(Session session, SettingsFrame frame) { try { listener.onSettings(session, frame); } catch (Throwable x) { LOG.info("Failure while notifying listener {}", listener, x); } } protected void notifyPing(Session session, PingFrame frame) { try { listener.onPing(session, frame); } catch (Throwable x) { LOG.info("Failure while notifying listener {}", listener, x); } } protected void notifyReset(Session session, ResetFrame frame) { try { listener.onReset(session, frame); } catch (Throwable x) { LOG.info("Failure while notifying listener {}", listener, x); } } protected void notifyGoAway(Session session, GoAwayFrame frame) { try { listener.onGoAway(session, frame); } catch (Throwable x) { LOG.info("Failure while notifying listener " + listener, x); } } protected void notifyClose(Session session, GoAwayFrame frame, Callback callback) { try { listener.onClose(session, frame, callback); } catch (Throwable x) { LOG.info("Failure while notifying listener {}", listener, x); } } protected boolean notifyIdleTimeout(Session session) { try { return listener.onIdleTimeout(session); } catch (Throwable x) { LOG.info("Failure while notifying listener {}", listener, x); return true; } } protected void notifyFailure(Session session, Throwable failure, Callback callback) { try { listener.onFailure(session, failure, callback); } catch (Throwable x) { LOG.info("Failure while notifying listener {}", listener, x); } } protected static boolean isClientStream(int streamId) { // Client-initiated stream ids are odd. return (streamId & 1) == 1; } @Override public void dump(Appendable out, String indent) throws IOException { dumpObjects(out, indent, new DumpableCollection("streams", streams.values())); } @Override public String toString() { return String.format("%s@%x{local:%s,remote:%s,sendWindow=%s,recvWindow=%s,%s}", getClass().getSimpleName(), hashCode(), getEndPoint().getLocalSocketAddress(), getEndPoint().getRemoteSocketAddress(), sendWindow, recvWindow, streamsState ); } public abstract static class Entry extends Callback.Nested { protected final Frame frame; protected final HTTP2Stream stream; protected Entry(Frame frame, HTTP2Stream stream, Callback callback) { super(callback); this.frame = frame; this.stream = stream; } public Frame frame() { return frame; } public abstract int getFrameBytesGenerated(); public int getDataBytesRemaining() { return 0; } public abstract boolean generate(ByteBufferPool.Accumulator accumulator) throws HpackException; boolean hasHighPriority() { return false; } @Override public void failed(Throwable x) { if (stream != null) { stream.close(); stream.getSession().removeStream(stream); } super.failed(x); } /** * @return whether the entry should not be processed */ public boolean shouldBeDropped() { switch (frame.getType()) { // Frames of this type should not be dropped. case PRIORITY: case SETTINGS: case PING: case GO_AWAY: case WINDOW_UPDATE: case PREFACE: case DISCONNECT: return false; // Frames of this type follow the logic below. case DATA: case HEADERS: case PUSH_PROMISE: case CONTINUATION: case RST_STREAM: break; default: throw new IllegalStateException(); } // SPEC: section 6.4. if (frame.getType() == FrameType.RST_STREAM) return stream != null && stream.isLocal() && !stream.isCommitted(); // Frames that do not have a stream associated are dropped. if (stream == null) return true; return stream.isResetOrFailed(); } void commit() { if (stream != null) stream.commit(); } @Override public String toString() { return frame.toString(); } } private class ControlEntry extends Entry { private int frameBytes; private ControlEntry(Frame frame, HTTP2Stream stream, Callback callback) { super(frame, stream, callback); } @Override public int getFrameBytesGenerated() { return frameBytes; } @Override public boolean generate(ByteBufferPool.Accumulator accumulator) throws HpackException { frameBytes = generator.control(accumulator, frame); beforeSend(); return true; } /** *

Performs actions just before writing the frame to the network.

*

Some frame, when sent over the network, causes the receiver * to react and send back frames that may be processed by the original * sender *before* {@link #succeeded()} is called. *

If the action to perform updates some state, this update may * not be seen by the received frames and cause errors.

*

For example, suppose the action updates the stream window to a * larger value; the sender sends the frame; the receiver is now entitled * to send back larger data; when the data is received by the original * sender, the action may have not been performed yet, causing the larger * data to be rejected, when it should have been accepted.

*/ private void beforeSend() { switch (frame.getType()) { case HEADERS -> { HeadersFrame headersFrame = (HeadersFrame)frame; stream.updateClose(headersFrame.isEndStream(), CloseState.Event.BEFORE_SEND); } case SETTINGS -> { SettingsFrame settingsFrame = (SettingsFrame)frame; if (!settingsFrame.isReply()) configure(settingsFrame.getSettings(), true); } } } @Override boolean hasHighPriority() { return frame.getType() == FrameType.PING; } @Override public void succeeded() { commit(); bytesWritten.addAndGet(frameBytes); frameBytes = 0; switch (frame.getType()) { case HEADERS -> { HeadersFrame headersFrame = (HeadersFrame)frame; if (headersFrame.getMetaData().isRequest()) onStreamOpened(stream); if (stream.updateClose(headersFrame.isEndStream(), CloseState.Event.AFTER_SEND)) removeStream(stream); } case WINDOW_UPDATE -> { flowControl.windowUpdate(HTTP2Session.this, stream, (WindowUpdateFrame)frame); } } super.succeeded(); } } private class DataEntry extends Entry { private int frameBytes; private int dataBytes; private int dataRemaining; private DataEntry(DataFrame frame, HTTP2Stream stream, Callback callback) { super(frame, stream, callback); // We don't do any padding, so the flow control length is // always the data remaining. This simplifies the handling // of data frames that cannot be completely written due to // the flow control window exhausting, since in that case // we would have to count the padding only once. dataRemaining = frame.remaining(); } @Override public int getFrameBytesGenerated() { return frameBytes; } @Override public int getDataBytesRemaining() { return dataRemaining; } @Override public boolean generate(ByteBufferPool.Accumulator accumulator) { int dataRemaining = getDataBytesRemaining(); int sessionSendWindow = getSendWindow(); int streamSendWindow = stream.updateSendWindow(0); int window = Math.min(streamSendWindow, sessionSendWindow); if (window <= 0 && dataRemaining > 0) return false; int length = Math.min(dataRemaining, window); // Only one DATA frame is generated. DataFrame dataFrame = (DataFrame)frame; int frameBytes = generator.data(accumulator, dataFrame, length); this.frameBytes += frameBytes; int dataBytes = frameBytes - Frame.HEADER_LENGTH; this.dataBytes += dataBytes; this.dataRemaining -= dataBytes; if (LOG.isDebugEnabled()) LOG.debug("Generated {}, length/window/data={}/{}/{}", dataFrame, dataBytes, window, dataRemaining); flowControl.onDataSending(stream, dataBytes); stream.updateClose(dataFrame.isEndStream(), CloseState.Event.BEFORE_SEND); return true; } @Override public void succeeded() { bytesWritten.addAndGet(frameBytes); frameBytes = 0; flowControl.onDataSent(stream, dataBytes); dataBytes = 0; // Do we have more to send ? DataFrame dataFrame = (DataFrame)frame; if (getDataBytesRemaining() == 0) { // Only now we can update the close state // and eventually remove the stream. if (stream.updateClose(dataFrame.isEndStream(), CloseState.Event.AFTER_SEND)) removeStream(stream); super.succeeded(); } } } private class OnResetCallback implements Callback { @Override public void succeeded() { complete(); } @Override public void failed(Throwable x) { if (LOG.isDebugEnabled()) LOG.debug("OnReset failed", x); complete(); } @Override public InvocationType getInvocationType() { return InvocationType.NON_BLOCKING; } private void complete() { flusher.iterate(); } } /** *

The HTTP/2 specification requires that stream ids are monotonically increasing, * see RFC 7540, 5.1.1.

*

This implementation uses a queue to atomically reserve a stream id and claim * a slot in the queue; the slot is then assigned the entries to write.

*

Concurrent threads push slots in the queue but only one thread flushes * the slots, up to the slot that has a non-null entries to write, therefore * guaranteeing that frames are sent strictly in their stream id order.

*

This class also coordinates the creation of streams with the close of * the session, see * RFC 7540, 6.8.

*/ private class StreamsState { private final AutoLock lock = new AutoLock(); private final Queue slots = new ArrayDeque<>(); // Must be incremented with the lock held. private final AtomicLong streamCount = new AtomicLong(); private long idleNanoTime = NanoTime.now(); private CloseState closed = CloseState.NOT_CLOSED; private Runnable zeroStreamsAction; private GoAwayFrame goAwayRecv; private GoAwayFrame goAwaySent; private Throwable failure; private Thread flushing; private CompletableFuture shutdownCallback; private CloseState getCloseState() { try (AutoLock ignored = lock.lock()) { return closed; } } private CompletableFuture shutdown() { CompletableFuture future; try (AutoLock ignored = lock.lock()) { if (shutdownCallback != null) return shutdownCallback; if (closed == CloseState.CLOSED) return CompletableFuture.completedFuture(null); shutdownCallback = future = new Callback.Completable(); } goAway(GoAwayFrame.GRACEFUL, Callback.NOOP); return future; } private boolean goAway(GoAwayFrame frame, Callback callback) { boolean sendGoAway = false; boolean tryRunZeroStreamsAction = false; try (AutoLock ignored = lock.lock()) { switch (closed) { case NOT_CLOSED -> { goAwaySent = frame; closed = CloseState.LOCALLY_CLOSED; sendGoAway = true; if (frame.isGraceful()) { // Try to send the non-graceful GOAWAY // when the last stream is destroyed. zeroStreamsAction = () -> { GoAwayFrame goAwayFrame = newGoAwayFrame(ErrorCode.NO_ERROR.code, "close"); goAway(goAwayFrame, Callback.NOOP); }; tryRunZeroStreamsAction = streamCount.get() == 0; } } case LOCALLY_CLOSED -> { if (frame.isGraceful()) { // Trying to send a non-first, but graceful, GOAWAY, ignore this one. if (LOG.isDebugEnabled()) LOG.debug("Already sent, ignored GOAWAY {} for {}", frame, HTTP2Session.this); } else { // SPEC: see section 6.8. if (goAwaySent.isGraceful() || frame.getLastStreamId() < goAwaySent.getLastStreamId() || frame.getError() != ErrorCode.NO_ERROR.code) { goAwaySent = frame; sendGoAway = true; } else { // Trying to send another non-graceful GOAWAY, ignore this one. if (LOG.isDebugEnabled()) LOG.debug("Already sent, ignored GOAWAY {} for {}", frame, HTTP2Session.this); } } } case REMOTELY_CLOSED -> { goAwaySent = frame; sendGoAway = true; if (frame.isGraceful()) { // Try to send the non-graceful GOAWAY // when the last stream is destroyed. zeroStreamsAction = () -> { GoAwayFrame goAwayFrame = newGoAwayFrame(ErrorCode.NO_ERROR.code, "close"); goAway(goAwayFrame, Callback.NOOP); }; tryRunZeroStreamsAction = streamCount.get() == 0; } else { if (goAwayRecv.isGraceful()) { if (LOG.isDebugEnabled()) LOG.debug("Waiting non-graceful GOAWAY for {}", HTTP2Session.this); } else { closed = CloseState.CLOSING; zeroStreamsAction = () -> terminate(frame); tryRunZeroStreamsAction = streamCount.get() == 0; } } } default -> { // Already closing or closed, ignore it. if (LOG.isDebugEnabled()) LOG.debug("Already closed, ignored {} for {}", frame, HTTP2Session.this); } } } if (sendGoAway) { if (tryRunZeroStreamsAction) sendGoAway(frame, Callback.from(callback, this::tryRunZeroStreamsAction)); else sendGoAway(frame, callback); return true; } else { callback.succeeded(); return false; } } private void halt(String reason) { if (LOG.isDebugEnabled()) LOG.debug("Halting ({}) for {}", reason, HTTP2Session.this); GoAwayFrame goAwayFrame = null; GoAwayFrame goAwayFrameEvent; try (AutoLock ignored = lock.lock()) { switch (closed) { case NOT_CLOSED, REMOTELY_CLOSED, LOCALLY_CLOSED, CLOSING -> { if (goAwaySent == null || goAwaySent.isGraceful()) goAwaySent = goAwayFrame = newGoAwayFrame(ErrorCode.NO_ERROR.code, reason); goAwayFrameEvent = goAwayRecv != null ? goAwayRecv : goAwaySent; closed = CloseState.CLOSED; zeroStreamsAction = null; if (failure != null) failure = toFailure(ErrorCode.NO_ERROR.code, reason); } default -> { return; } } } failStreams(stream -> true, reason, true); if (goAwayFrame != null) sendGoAwayAndTerminate(goAwayFrame, goAwayFrameEvent); else terminate(goAwayFrameEvent); } private void onGoAway(GoAwayFrame frame) { boolean failStreams = false; boolean tryRunZeroStreamsAction = false; try (AutoLock ignored = lock.lock()) { switch (closed) { case NOT_CLOSED -> { goAwayRecv = frame; if (frame.isGraceful()) { closed = CloseState.REMOTELY_CLOSED; if (LOG.isDebugEnabled()) LOG.debug("Waiting non-graceful GOAWAY for {}", HTTP2Session.this); } else { goAwaySent = newGoAwayFrame(ErrorCode.NO_ERROR.code, "close"); closed = CloseState.CLOSING; GoAwayFrame goAwayFrame = goAwaySent; zeroStreamsAction = () -> sendGoAwayAndTerminate(goAwayFrame, frame); tryRunZeroStreamsAction = streamCount.get() == 0; failStreams = true; } } case LOCALLY_CLOSED -> { goAwayRecv = frame; if (frame.isGraceful()) { // Wait for the non-graceful GOAWAY from the other peer. if (LOG.isDebugEnabled()) LOG.debug("Waiting non-graceful GOAWAY for {}", HTTP2Session.this); } else { closed = CloseState.CLOSING; if (goAwaySent.isGraceful()) { goAwaySent = newGoAwayFrame(ErrorCode.NO_ERROR.code, "close"); GoAwayFrame goAwayFrame = goAwaySent; zeroStreamsAction = () -> sendGoAwayAndTerminate(goAwayFrame, frame); tryRunZeroStreamsAction = streamCount.get() == 0; } else { zeroStreamsAction = () -> terminate(frame); tryRunZeroStreamsAction = streamCount.get() == 0; failStreams = true; } } } case REMOTELY_CLOSED -> { if (frame.isGraceful()) { // Received a non-first, but graceful, GOAWAY, ignore it. if (LOG.isDebugEnabled()) LOG.debug("Already received, ignoring GOAWAY for {}", HTTP2Session.this); } else { goAwayRecv = frame; closed = CloseState.CLOSING; if (goAwaySent == null || goAwaySent.isGraceful()) { goAwaySent = newGoAwayFrame(ErrorCode.NO_ERROR.code, "close"); GoAwayFrame goAwayFrame = goAwaySent; zeroStreamsAction = () -> sendGoAwayAndTerminate(goAwayFrame, frame); } else { zeroStreamsAction = () -> terminate(frame); } tryRunZeroStreamsAction = streamCount.get() == 0; failStreams = true; } } default -> { // Already closing or closed, ignore it. if (LOG.isDebugEnabled()) LOG.debug("Already closed, ignored {} for {}", frame, HTTP2Session.this); } } } notifyGoAway(HTTP2Session.this, frame); if (failStreams) { // Must compare the lastStreamId only with local streams. // For example, a client that sent request with streamId=137 may send a GOAWAY(4), // where streamId=4 is the last stream pushed by the server to the client. // The server must not compare its local streamId=4 with remote streamId=137. Predicate failIf = stream -> stream.isLocal() && stream.getId() > frame.getLastStreamId(); failStreams(failIf, "closing", false); } if (tryRunZeroStreamsAction) tryRunZeroStreamsAction(); } private void onShutdown() { String reason = "input_shutdown"; Throwable cause = null; boolean failStreams = false; try (AutoLock ignored = lock.lock()) { switch (closed) { case NOT_CLOSED, LOCALLY_CLOSED -> { if (LOG.isDebugEnabled()) LOG.debug("Unexpected ISHUT for {}", HTTP2Session.this); closed = CloseState.CLOSING; failure = cause = new ClosedChannelException(); } case REMOTELY_CLOSED -> { closed = CloseState.CLOSING; GoAwayFrame goAwayFrame = newGoAwayFrame(0, ErrorCode.NO_ERROR.code, reason); zeroStreamsAction = () -> terminate(goAwayFrame); failure = new ClosedChannelException(); failStreams = true; } case CLOSING -> { if (failure == null) failure = new ClosedChannelException(); failStreams = true; } default -> { if (LOG.isDebugEnabled()) LOG.debug("Already closed, ignoring ISHUT for {}", HTTP2Session.this); return; } } } if (failStreams) { // Since nothing else will arrive from the other peer, reset // the streams for which the other peer did not send all frames. Predicate failIf = stream -> !stream.isRemotelyClosed(); failStreams(failIf, reason, false); tryRunZeroStreamsAction(); } else { GoAwayFrame goAwayFrame = newGoAwayFrame(0, ErrorCode.NO_ERROR.code, reason); abort(reason, cause, Callback.from(() -> terminate(goAwayFrame))); } } private boolean onIdleTimeout() { String reason = "idle_timeout"; boolean notify = false; boolean terminate = false; boolean sendGoAway = false; GoAwayFrame goAwayFrame = null; Throwable cause = null; try (AutoLock ignored = lock.lock()) { switch (closed) { case NOT_CLOSED -> { long elapsed = NanoTime.millisSince(idleNanoTime); if (elapsed < endPoint.getIdleTimeout()) return false; notify = true; } case LOCALLY_CLOSED -> { // Timed out while waiting for closing events, fail all the streams. if (goAwaySent.isGraceful()) { goAwaySent = newGoAwayFrame(ErrorCode.NO_ERROR.code, reason); sendGoAway = true; } goAwayFrame = goAwaySent; closed = CloseState.CLOSING; zeroStreamsAction = null; failure = cause = newTimeoutException(); } case REMOTELY_CLOSED -> { goAwaySent = newGoAwayFrame(ErrorCode.NO_ERROR.code, reason); sendGoAway = true; goAwayFrame = goAwaySent; closed = CloseState.CLOSING; zeroStreamsAction = null; failure = cause = newTimeoutException(); } default -> terminate = true; } } if (terminate) { if (LOG.isDebugEnabled()) LOG.debug("Already closed, ignored idle timeout for {}", HTTP2Session.this); // Writes may be TCP congested, so termination never happened. flusher.abort(newTimeoutException()); return false; } if (notify) { boolean confirmed = notifyIdleTimeout(HTTP2Session.this); if (LOG.isDebugEnabled()) LOG.debug("Idle timeout {} for {}", confirmed ? "confirmed" : "ignored", HTTP2Session.this); if (confirmed) halt(reason); return false; } failStreams(stream -> true, reason, true); if (sendGoAway) sendGoAway(goAwayFrame, Callback.NOOP); notifyFailure(HTTP2Session.this, cause, Callback.NOOP); terminate(goAwayFrame); return false; } private TimeoutException newTimeoutException() { return new TimeoutException("Session idle timeout expired"); } private void onSessionFailure(int error, String reason, Callback callback) { GoAwayFrame goAwayFrame; Throwable cause; try (AutoLock ignored = lock.lock()) { switch (closed) { case NOT_CLOSED, LOCALLY_CLOSED, REMOTELY_CLOSED -> { // Send another GOAWAY with the error code. goAwaySent = goAwayFrame = newGoAwayFrame(error, reason); closed = CloseState.CLOSING; zeroStreamsAction = null; failure = cause = toFailure(error, reason); } default -> { if (LOG.isDebugEnabled()) LOG.debug("Already closed, ignored session failure {}", HTTP2Session.this, failure); callback.succeeded(); return; } } } if (LOG.isDebugEnabled()) LOG.debug("Session failure {}", HTTP2Session.this, cause); failStreams(stream -> true, reason, true); sendGoAway(goAwayFrame, Callback.NOOP); notifyFailure(HTTP2Session.this, cause, Callback.NOOP); terminate(goAwayFrame); } private void onWriteFailure(Throwable x) { String reason = "write_failure"; try (AutoLock ignored = lock.lock()) { switch (closed) { case NOT_CLOSED, LOCALLY_CLOSED, REMOTELY_CLOSED -> { closed = CloseState.CLOSING; zeroStreamsAction = () -> { GoAwayFrame goAwayFrame = newGoAwayFrame(0, ErrorCode.NO_ERROR.code, reason); terminate(goAwayFrame); }; failure = x; } default -> { return; } } } abort(reason, x, Callback.from(this::tryRunZeroStreamsAction)); } private void sendGoAwayAndTerminate(GoAwayFrame frame, GoAwayFrame eventFrame) { sendGoAway(frame, Callback.from(() -> terminate(eventFrame))); } private void sendGoAway(GoAwayFrame frame, Callback callback) { control(null, callback, frame); } private void onStreamCreated() { streamCount.incrementAndGet(); } private void onStreamDestroyed() { long count = streamCount.decrementAndGet(); // I've seen zero here, but it may increase again. // That's why tryRunZeroStreamsAction() must check // the count with the lock held. if (count == 0) tryRunZeroStreamsAction(); } private void tryRunZeroStreamsAction() { // Threads from onStreamClosed() and other events // such as onGoAway() may be in a race to finish, // but only one moves to CLOSED and runs the action. Runnable action = null; try (AutoLock ignored = lock.lock()) { long count = streamCount.get(); if (count > 0) { if (LOG.isDebugEnabled()) LOG.debug("Deferred closing action, {} pending streams on {}", count, HTTP2Session.this); return; } switch (closed) { case LOCALLY_CLOSED -> { if (goAwaySent.isGraceful()) { action = zeroStreamsAction; zeroStreamsAction = null; } } case REMOTELY_CLOSED -> { if (goAwaySent != null && goAwaySent.isGraceful()) { action = zeroStreamsAction; zeroStreamsAction = null; } } case CLOSING -> { closed = CloseState.CLOSED; action = zeroStreamsAction; zeroStreamsAction = null; } } } if (action != null) { if (LOG.isDebugEnabled()) LOG.debug("Executing zero streams action on {}", HTTP2Session.this); action.run(); } } private void terminate(GoAwayFrame frame) { if (LOG.isDebugEnabled()) LOG.debug("Terminating {}", HTTP2Session.this); CompletableFuture completable; try (AutoLock ignored = lock.lock()) { completable = shutdownCallback; } if (completable != null) completable.complete(null); HTTP2Session.this.terminate(failure); notifyClose(HTTP2Session.this, frame, Callback.NOOP); } private int priority(PriorityFrame frame, Callback callback) { Slot slot = new Slot(); int currentStreamId = frame.getStreamId(); int streamId = reserveSlot(slot, currentStreamId, callback::failed); if (streamId > 0) { if (currentStreamId <= 0) frame = frame.withStreamId(streamId); slot.entries = List.of(newEntry(frame, null, Callback.from(callback::succeeded, x -> { HTTP2Session.this.onStreamDestroyed(streamId); callback.failed(x); }))); flush(); } return streamId; } private void newLocalStream(HTTP2Stream.FrameList frameList, Promise promise, Stream.Listener listener) { Slot slot = new Slot(); int currentStreamId = frameList.getStreamId(); int streamId = reserveSlot(slot, currentStreamId, promise::failed); if (streamId > 0) { List frames = frameList.getFrames(); if (currentStreamId <= 0) { frames = frames.stream() .map(frame -> frame.withStreamId(streamId)) .collect(Collectors.toList()); } if (createLocalStream(slot, frames, promise, listener, streamId)) return; freeSlot(slot, streamId); } } private Stream newUpgradeStream(HeadersFrame frame, Stream.Listener listener, Consumer failFn) { int streamId = localStreamIds.getAndAdd(2); HTTP2Session.this.onStreamCreated(streamId); HTTP2Stream stream = HTTP2Session.this.createLocalStream(streamId, (MetaData.Request)frame.getMetaData(), x -> { HTTP2Session.this.onStreamDestroyed(streamId); failFn.accept(x); }); if (stream != null) { stream.setListener(listener); stream.updateClose(frame.isEndStream(), CloseState.Event.AFTER_SEND); } return stream; } private boolean newRemoteStream(int streamId) { boolean created; try (AutoLock ignored = lock.lock()) { created = switch (closed) { case NOT_CLOSED -> true; case LOCALLY_CLOSED -> // SPEC: streams larger than GOAWAY's lastStreamId are dropped. // Allow creation of streams that may have been in-flight. streamId <= goAwaySent.getLastStreamId(); default -> false; }; } if (created) HTTP2Session.this.onStreamCreated(streamId); return created; } private void push(PushPromiseFrame frame, Promise promise, Stream.Listener listener) { Slot slot = new Slot(); int streamId = reserveSlot(slot, 0, promise::failed); if (streamId > 0) { frame = frame.withStreamId(streamId); if (createLocalStream(slot, Collections.singletonList(frame), promise, listener, streamId)) return; freeSlot(slot, streamId); } } private boolean createLocalStream(Slot slot, List frames, Promise promise, Stream.Listener listener, int streamId) { MetaData.Request request = extractMetaDataRequest(frames.get(0)); if (request == null) return false; HTTP2Stream stream = HTTP2Session.this.createLocalStream(streamId, request, promise::failed); if (stream == null) return false; stream.setListener(listener); stream.process(new PrefaceFrame(), Callback.NOOP); Callback streamCallback = Callback.from(Invocable.InvocationType.NON_BLOCKING, () -> promise.succeeded(stream), x -> { HTTP2Session.this.onStreamDestroyed(streamId); promise.failed(x); }); int count = frames.size(); if (count == 1) { slot.entries = List.of(newEntry(frames.get(0), stream, streamCallback)); } else { Callback callback = new CountingCallback(streamCallback, count); slot.entries = frames.stream() .map(frame -> newEntry(frame, stream, callback)) .collect(Collectors.toList()); } flush(); return true; } private MetaData.Request extractMetaDataRequest(StreamFrame frame) { if (frame instanceof HeadersFrame) return (MetaData.Request)((HeadersFrame)frame).getMetaData(); if (frame instanceof PushPromiseFrame) return ((PushPromiseFrame)frame).getMetaData(); return null; } private int reserveSlot(Slot slot, int streamId, Consumer fail) { Throwable failure = null; boolean reserved = false; try (AutoLock ignored = lock.lock()) { // SPEC: cannot create new streams after receiving a GOAWAY. if (closed == CloseState.NOT_CLOSED) { if (streamId <= 0) { streamId = localStreamIds.getAndAdd(2); reserved = true; } slots.offer(slot); } else { failure = this.failure; if (failure == null) failure = new IllegalStateException("session closed"); } } if (failure == null) { if (reserved) HTTP2Session.this.onStreamCreated(streamId); return streamId; } else { fail.accept(failure); return 0; } } private void freeSlot(Slot slot, int streamId) { try (AutoLock ignored = lock.lock()) { slots.remove(slot); } HTTP2Session.this.onStreamDestroyed(streamId); flush(); } /** *

Iterates over the entries of the slot queue to flush them.

*

The flush proceeds until either one of the following two conditions is true:

*
    *
  • the queue is empty
  • *
  • a slot with a no entries is encountered
  • *
*

When a slot with a no entries is encountered, then it means that a concurrent thread reserved * a slot but hasn't set its entries yet. Since slots must be flushed in order, the thread encountering * the slot with no entries must bail out and it is up to the concurrent thread to finish up flushing.

*

Note that only one thread can flush at any time; if two threads happen to call this method * concurrently, one will do the work while the other will bail out, so it is safe that all * threads call this method after they are done reserving a slot and setting the entries.

*/ private void flush() { Thread thread = Thread.currentThread(); boolean queued = false; while (true) { List entries; try (AutoLock ignored = lock.lock()) { if (flushing == null) flushing = thread; else if (flushing != thread) return; // Another thread is flushing. Slot slot = slots.peek(); entries = slot == null ? null : slot.entries; if (entries == null) { flushing = null; // No more slots or null entries, so we may iterate on the flusher. break; } slots.poll(); } queued |= flusher.append(entries); } if (queued) flusher.iterate(); } @Override public String toString() { try (AutoLock ignored = lock.lock()) { return String.format("state=[streams=%d,%s,goAwayRecv=%s,goAwaySent=%s,failure=%s]", streamCount.get(), closed, goAwayRecv, goAwaySent, failure ); } } private static class Slot { private volatile List entries; } } private class StreamTimeouts extends CyclicTimeouts { private StreamTimeouts(Scheduler scheduler) { super(scheduler); } @Override protected Iterator iterator() { return streams.values().stream().map(HTTP2Stream.class::cast).iterator(); } @Override protected boolean onExpired(HTTP2Stream stream) { stream.onIdleTimeout(new TimeoutException("Idle timeout " + stream.getIdleTimeout() + " ms elapsed")); // The implementation of the Iterator returned above does not support // removal, but the HTTP2Stream will be removed by stream.onIdleTimeout(). return false; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy