package netflix.karyon.transport.util; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import rx.Observable; import rx.Observer; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author Nitesh Kant */ public class HttpContentInputStream extends InputStream { private static final Logger logger = LoggerFactory.getLogger(HttpContentInputStream.class); private final Lock lock = new ReentrantLock(); private volatile boolean isClosed = false; private volatile boolean isCompleted = false; private volatile Throwable completedWithError = null; private final Condition contentAvailabilityMonitor = lock.newCondition(); private final ByteBuf contentBuffer; public HttpContentInputStream(final ByteBufAllocator allocator, final Observable<ByteBuf> content) { contentBuffer = allocator.buffer(); content.subscribe(new Observer<ByteBuf>() { @Override public void onCompleted() { lock.lock(); try { isCompleted = true; logger.debug( "Processing complete" ); contentAvailabilityMonitor.signalAll(); } finally { lock.unlock(); } } @Override public void onError(Throwable e) { lock.lock(); try { completedWithError = e; isCompleted = true; logger.error("Observer, got error: " + e.getMessage()); contentAvailabilityMonitor.signalAll(); } finally { lock.unlock(); } } @Override public void onNext(ByteBuf byteBuf) { lock.lock(); try { //This is not only to stop writing 0 bytes as it might seems //In case of no payload request, like GET //We are getting onNext( 0 bytes ), onComplete during the stress conditions //AFTER we say subscriber.onCompleted() and tiered down and close //request stream, this doesn't contradict logic, in fact 0 bytes is just the same as nothing to write //but that save us annoying log record every time this happens if( byteBuf.readableBytes() > 0 ) { contentBuffer.writeBytes( byteBuf ); } contentAvailabilityMonitor.signalAll(); } catch( Exception e ) { logger.error("Error on server", e); } finally { lock.unlock(); } } }); } @Override public int available() throws IOException { return isCompleted ? contentBuffer.readableBytes() : 0; } @Override public void mark(int readlimit) { contentBuffer.markReaderIndex(); } @Override public boolean markSupported() { return true; } @Override public int read() throws IOException { lock.lock(); try { if( !await() ) { return -1; } return contentBuffer.readByte() & 0xff; } finally { lock.unlock(); } } @Override public int read(final byte[] b, final int off, final int len) throws IOException { if( b == null ) { throw new NullPointerException( "Null buffer" ); } if( len < 0 || off < 0 || len > b.length - off ) { throw new IndexOutOfBoundsException( "Invalid index" ); } if( len == 0 ) { return 0; } lock.lock(); try { if( !await() ) { return -1; } int size = Math.min( len, contentBuffer.readableBytes() ); contentBuffer.readBytes( b, off, size ); return size; } finally { lock.unlock(); } } @Override public void close() throws IOException { //we need a sync block here, as the double close sometimes is reality and we want to decrement ref. counter only once synchronized ( this ) { if ( isClosed ) { return; } isClosed = true; } contentBuffer.release(); } @Override public void reset() throws IOException { contentBuffer.resetReaderIndex(); } @Override public long skip(long n) throws IOException { //per InputStream contract, if n is negative, 0 bytes are skipped if( n <= 0 ) { return 0; } if (n > Integer.MAX_VALUE) { return skipBytes(Integer.MAX_VALUE); } else { return skipBytes((int) n); } } private int skipBytes(int n) throws IOException { int nBytes = Math.min(available(), n); contentBuffer.skipBytes(nBytes); return nBytes; } private boolean await() throws IOException { //await in here while( !isCompleted && !contentBuffer.isReadable() ) { try { contentAvailabilityMonitor.await(); } catch (InterruptedException e) { // Restore interrupt status and bailout Thread.currentThread().interrupt(); logger.error("Interrupted: " + e.getMessage()); throw new IOException( e ); } } if( completedWithError != null ) { throw new IOException( completedWithError ); } if( isCompleted && !contentBuffer.isReadable() ) { return false; } return true; } }