
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