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

org.glassfish.grizzly.http2.DefaultOutputSink Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package org.glassfish.grizzly.http2;

import static org.glassfish.grizzly.http2.Termination.OUT_FIN_TERMINATION;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.WARNING;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;

import org.glassfish.grizzly.Buffer;
import org.glassfish.grizzly.CompletionHandler;
import org.glassfish.grizzly.Grizzly;
import org.glassfish.grizzly.WriteHandler;
import org.glassfish.grizzly.WriteResult;
import org.glassfish.grizzly.asyncqueue.AsyncQueueRecord;
import org.glassfish.grizzly.asyncqueue.MessageCloner;
import org.glassfish.grizzly.asyncqueue.TaskQueue;
import org.glassfish.grizzly.filterchain.FilterChainContext;
import org.glassfish.grizzly.http.HttpContent;
import org.glassfish.grizzly.http.HttpHeader;
import org.glassfish.grizzly.http.HttpPacket;
import org.glassfish.grizzly.http.HttpResponsePacket;
import org.glassfish.grizzly.http.HttpTrailer;
import org.glassfish.grizzly.http2.frames.ErrorCode;
import org.glassfish.grizzly.http2.frames.HeadersFrame;
import org.glassfish.grizzly.http2.frames.Http2Frame;
import org.glassfish.grizzly.http2.frames.PushPromiseFrame;
import org.glassfish.grizzly.http2.utils.ChunkedCompletionHandler;
import org.glassfish.grizzly.memory.Buffers;

/**
 * Default implementation of an output sink, which is associated with specific {@link Http2Stream}.
 *
 * The implementation is aligned with HTTP/2 requirements with regards to message flow control.
 *
 * @author Alexey Stashok
 */
class DefaultOutputSink implements StreamOutputSink {
    private static final Logger LOGGER = Grizzly.logger(DefaultOutputSink.class);

    private static final int MAX_OUTPUT_QUEUE_SIZE = 65536;

    private static final int ZERO_QUEUE_RECORD_SIZE = 1;

    private static final OutputQueueRecord TERMINATING_QUEUE_RECORD = new OutputQueueRecord(null, null, true, true);

    // async output queue
    final TaskQueue outputQueue = TaskQueue.createTaskQueue(new TaskQueue.MutableMaxQueueSize() {

        @Override
        public int getMaxQueueSize() {
            return MAX_OUTPUT_QUEUE_SIZE;
        }
    });

    // the space (in bytes) in flow control window, that still could be used.
    // in other words the number of bytes, which could be sent to the peer
    private final AtomicInteger availStreamWindowSize;

    // true, if last output frame has been queued
    private volatile boolean isLastFrameQueued;
    // not null if last output frame has been sent or forcibly terminated
    private Termination terminationFlag;

    // associated HTTP/2 session
    private final Http2Session http2Session;
    // associated HTTP/2 stream
    private final Http2Stream stream;

    // counter for unflushed writes
    private final AtomicInteger unflushedWritesCounter = new AtomicInteger();
    // sync object to count/notify flush handlers
    private final Object flushHandlersSync = new Object();
    // flush handlers queue
    private BundleQueue> flushHandlersQueue;

    DefaultOutputSink(final Http2Stream stream) {
        this.stream = stream;
        http2Session = stream.getHttp2Session();
        availStreamWindowSize = new AtomicInteger(stream.getPeerWindowSize());
    }

    @Override
    public boolean canWrite() {
        return outputQueue.size() < MAX_OUTPUT_QUEUE_SIZE;
    }

    @Override
    public void notifyWritePossible(final WriteHandler writeHandler) {
        outputQueue.notifyWritePossible(writeHandler, MAX_OUTPUT_QUEUE_SIZE);
    }

    private void assertReady() throws IOException {
        // if the last frame (fin flag == 1) has been queued already - throw an IOException
        if (isTerminated()) {
            if (LOGGER.isLoggable(FINE)) {
                LOGGER.log(FINE, "Terminated!!! id={0} description={1}", new Object[] { stream.getId(), terminationFlag.getDescription() });
            }
            throw new IOException(terminationFlag.getDescription());
        } else if (isLastFrameQueued) {
            throw new IOException("Write beyond end of stream");
        }
    }

    /**
     * The method is called by HTTP2 Filter once WINDOW_UPDATE message comes for this {@link Http2Stream}.
     *
     * @param delta the delta.
     * @throws Http2StreamException if an error occurs processing the window update.
     */
    @Override
    public void onPeerWindowUpdate(final int delta) throws Http2StreamException {

        final int currentWindow = availStreamWindowSize.get();
        if (delta > 0 && currentWindow > 0 && currentWindow + delta < 0) {
            throw new Http2StreamException(stream.getId(), ErrorCode.FLOW_CONTROL_ERROR, "Session flow-control window overflow.");
        }
        // update the available window size
        availStreamWindowSize.addAndGet(delta);

        // try to write until window limit allows
        while ((isWantToWrite() && !outputQueue.isEmpty())
            || (outputQueue.peek() != null && outputQueue.peek().trailer != null)) {

            // pick up the first output record in the queue
            OutputQueueRecord outputQueueRecord = outputQueue.poll();

            // if there is nothing to write - return
            if (outputQueueRecord == null) {
                return;
            }

            // if it's terminating record - processFin
            if (outputQueueRecord == TERMINATING_QUEUE_RECORD) {
                // if it's TERMINATING_QUEUE_RECORD - don't forget to release ATOMIC_QUEUE_RECORD_SIZE
                releaseWriteQueueSpace(0, true, true);
                writeEmptyFin();
                return;
            }

            final FlushCompletionHandler completionHandler = outputQueueRecord.chunkedCompletionHandler;
            boolean isLast = outputQueueRecord.isLast;
            final boolean isZeroSizeData = outputQueueRecord.isZeroSizeData;
            final Source resource = outputQueueRecord.resource;

            final HttpTrailer currentTrailer = outputQueueRecord.trailer;
            if (currentTrailer != null) {
                outputQueueRecord = null;
                sendTrailers(completionHandler, currentTrailer);
                return;
            }

            // check if output record's buffer is fitting into window size
            // if not - split it into 2 parts: part to send, part to keep in the queue
            final int bytesToSend = checkOutputWindow(resource.remaining());
            final Buffer dataChunkToSend = resource.read(bytesToSend);
            final boolean hasRemaining = resource.hasRemaining();

            // if there is a chunk to store
            if (hasRemaining) {
                // Create output record for the chunk to be stored
                outputQueueRecord.reset(resource, completionHandler, isLast);
                outputQueueRecord.incChunksCounter();

                // reset isLast for the current chunk
                isLast = false;
            } else {
                outputQueueRecord.release();
                outputQueueRecord = null;
            }

            // if there is a chunk to sent
            if (dataChunkToSend != null && (dataChunkToSend.hasRemaining() || isLast)) {
                final int dataChunkToSendSize = dataChunkToSend.remaining();

                // send a http2 data frame
                flushToConnectionOutputSink(dataChunkToSend, completionHandler, isLast);

                // update the available window size bytes counter
                availStreamWindowSize.addAndGet(-dataChunkToSendSize);
                releaseWriteQueueSpace(dataChunkToSendSize, isZeroSizeData, outputQueueRecord == null);

                outputQueue.doNotify();
            } else if (isZeroSizeData && outputQueueRecord == null) {
                // if it's atomic and no remainder left - don't forget to release ATOMIC_QUEUE_RECORD_SIZE
                releaseWriteQueueSpace(0, true, true);
                outputQueue.doNotify();
            }

            if (outputQueueRecord != null) {
                // if there is a chunk to be stored - store it and return
                outputQueue.setCurrentElement(outputQueueRecord);
                break;
            }
        }
    }
    /**
     * Send an {@link HttpPacket} to the {@link Http2Stream}.
     *
     * The writeDownStream(...) methods have to be synchronized with shutdown().
     *
     * @param httpPacket {@link HttpPacket} to send
     * @param completionHandler the {@link CompletionHandler}, which will be notified about write progress.
     * @param messageCloner the {@link MessageCloner}, which will be able to clone the message in case it can't be
     * completely written in the current thread.
     * @throws IOException if an error occurs with the write operation.
     */
    @Override
    public synchronized void writeDownStream(final HttpPacket httpPacket, final FilterChainContext ctx,
            final CompletionHandler completionHandler, final MessageCloner messageCloner)
                throws IOException {
        assert ctx != null;
        assertReady();
        final OutputQueueRecord next = writeDownStream0(httpPacket, ctx, completionHandler, messageCloner);
        if (next != null) {
            addOutputQueueRecord(next);
        }
    }


    private OutputQueueRecord writeDownStream0(final HttpPacket httpPacket,
         final FilterChainContext ctx,
         final CompletionHandler completionHandler,
         final MessageCloner messageCloner)
    throws IOException {

        final HttpHeader httpHeader = stream.getOutputHttpHeader();
        final HttpContent httpContent = HttpContent.isContent(httpPacket) ? (HttpContent) httpPacket : null;
        List headerFrames = null;
        boolean sendTrailers = false;
        boolean lockedByMe = false;
        try {
            boolean isLast = httpContent != null && httpContent.isLast();
            final boolean isTrailer = HttpTrailer.isTrailer(httpContent);

            // If HTTP header hasn't been committed - commit it
            if (!httpHeader.isCommitted()) {
                final boolean dontSendPayload = !httpHeader.isExpectContent()
                    || (httpContent != null && httpContent.isLast() && !httpContent.getContent().hasRemaining());
                LOGGER.finest(() -> "Header not committed yet; dontSendPayload=" + dontSendPayload);

                http2Session.getDeflaterLock().lock();
                lockedByMe = true;
                final boolean logging = NetLogger.isActive();
                final Map capture = logging ? new HashMap<>() : null;
                headerFrames = http2Session.encodeHttpHeaderAsHeaderFrames(
                    ctx, httpHeader, stream.getId(), dontSendPayload, null, capture);
                if (logging) {
                    for (Http2Frame http2Frame : headerFrames) {
                        if (http2Frame.getType() == PushPromiseFrame.TYPE) {
                            NetLogger.log(NetLogger.Context.TX, http2Session, (HeadersFrame) http2Frame, capture);
                            break;
                        }
                    }
                }
                stream.onSndHeaders(dontSendPayload);

                // 100-Continue block
                if (!httpHeader.isRequest()) {
                    HttpResponsePacket response = (HttpResponsePacket) httpHeader;
                    if (response.isAcknowledgement()) {
                        response.acknowledged();
                        response.getHeaders().clear();
                        unflushedWritesCounter.incrementAndGet();
                        flushToConnectionOutputSink(headerFrames, completionHandler, messageCloner, false);
                        LOGGER.finest("Acknowledgement has been sent.");
                        return null;
                    }
                }

                httpHeader.setCommitted(true);

                if (dontSendPayload || httpContent == null) {
                    // if we don't expect any HTTP payload, mark this frame as last and return
                    unflushedWritesCounter.incrementAndGet();
                    flushToConnectionOutputSink(headerFrames, completionHandler, messageCloner, dontSendPayload);
                    sendTrailers = false;
                    LOGGER.finest(() -> "Nothing to send; dontSendPayload=" + dontSendPayload);
                    return null;
                }
            }

            if (httpContent == null) {
                sendTrailers = false;
                LOGGER.finest("Nothing to send, httpContent is null.");
                return null;
            }

            http2Session.handlerFilter.onHttpContentEncoded(httpContent, ctx);

            Buffer data = httpContent.getContent();
            final int dataSize = data.remaining();

            unflushedWritesCounter.incrementAndGet();
            final FlushCompletionHandler flushCompletionHandler = new FlushCompletionHandler(completionHandler);
            final boolean isZeroSizeData = dataSize == 0;
            final int spaceToReserve = isZeroSizeData ? ZERO_QUEUE_RECORD_SIZE : dataSize;
            boolean isDataCloned = false;

            // Check if output queue is not empty - add new element
            final int spaceReserved = reserveWriteQueueSpace(spaceToReserve);
            LOGGER.finest(() -> "Bytes reserved: " + spaceReserved + ", was requested: " + spaceToReserve);
            if (spaceReserved > spaceToReserve) {
                // if the queue is not empty - the headers should have been sent
                assert headerFrames == null;
                if (messageCloner != null) {
                    data = messageCloner.clone(http2Session.getConnection(), data);
                    isDataCloned = true;
                }

                final OutputQueueRecord record = createOutputQueueRecord(data, httpContent, flushCompletionHandler,
                    isLast, isZeroSizeData);
                outputQueue.offer(record);

                // there is yet something in the queue before current record
                if (outputQueue.size() != spaceToReserve) {
                    sendTrailers = isLast && isTrailer;
                    return null;
                }
                //
                if (!outputQueue.remove(record)) {
                    sendTrailers = false;
                    LOGGER.finest("The record has been already processed.");
                    return null;
                }
            }

            // our element is first in the output queue
            final int remaining = data.remaining();

            // check if output record's buffer is fitting into window size
            // if not - split it into 2 parts: part to send, part to keep in the queue
            final int fitWindowLen = checkOutputWindow(remaining);
            LOGGER.finest(() -> "Remaining: " + remaining + ", fitWindowLen: " + fitWindowLen);

            final OutputQueueRecord outputQueueRecord;
            if (fitWindowLen >= remaining) {
                outputQueueRecord = null;
            } else {
                // if there is a chunk to store
                if (!isDataCloned && messageCloner != null) {
                    data = messageCloner.clone(http2Session.getConnection(), data);
                    isDataCloned = true;
                }

                final Buffer dataChunkToStore = splitOutputBufferIfNeeded(data, fitWindowLen);

                // Create output record for the chunk to be stored
                outputQueueRecord = createOutputQueueRecord(dataChunkToStore, null, flushCompletionHandler, isLast,
                    isZeroSizeData);

                // reset completion handler and isLast for the current chunk
                isLast = false;
            }

            // if there's anything to send - send it
            final Buffer dataToSend = prepareDataToSend(outputQueueRecord == null, isLast, data, isZeroSizeData);
            if (headerFrames != null || dataToSend != null) {
                // if another part of data is stored in the queue -
                // we have to increase CompletionHandler counter to avoid premature notification
                if (outputQueueRecord != null) {
                    outputQueueRecord.incChunksCounter();
                }
                flushToConnectionOutputSink(headerFrames, dataToSend, flushCompletionHandler,
                        isDataCloned ? null : messageCloner, isLast && !isTrailer);
            }

            LOGGER.finest("isLast=" + isLast + "; isTrailer=" + isTrailer);
            sendTrailers = isLast && isTrailer;
            return isLast ? null : outputQueueRecord;
        } finally {
            LOGGER.finest("sendTrailers=" + sendTrailers);
            if (sendTrailers) {
                sendTrailers(completionHandler, (HttpTrailer) httpContent);
            }
            if (lockedByMe) {
                http2Session.getDeflaterLock().unlock();
            }
        }
    }


    private OutputQueueRecord createOutputQueueRecord(final Buffer data, final HttpContent httpContent,
        final FlushCompletionHandler flushCompletionHandler, boolean isLast, final boolean isZeroSizeData) {
        final Source bufferSource = Source.factory(stream).createBufferSource(data);
        if (httpContent instanceof HttpTrailer) {
            return new OutputQueueRecord(bufferSource, flushCompletionHandler, (HttpTrailer) httpContent, false);
        }
        return new OutputQueueRecord(bufferSource, flushCompletionHandler, isLast, isZeroSizeData);
    }


    private Buffer prepareDataToSend(final boolean isRecordNull, final boolean isLast, final Buffer data,
        final boolean isZeroSizeData) {
        if (data == null) {
            return null;
        }
        if (data.hasRemaining() || isLast) {
            final int dataChunkToSendSize = data.remaining();
            // update the available window size bytes counter
            availStreamWindowSize.addAndGet(-dataChunkToSendSize);
            releaseWriteQueueSpace(dataChunkToSendSize, isZeroSizeData, isRecordNull);
            return data;
        }
        return null;
    }



    /**
     * Flush {@link Http2Stream} output and notify {@link CompletionHandler} once all output data has been flushed.
     *
     * @param completionHandler {@link CompletionHandler} to be notified
     */
    @Override
    public void flush(final CompletionHandler completionHandler) {

        // check if there are pending unflushed data
        if (unflushedWritesCounter.get() > 0) {
            // if yes - synchronize do disallow decrease counter from other thread (increasing is ok)
            synchronized (flushHandlersSync) {
                // double check the pending flushes counter
                final int counterNow = unflushedWritesCounter.get();
                if (counterNow > 0) {
                    // if there are pending flushes
                    if (flushHandlersQueue == null) {
                        // create a flush handlers queue
                        flushHandlersQueue = new BundleQueue<>();
                    }

                    // add the handler to the queue
                    flushHandlersQueue.add(counterNow, completionHandler);

                    return;
                }
            }
        }

        // if there are no pending flushes - notify the handler
        completionHandler.completed(stream);
    }

    /**
     * The method is responsible for checking the current output window size.
     * The returned integer value is the size of the data, which could be
     * sent now.
     *
     * @param size check the provided size against the window size limit.
     *
     * @return the amount of data that may be written.
     */
    private int checkOutputWindow(final long size) {
        // take a snapshot of the current output window state and check if we
        // can fit "size" into window.
        // Make sure we return positive value or zero, because availStreamWindowSize could be negative.
        return Math.max(0, Math.min(availStreamWindowSize.get(), (int) size));
    }

    private Buffer splitOutputBufferIfNeeded(final Buffer buffer, final int length) {
        if (length == buffer.remaining()) {
            return null;
        }

        return buffer.split(buffer.position() + length);
    }

    private void flushToConnectionOutputSink(final List headerFrames,
        final CompletionHandler completionHandler,
        final MessageCloner messageCloner,
        final boolean sendFIN) {
        final FlushCompletionHandler flushCompletionHandler = new FlushCompletionHandler(completionHandler);
        flushToConnectionOutputSink(headerFrames, null, flushCompletionHandler, messageCloner, sendFIN);
    }

    private void flushToConnectionOutputSink(
        final Buffer data,
        final FlushCompletionHandler flushCompletionHandler,
        final boolean sendFIN) {
        flushToConnectionOutputSink(null, data, flushCompletionHandler, null, sendFIN);
    }

    private void flushToConnectionOutputSink(
            final List headerFrames,
            final Buffer data,
            final CompletionHandler completionHandler,
            final MessageCloner messageCloner,
            final boolean sendFIN) {

        http2Session.getOutputSink().writeDataDownStream(
            stream, headerFrames, data, completionHandler, messageCloner, sendFIN);

        if (sendFIN) {
            terminate(OUT_FIN_TERMINATION);
        }
    }

    /**
     * Closes the output sink by adding last DataFrame with the FIN flag to a queue. If the output sink is already closed -
     * method does nothing.
     */
    @Override
    public synchronized void close() {
        if (!isClosed()) {
            isLastFrameQueued = true;

            if (outputQueue.isEmpty()) {
                writeEmptyFin();
                return;
            }

            outputQueue.reserveSpace(ZERO_QUEUE_RECORD_SIZE);
            outputQueue.offer(TERMINATING_QUEUE_RECORD);

            if (outputQueue.size() == ZERO_QUEUE_RECORD_SIZE && outputQueue.remove(TERMINATING_QUEUE_RECORD)) {
                writeEmptyFin();
            }
        }
    }

    /**
     * Unlike {@link #close()} this method forces the output sink termination by setting termination flag and canceling all
     * the pending writes.
     */
    @Override
    public synchronized void terminate(final Termination terminationFlag) {
        if (!isTerminated()) {
            this.terminationFlag = terminationFlag;
            outputQueue.onClose();
            // NOTIFY STREAM
            stream.onOutputClosed();
        }
    }

    @Override
    public boolean isClosed() {
        return isLastFrameQueued || isTerminated();
    }

    /**
     * @return the number of writes (not bytes), that haven't reached network layer
     */
    @Override
    public int getUnflushedWritesCount() {
        return unflushedWritesCounter.get();
    }

    private boolean isTerminated() {
        return terminationFlag != null;
    }

    private void writeEmptyFin() {
        if (!isTerminated()) {
            unflushedWritesCounter.incrementAndGet();
            flushToConnectionOutputSink(Buffers.EMPTY_BUFFER, new FlushCompletionHandler(null), true);
        }
    }

    private boolean isWantToWrite() {
        // update the available window size
        final int availableWindowSizeBytesNow = availStreamWindowSize.get();

        // get the current peer's window size limit
        final int windowSizeLimit = stream.getPeerWindowSize();

        return availableWindowSizeBytesNow >= windowSizeLimit / 4;
    }

    private void addOutputQueueRecord(OutputQueueRecord outputQueueRecord) throws Http2StreamException {
        do {
            outputQueue.setCurrentElement(outputQueueRecord);

            // check if situation hasn't changed and we can't send the data chunk now
            if (!isWantToWrite() || !outputQueue.compareAndSetCurrentElement(outputQueueRecord, null)) {
                break; // will be (or already) written asynchronously
            }

            // if we can send the output record now - do that
            final FlushCompletionHandler chunkedCompletionHandler = outputQueueRecord.chunkedCompletionHandler;
            final HttpTrailer currentTrailer = outputQueueRecord.trailer;
            if (currentTrailer != null) {
                sendTrailers(chunkedCompletionHandler, currentTrailer);
                return;
            }

            boolean isLast = outputQueueRecord.isLast;
            final boolean isZeroSizeData = outputQueueRecord.isZeroSizeData;
            final Source currentResource = outputQueueRecord.resource;

            final int fitWindowLen = checkOutputWindow(currentResource.remaining());
            final Buffer dataChunkToSend = currentResource.read(fitWindowLen);

            // if there is a chunk to store
            if (currentResource.hasRemaining()) {
                // Create output record for the chunk to be stored
                outputQueueRecord.reset(currentResource, chunkedCompletionHandler, isLast);
                outputQueueRecord.incChunksCounter();

                // reset isLast for the current chunk
                isLast = false;
            } else {
                outputQueueRecord.release();
                outputQueueRecord = null;
            }

            // if there is a chunk to send
            if (dataChunkToSend != null && (dataChunkToSend.hasRemaining() || isLast)) {
                final int dataChunkToSendSize = dataChunkToSend.remaining();
                flushToConnectionOutputSink(dataChunkToSend, chunkedCompletionHandler, isLast);

                // update the available window size bytes counter
                availStreamWindowSize.addAndGet(-dataChunkToSendSize);
                releaseWriteQueueSpace(dataChunkToSendSize, isZeroSizeData, outputQueueRecord == null);
            } else if (isZeroSizeData && outputQueueRecord == null) {
                // if it's atomic and no remainder left - don't forget to release ATOMIC_QUEUE_RECORD_SIZE
                releaseWriteQueueSpace(0, true, true);
            } else if (dataChunkToSend != null && !dataChunkToSend.hasRemaining()) {
                // current window won't allow the data to be sent.  Will be written once the
                // window changes.
                if (outputQueueRecord != null) {
                    reserveWriteQueueSpace(outputQueueRecord.resource.remaining());
                    outputQueue.offer(outputQueueRecord);
                }
                break;
            }
        } while (outputQueueRecord != null);
    }

    private int reserveWriteQueueSpace(final int spaceToReserve) {
        return outputQueue.reserveSpace(spaceToReserve);
    }

    private void releaseWriteQueueSpace(final int justSentBytes, final boolean isAtomic, final boolean isEndOfChunk) {
        if (isEndOfChunk) {
            outputQueue.releaseSpace(isAtomic ? ZERO_QUEUE_RECORD_SIZE : justSentBytes);
        } else if (!isAtomic) {
            outputQueue.releaseSpace(justSentBytes);
        }
    }

    private void sendTrailers(final CompletionHandler completionHandler, final HttpTrailer httpContent) {
        http2Session.getDeflaterLock().lock();
        try {
            final boolean logging = NetLogger.isActive();
            final Map capture = logging ? new HashMap<>() : null;
            final List trailerFrames =
                    http2Session.encodeTrailersAsHeaderFrames(stream.getId(),
                            new ArrayList<>(4),
                            httpContent.getHeaders(), capture);
            if (logging) {
                for (Http2Frame http2Frame : trailerFrames) {
                    if (http2Frame.getType() == PushPromiseFrame.TYPE) {
                        NetLogger.log(NetLogger.Context.TX, http2Session, (HeadersFrame) http2Frame, capture);
                        break;
                    }
                }
            }
            flushToConnectionOutputSink(trailerFrames, completionHandler, null, true);
            unflushedWritesCounter.incrementAndGet();
        } catch (IOException ex) {
            LOGGER.log(WARNING, "Error sending trailers.", ex);
        } finally {
            close();
            LOGGER.finest("Sending trailers finished, unlocking the deflater lock ...");
            http2Session.getDeflaterLock().unlock();
        }
    }

    private static class OutputQueueRecord extends AsyncQueueRecord {
        private final HttpTrailer trailer;
        private final boolean isZeroSizeData;

        private Source resource;
        private FlushCompletionHandler chunkedCompletionHandler;
        private boolean isLast;


        public OutputQueueRecord(final Source resource, final FlushCompletionHandler completionHandler,
                final boolean isLast, final boolean isZeroSizeData) {
            super(null, null, null);

            this.resource = resource;
            this.chunkedCompletionHandler = completionHandler;
            this.isLast = isLast;
            this.trailer = null;
            this.isZeroSizeData = isZeroSizeData;
        }

        public OutputQueueRecord(final Source resource,
                final FlushCompletionHandler completionHandler,
                final HttpTrailer trailer, final boolean isZeroDataSize) {
            super(null, null, null);

            this.resource = resource;
            this.chunkedCompletionHandler = completionHandler;
            this.isLast = true;
            this.trailer = trailer;
            this.isZeroSizeData = isZeroDataSize;
        }

        private void incChunksCounter() {
            if (chunkedCompletionHandler != null) {
                chunkedCompletionHandler.incChunks();
            }
        }

        private void reset(final Source resource, final FlushCompletionHandler completionHandler, final boolean last) {
            this.resource = resource;
            this.chunkedCompletionHandler = completionHandler;
            this.isLast = last;
        }

        public void release() {
            if (resource != null) {
                resource.release();
                resource = null;
            }
        }

        @Override
        public void notifyFailure(final Throwable e) {
            final CompletionHandler chLocal = chunkedCompletionHandler;
            chunkedCompletionHandler = null;
            try {
                if (chLocal != null) {
                    chLocal.failed(e);
                }
            } finally {
                release();
            }
        }

        @Override
        public void recycle() {
        }

        @Override
        public WriteResult getCurrentResult() {
            return null;
        }
    }

    /**
     * Flush {@link CompletionHandler}, which will be passed on each {@link Http2Stream} write to make sure
     * the data reached the wires.
     *
     * Usually FlushCompletionHandler is also used as a wrapper for custom {@link CompletionHandler}
     * provided by users.
     *
     * The parent class has an internal state, so be careful with reuses of the same instance!
     */
    private final class FlushCompletionHandler extends ChunkedCompletionHandler {

        public FlushCompletionHandler(final CompletionHandler parentCompletionHandler) {
            super(parentCompletionHandler);
        }

        /**
         * Notifies all flush handlers about the completition.
         */
        @Override
        protected void done0() {
            synchronized (flushHandlersSync) { // synchronize with flush()
                unflushedWritesCounter.decrementAndGet();
                if (flushHandlersQueue == null || !flushHandlersQueue.nextBundle()) {
                    return;
                }
            }

            boolean hasNext;
            CompletionHandler handler;

            do {
                synchronized (flushHandlersSync) {
                    handler = flushHandlersQueue.next();
                    hasNext = flushHandlersQueue.hasNext();
                }

                try {
                    handler.completed(stream);
                } catch (Exception ignored) {
                }
            } while (hasNext);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy