package org.limewire.nio.channel; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.Channel; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.limewire.collection.Periodic; import org.limewire.nio.NIODispatcher; import org.limewire.nio.observer.Shutdownable; import org.limewire.nio.observer.WriteObserver; /** * Stores data within a buffer and writes it out after a configurable delay, * or if the buffer fills up. */ public class DelayedBufferWriter implements ChannelWriter, InterestWritableByteChannel { private static final Log LOG = LogFactory.getLog(DelayedBufferWriter.class); /** The default delay time to use before forcing a flush. */ private final static int DEFAULT_DELAY = 200; /** The channel to write to & interest on. */ private volatile InterestWritableByteChannel sink; /** The next observer. */ private volatile WriteObserver observer; /** * The buffer where we store delayed data. Most of the time it will be * written to, so we keep it in compacted state by default. */ private final ByteBuffer buf; /** The delay time to use before forcing a flush. */ private final long delay; private final Periodic interester; /** The last time we flushed, so we don't flush again too soon. */ private long lastFlushTime; /** Constructs a new <code>DelayedBufferWriter</code> whose buffer is the * given size. */ public DelayedBufferWriter(int size) { this(size, DEFAULT_DELAY); } /** * Constructs a new <code>DelayedBufferWriter</code> whose buffer is the * given size and delay. */ public DelayedBufferWriter(int size, long delay) { this(size, delay, NIODispatcher.instance().getScheduledExecutorService()); } DelayedBufferWriter(int size, long delay, ScheduledExecutorService scheduler) { buf = ByteBuffer.allocate(size); this.delay = TimeUnit.MILLISECONDS.toNanos(delay); this.interester = new Periodic(new Interester(), scheduler); } /** * Used by an observer to interest themselves in when something can write to * this. */ public synchronized void interestWrite(WriteObserver observer, boolean status) { if (status) { this.observer = observer; interester.unschedule(); LOG.debug("cancelling scheduled flush"); } else this.observer = null; InterestWritableByteChannel source = sink; if (source != null) source.interestWrite(this, true); } /** Closes the underlying channel. */ public void close() throws IOException { Channel chan = sink; if(chan != null) chan.close(); } /** Determines if the underlying channel is open. */ public boolean isOpen() { Channel chan = sink; return chan != null ? chan.isOpen() : false; } /** Retrieves the sink. */ public InterestWritableByteChannel getWriteChannel() { return sink; } /** Sets the sink. */ public void setWriteChannel(InterestWritableByteChannel newChannel) { sink = newChannel; newChannel.interestWrite(this,true); } /** Unused, Unsupported. */ public void handleIOException(IOException iox) { throw new RuntimeException("Unsupported", iox); } /** Shuts down the last observer. */ public void shutdown() { Shutdownable listener = observer; if(listener != null) listener.shutdown(); } /** * Writes data into the internal buffer. * <p> * If the internal buffer gets filled, it tries flushing some data out * to the sink. If some data can be flushed, this continues filling the * internal buffer. This continues forever until either the incoming * buffer is emptied or no data can be written to the sink. */ public int write(ByteBuffer buffer) throws IOException { int originalPos = buffer.position(); while(buffer.hasRemaining()) { if(buf.hasRemaining()) { int remaining = buf.remaining(); int adding = buffer.remaining(); if(remaining >= adding) { buf.put(buffer); } else { int oldLimit = buffer.limit(); int position = buffer.position(); buffer.limit(position + remaining); buf.put(buffer); buffer.limit(oldLimit); } } else { flush(System.nanoTime()); if (!buf.hasRemaining()) break; } } return buffer.position() - originalPos; } /** * Notification that a write can happen. The observer is informed of the event * in order to try filling our internal buffer. If our last flush was too long * ago, we force a flush to occur. We also force a flush if the observer is no * longer interested to make sure its last data is flushed from the buffer. */ public boolean handleWrite() throws IOException { WriteObserver upper = observer; if (upper != null) upper.handleWrite(); long now = System.nanoTime(); if (lastFlushTime == 0) lastFlushTime = now; if (now - lastFlushTime > delay) flush(now); synchronized (this) { // It is possible that between the above check for // interested.handleWrite & here, we got pre-empted // and another thread turned on interest. upper = observer; if (upper == null) { sink.interestWrite(this, false); // If still no data after that, we've written everything we want // -- exit. if (!hasBufferedData()) return false; else { // otherwise schedule a flushing event. interester.rescheduleIfLater(TimeUnit.NANOSECONDS.toMillis(lastFlushTime + delay - now)); } } } return true; } /** * Writes data to the underlying channel, remembering the time we did this * if anything was written. * <p> * <b>Important</b>: * <code>flush</code> does not block, nor does it enforce that all data will be written, * unlike <code>OutputStream.flush()</code>. * * @return true if the buffer is now empty */ public boolean flush() throws IOException { flush(System.nanoTime()); return !hasBufferedData(); } private void flush(long now) throws IOException { buf.flip(); InterestWritableByteChannel chan = sink; chan.write(buf); // if we wrote anything, consider this flushed if (hasBufferedData()) { lastFlushTime = now; if (buf.hasRemaining()) buf.compact(); else buf.clear(); } else { buf.position(buf.limit()).limit(buf.capacity()); } } private boolean hasBufferedData() { return buf.position() > 0; } private class Interester implements Runnable { public void run() { DelayedBufferWriter me = DelayedBufferWriter.this; synchronized (me) { InterestWritableByteChannel below = me.sink; WriteObserver above = observer; if (below != null && below.isOpen() && above == null && buf.position() > 0) { LOG.debug("forcing a flush"); below.interestWrite(me, true); } } } } public boolean hasBufferedOutput() { InterestWritableByteChannel channel = this.sink; return hasBufferedData() || (channel != null && channel.hasBufferedOutput()); } }