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

co.easimart.vertx.stream.ReadBufferInputStream Maven / Gradle / Ivy

package co.easimart.vertx.stream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import javax.annotation.Nonnull;

import io.netty.buffer.ByteBuf;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.streams.ReadStream;

/**
 * InputStream which wraps ReadStream<Buffer>
 */
public class ReadBufferInputStream extends InputStream {

    private static Logger logger = LoggerFactory.getLogger(ReadBufferInputStream.class);

    private final ReadStream readStream;
    private Long totalReadSize;

    protected void setupHandlers() {
        this.readStream.handler(buffer -> {
            //noinspection ThrowableResultOfMethodCallIgnored
            if (closed.get() || canceled.get() != null) {
                this.readStream.handler(null);
                return;
            }

            ByteBuf buf = buffer.getByteBuf();
            int bufSize = buf.readableBytes();
            this.totalReadSize += bufSize;
            buffers.add(buf);

            if (this.currentBufferSize.compareAndSet(0, bufSize)) { // buffer is empty
                synchronized (this) {
                    this.notifyAll(); // wake up all readers
                }
            } else if (this.currentBufferSize.addAndGet(bufSize) >= MAX_BUFFER_SIZE) { // buffer is full now
                pauseReadStreamIfBufferIsFull();
            }

        });
        this.readStream.endHandler(r -> {
            this.completed.set(true);
            if (isBufferEmpty()) {
                synchronized (this) {
                    this.notifyAll();
                }
            }
        });
        this.readStream.exceptionHandler(this::cancel);
    }

    private void pauseReadStreamIfBufferIsFull() {
        synchronized (this.paused) { // synchronized between pause and resume
            // Pause stream when buffer is full now, stream has not completed and it has not been paused.
            // because data handler still get called sometimes even the steam has been paused.
            if (this.isBufferFull() && !this.completed.get() && this.paused.compareAndSet(false, true)) {
                this.readStream.pause(); // Stop read stream until buffer has more space
            }
        }
    }

    private void resumeReadStreamIfBufferIsNotFull() {
        synchronized (this.paused) {
            // Resume stream when buffer has space, stream has not completed and stream is paused.
            if (!this.isBufferFull() && !this.completed.get() && this.paused.compareAndSet(true, false)) {
                this.readStream.resume();
            }
        }
    }

    private final ConcurrentLinkedQueue buffers;
    private final AtomicInteger currentBufferSize;
    private final AtomicBoolean completed;
    private final AtomicBoolean closed;
    private final AtomicBoolean paused;
    private final AtomicReference canceled;
    private Long totalConsumedSize;
    final int MAX_BUFFER_SIZE = 4 * 1024 * 1024;

    public ReadBufferInputStream(ReadStream readStream) {
        this.readStream = readStream;
        setupHandlers();

        this.buffers = new ConcurrentLinkedQueue<>();
        this.currentBufferSize = new AtomicInteger(0);
        this.completed = new AtomicBoolean(false);
        this.closed = new AtomicBoolean(false);
        this.canceled = new AtomicReference<>(null);
        this.paused = new AtomicBoolean(false);
        this.totalConsumedSize = 0L;
        this.totalReadSize = 0L;
    }

    public boolean isBufferFull() {
        return this.currentBufferSize.get() >= MAX_BUFFER_SIZE;
    }

    public boolean isBufferEmpty() {
        return (this.currentBufferSize.get() <= 0);
    }

    private void checkClosedOrCanceled() throws IOException {
        if (closed.get()) throw new IOException("The stream has been closed");
        Throwable cause = canceled.get();
        if (cause != null) throw new IOException("The stream is unexpectedly stopped", cause);
    }

    /**
     * Reads the next byte of data from this input stream. The
     * value byte is returned as an int in the range
     * 0 to 255.
     * This method blocks until input data is available, the end of the
     * stream is detected, or an exception is thrown.
     *
     * @return the next byte of data, or -1 if the end of the
     * stream is reached.
     * @throws IOException if an I/O error occurs.
     */
    public synchronized int read() throws IOException {
        byte[] b = new byte[1];
        for (int i = 0; i < 10; i++) {
            int r = this.read(b, 0, 1);
            if (r < 0) return r;
            if (r == 1) return b[0];
        }
        throw new IllegalStateException("Cannot read one byte because InputStream.read() keeps returning zero for 10 times.");
    }

    /**
     * Reads up to len bytes of data from this piped input
     * stream into an array of bytes. Less than len bytes
     * will be read if the end of the data stream is reached or if
     * len exceeds the pipe's buffer size.
     * If len  is zero, then no bytes are read and 0 is returned;
     * otherwise, the method blocks until at least 1 byte of input is
     * available, end of the stream has been detected, or an exception is
     * thrown.
     *
     * @param b   the buffer into which the data is read.
     * @param off the start offset in the destination array b
     * @param len the maximum number of bytes read.
     * @return the total number of bytes read into the buffer, or
     * -1 if there is no more data because the end of
     * the stream has been reached.
     * @throws NullPointerException      If b is null.
     * @throws IndexOutOfBoundsException If off is negative,
     *                                   len is negative, or len is greater than
     *                                   b.length - off
     * @throws IOException               if an I/O error occurs.
     */
    public synchronized int read(@Nonnull byte b[], int off, int len) throws IOException {
        checkClosedOrCanceled();

        while (isBufferEmpty()) {
            if (completed.get()) return -1;
            checkClosedOrCanceled();

            // wait when buffer gets something or timeout
            logger.debug("Stream is waiting. read={}, consumed={}, buffer size={}", getTotalReceivedSize(), getTotalConsumedSize(), currentBufferSize.get());
            try {
                wait(10000);
            } catch (InterruptedException ignored) {
            }
        }

        Queue bufferList = this.buffers;
        ByteBuf buf = bufferList.peek();
        int canRead = Math.min(len, buf.readableBytes());
        buf.readBytes(b, off, canRead);
        if (buf.readableBytes() == 0) {
            bufferList.poll();
        }

        // update buffer size
        reclaimBufferSpace(canRead);
        this.totalConsumedSize += canRead;

        return canRead;
    }

    private boolean reclaimBufferSpace(int size) {
        long newSize = this.currentBufferSize.addAndGet(-size);

        if (newSize + size >= MAX_BUFFER_SIZE && newSize < MAX_BUFFER_SIZE) { // buffer was full and has more space now
            resumeReadStreamIfBufferIsNotFull();
            return true;
        }
        return false;
    }

    /**
     * Returns the number of bytes that can be read from this input
     * stream without blocking.
     *
     * @return the number of bytes that can be read from this input stream
     * without blocking, or {@code 0} if this input stream has been
     * closed by invoking its {@link #close()} method.
     * @throws IOException if an I/O error occurs.
     * @since JDK1.0.2
     */
    public synchronized int available() throws IOException {
        return this.currentBufferSize.get();
    }

    /**
     * Closes this piped input stream and releases any system resources
     * associated with the stream.
     *
     * @throws IOException if an I/O error occurs.
     */
    public synchronized void close() throws IOException {
        this.closed.set(true);
        this.buffers.clear();
        this.currentBufferSize.set(0);
        notifyAll();
    }

    public void cancel(Throwable cause) {
        this.canceled.set(cause);
        this.buffers.clear();
        this.currentBufferSize.set(0);
        synchronized (this) {
            notifyAll();
        }
    }

    public Long getTotalConsumedSize() {
        return totalConsumedSize;
    }

    public Long getTotalReceivedSize() {
        return totalReadSize;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy