package com.limegroup.gnutella.io; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.Channel; import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.Log; /** * A writer that throttles data according to a throttle. * * To work with the Throttle, this uses an attachment (which must be the same as the * attachment of the SelectionKey associated with the socket this is using). */ public class ThrottleWriter implements ChannelWriter, InterestWriteChannel, ThrottleListener { private static final Log LOG = LogFactory.getLog(ThrottleWriter.class); /** The channel to write to & interest on. */ private volatile InterestWriteChannel channel; /** The last observer. */ private volatile WriteObserver observer; /** The throttle we're using. */ private final Throttle throttle; /** The amount of data we were told we can write. */ private int available; /** The object that the Throttle will recognize as the SelectionKey attachments */ private Object attachment; /** * Constructs a ThrottleWriter with the given Throttle. * * You MUST call setWriteChannel prior to using this. */ public ThrottleWriter(Throttle throttle) { this(throttle, null); } /** * Constructs a new ThrottleWriter with the given throttle & channel. */ public ThrottleWriter(Throttle throttle, InterestWriteChannel channel) { this.throttle = throttle; this.channel = channel; } /** Retreives the sink. */ public InterestWriteChannel getWriteChannel() { return channel; } /** Sets the sink. */ public void setWriteChannel(InterestWriteChannel channel) { this.channel = channel; throttle.interest(this); } /** Sets the attachment that the Throttle will recognize for this Writer. */ public void setAttachment(Object att) { attachment = att; } /** Gets the attachment. */ public Object getAttachment() { return attachment; } /** * Tells the Throttle that we're interested in receiving bandwidthAvailable * events at some point in time. */ public void interest(WriteObserver observer, boolean status) { if(status) { this.observer = observer; if(channel != null) throttle.interest(this); } else { this.observer = null; } } /** * Notification from the Throttle that bandwidth is available. * Returns false if this no longer is open & will not be interested * ever again. */ public boolean bandwidthAvailable() { if(channel.isOpen()) { channel.interest(this, true); return true; } else { return false; } } /** * Writes data to the chain. * * Only writes up to 'available' amount of data. */ public int write(ByteBuffer buffer) throws IOException { InterestWriteChannel chain = channel; if(chain == null) throw new IllegalStateException("writing with no chain!"); if(available == 0) return 0; int priorLimit = buffer.limit(); if(buffer.remaining() > available) buffer.limit(buffer.position() + available); int totalWrote = channel.write(buffer); available -= totalWrote; buffer.limit(priorLimit); return totalWrote; } /** Closes the underlying channel. */ public void close() throws IOException { Channel source = channel; if(source != null) source.close(); } /** Determines if the underlying channel is open. */ public boolean isOpen() { Channel source = channel; return source != null ? source.isOpen() : false; } /** * Requests some space from the Throttle to write data. * * A global 'available' variable is set, and it is assumed that * the interested party will try writing to us. Our chained-write * is limited to the available amount, and available is decremented. * We then release the amount of space that we couldn't write. */ public boolean handleWrite() throws IOException { InterestWriteChannel chain = channel; if(chain == null) throw new IllegalStateException("writing with no source."); WriteObserver interested = observer; available = throttle.request(); // If nothing is available, DO NOT CHANGE INTEREST WITHOUT // TRYING TO WRITE. Otherwise, because of a bug(?) in selecting, // we will not be immediately notified again that data can be // written. If we leave the interest alone then we will be // notified again. if(available != 0) { try { chain.interest(this, false); if(interested != null) interested.handleWrite(); } finally { throttle.release(available); } interested = observer; // re-get it, since observer may have changed interest. if(interested != null) { throttle.interest(this); return true; } else { return false; } } else { return true; } } /** Shuts down the last observer. */ public void shutdown() { Shutdownable listener = observer; if(listener != null) listener.shutdown(); } /** Unused, Unsupported */ public void handleIOException(IOException x) { throw new RuntimeException("Unsupported", x); } }