package rocks.inspectit.shared.all.storage.nio.stream; import java.io.IOException; import java.nio.ByteBuffer; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import com.esotericsoftware.kryo.io.ByteBufferInputStream; import rocks.inspectit.shared.all.spring.logger.Log; import rocks.inspectit.shared.all.storage.nio.ByteBufferProvider; /** * This is abstract class for all input streams that can read data with limited numbers of buffers. * <p> * The implementing classes must ensure that a buffer is taken from the empty buffers queue, filled * with data and then placed in the full buffers queue. It's on the implementing classes to * introduce the logic for this. * <p> * This class implements the classic input stream methods like {@link #read()}, * {@link #read(byte[])} and {@link #read(byte[], int, int)}, as well as {@link #close()} and * {@link #prepare()}. Thus, sub-classes don't have to care about implementing this, as long as they * provide the data in the full buffers queue. * * @author Ivan Senic * */ public abstract class AbstractExtendedByteBufferInputStream extends ByteBufferInputStream { /** * Minimum amount of buffers that can be used. */ private static final int MIN_BUFFERS = 2; /** * Maximum amount of buffers that can be used. */ private static final int MAX_BUFFERS = 5; /** * MAx amount of tries to get the full or empty buffer from the queues. */ protected static final int MAX_BUFFER_POOL_TRIES = 100; /** * Logger of this class. */ @Log protected Logger log; /** * {@link ByteBufferProvider}. */ @Autowired ByteBufferProvider byteBufferProvider; /** * Amount of buffers to use during read. */ private int numberOfBuffers; /** * Total size that has to be read. */ private long totalSize; /** * Current streaming position. */ private long position; /** * Queue of empty buffers. These buffers will be filled with the information from the disk. */ private LinkedBlockingQueue<ByteBuffer> emptyBuffers = new LinkedBlockingQueue<ByteBuffer>(); /** * Queue of full buffers. These buffers will be used to stream data. */ private LinkedBlockingQueue<ByteBuffer> fullBuffers = new LinkedBlockingQueue<ByteBuffer>(); /** * If stream has been closed. */ private volatile boolean closed; /** * Boolean for flagging that in some way reading has failed. For example a read from socket * returned an IO exception or the read resulted with -1 read size signaling that the stream has * been closed unexpectedly. * <p> * Sub-classes must set this to true when data could not be provided anymore (stream reached end * for example). */ private volatile boolean readFailed; /** * No-arg constructor. */ public AbstractExtendedByteBufferInputStream() { } /** * Constructor that defines number of bytes to use. * * @param numberOfBuffers * Number of buffers. */ public AbstractExtendedByteBufferInputStream(int numberOfBuffers) { this.numberOfBuffers = numberOfBuffers; } /** * Prepares the stream for read. Must be called before any read operation is executed. * <p> * Implementing classes must extend this method in way that full buffers queue is filled with * data that will be available for the reader of input stream. * * @throws IOException * if preparation fails due to inability to obtain defined number of byte buffers */ public void prepare() throws IOException { // get the buffers first int buffers = numberOfBuffers; if (buffers < MIN_BUFFERS) { buffers = MIN_BUFFERS; } else if (buffers > MAX_BUFFERS) { buffers = MAX_BUFFERS; } for (int i = 0; i < buffers; i++) { ByteBuffer byteBuffer = byteBufferProvider.acquireByteBuffer(); emptyBuffers.add(byteBuffer); } numberOfBuffers = buffers; } /** * Returns if the stream has more bytes remaining to stream. * * @return True if stream can provide more bytes. */ public boolean hasRemaining() { return bytesLeft() > 0; } /** * {@inheritDoc} */ @Override public int available() throws IOException { return (int) bytesLeft(); } /** * {@inheritDoc} */ @Override public int read() throws IOException { // if we are empty, return -1 by the input stream contract if ((0 == totalSize) || !hasRemaining()) { return -1; } try { // change the buffer if necessary if (hasRemaining() || (null == super.getByteBuffer())) { bufferChange(); } if (!super.getByteBuffer().hasRemaining()) { // check if we can read more if (hasRemaining()) { bufferChange(); int read = super.read(); position += read; return read; } else { return -1; } } else { int read = super.read(); position += read; return read; } } catch (ReadFailedException e) { throw new IOException("Read from the input stream failed.", e); } } /** * {@inheritDoc} */ @Override public int read(byte[] b, int off, int len) throws IOException { // if we are empty, return -1 by the input stream contract if ((0 == totalSize) || !hasRemaining()) { return -1; } // don't try to read anything if the required length is 0 if (0 == len) { return 0; } try { // change the buffer if necessary if (hasRemaining() && (null == super.getByteBuffer())) { bufferChange(); } int bufferRemaining = super.getByteBuffer().remaining(); if (bufferRemaining >= len) { int read = super.read(b, off, len); position += read; return read; } else { int res = 0; if (bufferRemaining > 0) { super.getByteBuffer().get(b, off, bufferRemaining); res = bufferRemaining; position += bufferRemaining; } if (hasRemaining()) { bufferChange(); int read = this.read(b, off + bufferRemaining, len - bufferRemaining); res += read; } if (res > 0) { return res; } else { return -1; } } } catch (ReadFailedException e) { log.warn("Read failed, can not get full byte buffer.", e); throw new IOException("Read from the input stream failed.", e); } } /** * Changes the current buffer used for streaming with a full one. * * @throws ReadFailedException * if buffer change can not be performed due to the flagged {@link #readFailed}. */ private synchronized void bufferChange() throws ReadFailedException { ByteBuffer current = super.getByteBuffer(); if (null != current) { current.clear(); emptyBuffers.add(current); } int tries = 0; while (true) { try { // poll for full buffer ByteBuffer buffer = fullBuffers.poll(100, TimeUnit.MILLISECONDS); if (null != buffer) { // if we have full buffer, set is as current and break from while super.setByteBuffer(buffer); break; } else { tries++; if (readFailed) { throw new ReadFailedException("Read failed signal received."); } else if (tries > MAX_BUFFER_POOL_TRIES) { throw new ReadFailedException("Time-out trying to get the full byte buffer to read after " + TimeUnit.MILLISECONDS.toSeconds(100 * MAX_BUFFER_POOL_TRIES) + " sec."); } } } catch (InterruptedException e) { Thread.interrupted(); } } } /** * Return number of bytes left for read. * * @return Number of bytes left. */ private long bytesLeft() { return totalSize - position; } /** * {@inheritDoc} * <p> * Releases all byte buffers that are hold. */ @Override public synchronized void close() throws IOException { if (closed) { return; } int releasedBuffers = 0; while (releasedBuffers < numberOfBuffers) { // release buffers from both queues while (!fullBuffers.isEmpty()) { ByteBuffer byteBuffer = fullBuffers.poll(); if (null != byteBuffer) { byteBufferProvider.releaseByteBuffer(byteBuffer); releasedBuffers++; } } while (!emptyBuffers.isEmpty()) { ByteBuffer byteBuffer = emptyBuffers.poll(); if (null != byteBuffer) { byteBufferProvider.releaseByteBuffer(byteBuffer); releasedBuffers++; } } // also release the one we could have set for current reading ByteBuffer currentBuffer = super.getByteBuffer(); if (null != currentBuffer) { byteBufferProvider.releaseByteBuffer(currentBuffer); releasedBuffers++; super.setByteBuffer(null); } } closed = true; } /** * Gets {@link #totalSize}. * * @return {@link #totalSize} */ public long getTotalSize() { return totalSize; } /** * Sets {@link #totalSize}. * * @param totalSize * New value for {@link #totalSize} */ public void setTotalSize(long totalSize) { this.totalSize = totalSize; } /** * Gets {@link #emptyBuffers}. * * @return {@link #emptyBuffers} */ public LinkedBlockingQueue<ByteBuffer> getEmptyBuffers() { return emptyBuffers; } /** * Gets {@link #fullBuffers}. * * @return {@link #fullBuffers} */ public LinkedBlockingQueue<ByteBuffer> getFullBuffers() { return fullBuffers; } /** * Gets {@link #closed}. * * @return {@link #closed} */ public boolean isClosed() { return closed; } /** * Sets {@link #byteBufferProvider}. * * @param byteBufferProvider * New value for {@link #byteBufferProvider} */ public void setByteBufferProvider(ByteBufferProvider byteBufferProvider) { this.byteBufferProvider = byteBufferProvider; } /** * Sets {@link #position}. * * @param position * New value for {@link #position} */ protected void setPosition(long position) { this.position = position; } /** * Sets {@link #readFailed}. * * @param readFailed * New value for {@link #readFailed} */ protected void setReadFailed(boolean readFailed) { this.readFailed = readFailed; } /** * Sets {@link #log}. * * @param log * New value for {@link #log} */ void setLog(Logger log) { this.log = log; } /** * {@inheritDoc} * <p> * Closing the stream on finalize. */ @Override protected void finalize() throws Throwable { this.close(); super.finalize(); } /** * Checked exception to be used when read has failed. As this will not be propagated outside * this class, it's private and final. * * @author Ivan Senic * */ private static final class ReadFailedException extends Exception { /** * Generated UID. */ private static final long serialVersionUID = 706702393154564017L; /** * Default constructor. * * @param message * Message */ ReadFailedException(String message) { super(message); } } }