io.micronaut.http.netty.body.StreamingNettyByteBody Maven / Gradle / Ivy
/*
 * Copyright 2017-2024 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.micronaut.http.netty.body;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.execution.DelayedExecutionFlow;
import io.micronaut.core.execution.ExecutionFlow;
import io.micronaut.core.util.SupplierUtil;
import io.micronaut.http.body.CloseableAvailableByteBody;
import io.micronaut.http.body.CloseableByteBody;
import io.micronaut.http.body.stream.BaseSharedBuffer;
import io.micronaut.http.body.stream.BodySizeLimits;
import io.micronaut.http.body.stream.BufferConsumer;
import io.micronaut.http.body.stream.PublisherAsBlocking;
import io.micronaut.http.body.stream.UpstreamBalancer;
import io.micronaut.http.netty.PublisherAsStream;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.EventLoop;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.util.ReferenceCounted;
import io.netty.util.ResourceLeakDetector;
import io.netty.util.ResourceLeakDetectorFactory;
import io.netty.util.ResourceLeakTracker;
import reactor.core.publisher.Flux;
import java.io.InputStream;
import java.util.List;
import java.util.OptionalLong;
import java.util.function.Supplier;
/**
 * Netty implementation for streaming ByteBody.
 *
 * @since 4.5.0
 * @author Jonas Konrad
 */
@Internal
public final class StreamingNettyByteBody extends NettyByteBody implements CloseableByteBody {
    private final SharedBuffer sharedBuffer;
    /**
     * We have reserve, subscribe, and add calls in {@link SharedBuffer} that all modify the same
     * data structures. They can all happen concurrently and must be moved to the event loop. We
     * also need to ensure that a reserve and associated subscribe stay serialized
     * ({@link io.micronaut.http.netty.EventLoopFlow} semantics). But because of the potential
     * concurrency, we actually need stronger semantics than
     * {@link io.micronaut.http.netty.EventLoopFlow}.
     * 
     * The solution is to use the old {@link EventLoop#inEventLoop()} + {@link EventLoop#execute}
     * pattern. Serialization semantics for reserve to subscribe are guaranteed using this field:
     * If the reserve call is delayed, this field is {@code true}, and the subscribe call will also
     * be delayed. This approach is possible because we only need to serialize a single reserve
     * with a single subscribe.
     */
    private final boolean forceDelaySubscribe;
    private BufferConsumer.Upstream upstream;
    public StreamingNettyByteBody(SharedBuffer sharedBuffer) {
        this(sharedBuffer, false, sharedBuffer.getRootUpstream());
    }
    private StreamingNettyByteBody(SharedBuffer sharedBuffer, boolean forceDelaySubscribe, BufferConsumer.Upstream upstream) {
        this.sharedBuffer = sharedBuffer;
        this.forceDelaySubscribe = forceDelaySubscribe;
        this.upstream = upstream;
    }
    public BufferConsumer.Upstream primary(ByteBufConsumer primary) {
        BufferConsumer.Upstream upstream = this.upstream;
        if (upstream == null) {
            BaseSharedBuffer.failClaim();
        }
        this.upstream = null;
        BaseSharedBuffer.logClaim();
        sharedBuffer.subscribe(primary, upstream, forceDelaySubscribe);
        return upstream;
    }
    @Override
    public @NonNull CloseableByteBody split(@NonNull SplitBackpressureMode backpressureMode) {
        BufferConsumer.Upstream upstream = this.upstream;
        if (upstream == null) {
            BaseSharedBuffer.failClaim();
        }
        UpstreamBalancer.UpstreamPair pair = UpstreamBalancer.balancer(upstream, backpressureMode);
        this.upstream = pair.left();
        boolean forceDelaySubscribe = this.sharedBuffer.reserve();
        return new StreamingNettyByteBody(sharedBuffer, forceDelaySubscribe, pair.right());
    }
    @Override
    public @NonNull StreamingNettyByteBody allowDiscard() {
        BufferConsumer.Upstream upstream = this.upstream;
        if (upstream == null) {
            BaseSharedBuffer.failClaim();
        }
        upstream.allowDiscard();
        return this;
    }
    @Override
    protected Flux toByteBufPublisher() {
        AsFlux asFlux = new AsFlux(sharedBuffer);
        BufferConsumer.Upstream upstream = primary(asFlux);
        return asFlux.asFlux(upstream)
            .doOnDiscard(ByteBuf.class, ReferenceCounted::release);
    }
    @Override
    public @NonNull OptionalLong expectedLength() {
        return sharedBuffer.getExpectedLength();
    }
    @Override
    public @NonNull InputStream toInputStream() {
        PublisherAsBlocking blocking = new PublisherAsBlocking<>() {
            @Override
            protected void release(ByteBuf item) {
                item.release();
            }
        };
        toByteBufPublisher().subscribe(blocking);
        return new PublisherAsStream(blocking);
    }
    @Override
    public @NonNull ExecutionFlow extends CloseableAvailableByteBody> bufferFlow() {
        BufferConsumer.Upstream upstream = this.upstream;
        if (upstream == null) {
            BaseSharedBuffer.failClaim();
        }
        this.upstream = null;
        BaseSharedBuffer.logClaim();
        upstream.start();
        upstream.onBytesConsumed(Long.MAX_VALUE);
        return sharedBuffer.subscribeFull(upstream, forceDelaySubscribe).map(AvailableNettyByteBody::new);
    }
    @Override
    public @NonNull CloseableByteBody move() {
        BufferConsumer.Upstream upstream = this.upstream;
        if (upstream == null) {
            BaseSharedBuffer.failClaim();
        }
        this.upstream = null;
        return new StreamingNettyByteBody(sharedBuffer, forceDelaySubscribe, upstream);
    }
    @Override
    public void close() {
        BufferConsumer.Upstream upstream = this.upstream;
        if (upstream == null) {
            return;
        }
        this.upstream = null;
        BaseSharedBuffer.logClaim();
        upstream.allowDiscard();
        upstream.disregardBackpressure();
        upstream.start();
        sharedBuffer.subscribe(null, upstream, forceDelaySubscribe);
    }
    private static final class AsFlux extends BaseSharedBuffer.AsFlux implements ByteBufConsumer {
        public AsFlux(BaseSharedBuffer, ?> sharedBuffer) {
            super(sharedBuffer);
        }
        @Override
        public void add(ByteBuf buf) {
            if (!add0(buf)) {
                buf.release();
            }
        }
        @Override
        protected int size(ByteBuf buf) {
            return buf.readableBytes();
        }
    }
    /**
     * This class buffers input data and distributes it to multiple {@link StreamingNettyByteBody}
     * instances.
     * Thread safety: The {@link ByteBufConsumer} methods must only be called from one
     * thread, the {@link #eventLoop} thread. The other methods (subscribe, reserve) can be
     * called from any thread.
     */
    public static final class SharedBuffer extends BaseSharedBuffer implements ByteBufConsumer {
        private static final Supplier> LEAK_DETECTOR = SupplierUtil.memoized(() ->
            ResourceLeakDetectorFactory.instance().newResourceLeakDetector(SharedBuffer.class));
        @Nullable
        private final ResourceLeakTracker tracker = LEAK_DETECTOR.get().track(this);
        private final EventLoop eventLoop;
        /**
         * Buffered data. This is forwarded to new subscribers.
         */
        private CompositeByteBuf buffer;
        /**
         * Active subscribers that need the fully buffered body.
         */
        private List<@NonNull DelayedExecutionFlow> fullSubscribers;
        private ByteBuf addingBuffer;
        public SharedBuffer(EventLoop loop, BodySizeLimits limits, Upstream rootUpstream) {
            super(limits, rootUpstream);
            this.eventLoop = loop;
        }
        public void setExpectedLengthFrom(HttpHeaders headers) {
            setExpectedLengthFrom(headers.get(HttpHeaderNames.CONTENT_LENGTH));
        }
        boolean reserve() {
            if (eventLoop.inEventLoop() && addingBuffer == null) {
                reserve0();
                return false;
            } else {
                eventLoop.execute(this::reserve0);
                return true;
            }
        }
        @Override
        protected void reserve0() {
            super.reserve0();
            if (tracker != null) {
                tracker.record();
            }
        }
        /**
         * Add a subscriber. Must be preceded by a reservation.
         *
         * @param subscriber       The subscriber to add. Can be {@code null}, then the bytes will just be discarded
         * @param specificUpstream The upstream for the subscriber. This is used to call allowDiscard if there was an error
         * @param forceDelay       Whether to require an {@link EventLoop#execute} call to ensure serialization with previous {@link #reserve()} call
         */
        void subscribe(@Nullable ByteBufConsumer subscriber, Upstream specificUpstream, boolean forceDelay) {
            if (!forceDelay && eventLoop.inEventLoop() && addingBuffer == null) {
                subscribe0(subscriber, specificUpstream);
            } else {
                eventLoop.execute(() -> subscribe0(subscriber, specificUpstream));
            }
        }
        @Override
        protected void forwardInitialBuffer(@Nullable ByteBufConsumer subscriber, boolean last) {
            if (subscriber != null) {
                if (buffer != null) {
                    if (last) {
                        subscriber.add(buffer.slice());
                        buffer = null;
                    } else {
                        subscriber.add(buffer.retainedSlice());
                    }
                }
            } else {
                if (buffer != null && last) {
                    buffer.release();
                    buffer = null;
                }
            }
        }
        @Override
        protected void afterSubscribe(boolean last) {
            if (tracker != null) {
                if (last) {
                    tracker.close(this);
                } else {
                    tracker.record();
                }
            }
        }
        @Override
        protected ByteBuf subscribeFullResult(boolean last) {
            if (buffer == null) {
                return Unpooled.EMPTY_BUFFER;
            } else if (last) {
                ByteBuf buf = buffer;
                buffer = null;
                return buf;
            } else {
                return buffer.retainedSlice();
            }
        }
        /**
         * Optimized version of {@link #subscribe} for subscribers that want to buffer the full
         * body.
         *
         * @param specificUpstream The upstream for the subscriber. This is used to call allowDiscard if there was an error
         * @param forceDelay       Whether to require an {@link EventLoop#execute} call to ensure serialization with previous {@link #reserve()} call
         * @return A flow that will complete when all data has arrived, with a buffer containing that data
         */
        ExecutionFlow subscribeFull(Upstream specificUpstream, boolean forceDelay) {
            DelayedExecutionFlow asyncFlow = DelayedExecutionFlow.create();
            if (!forceDelay && eventLoop.inEventLoop() && addingBuffer == null) {
                return subscribeFull0(asyncFlow, specificUpstream, true);
            } else {
                eventLoop.execute(() -> {
                    ExecutionFlow res = subscribeFull0(asyncFlow, specificUpstream, false);
                    assert res == asyncFlow;
                });
                return asyncFlow;
            }
        }
        @Override
        public void add(ByteBuf buf) {
            addingBuffer = buf.touch();
            add(buf.readableBytes());
            addingBuffer = null;
        }
        @Override
        protected void addForward(List consumers) {
            for (ByteBufConsumer consumer : consumers) {
                consumer.add(addingBuffer.retainedSlice());
            }
        }
        @Override
        protected void addBuffer() {
            if (buffer == null) {
                buffer = addingBuffer.alloc().compositeBuffer();
            }
            buffer.addComponent(true, addingBuffer);
        }
        @Override
        protected void addDoNotBuffer() {
            addingBuffer.release();
        }
        @Override
        protected void discardBuffer() {
            if (buffer != null) {
                buffer.release();
                buffer = null;
            }
        }
    }
}