/**
*
*/
package com.trendrr.oss.networking;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.channels.spi.SelectorProvider;
import java.util.Iterator;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentSkipListSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.trendrr.oss.exceptions.TrendrrDisconnectedException;
import com.trendrr.oss.exceptions.TrendrrException;
/**
* @author Dustin Norlander
* @created Mar 11, 2011
*
*/
public class SelectorThread implements Runnable{
protected static Log log = LogFactory.getLog(SelectorThread.class);
private static SelectorThread instance = null;
public static synchronized SelectorThread registerChannel(SocketChannelWrapper wrapper) throws IOException {
boolean startThread = instance == null;
if (startThread) {
instance = new SelectorThread();
}
instance.register(wrapper);
if (startThread) {
Thread t = new Thread(instance);
t.setDaemon(true);
t.start();
}
return instance;
}
private Selector selector;
private ConcurrentHashMap<SocketChannel, SocketChannelWrapper> channels = new ConcurrentHashMap<SocketChannel, SocketChannelWrapper>();
private ConcurrentLinkedQueue<SocketChannelWrapper> changeQueue = new ConcurrentLinkedQueue<SocketChannelWrapper>();
private ConcurrentLinkedQueue<SocketChannelWrapper> registerQueue = new ConcurrentLinkedQueue<SocketChannelWrapper>();
public SelectorThread() throws IOException {
this.selector = SelectorProvider.provider().openSelector();
}
public void register(SocketChannelWrapper wrapper) throws IOException {
this.channels.put(wrapper.getChannel(), wrapper);
wrapper.getChannel().configureBlocking(false); //set to non-blocking
registerQueue.add(wrapper);
this.selector.wakeup();
}
/**
* registers that something has changed with the wrapper (read or writes waiting, or disconnect).
* @param wrapper
*/
public void registerChange(SocketChannelWrapper wrapper) {
changeQueue.add(wrapper);
this.selector.wakeup();
}
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
while (true) {
SelectionKey key = null;
try {
//register any sockets that need it.
while(!this.registerQueue.isEmpty()) {
SocketChannelWrapper wrapper = registerQueue.poll();
//keep key on case of disconnect.
key = wrapper.getChannel().register(this.selector, SelectionKey.OP_READ); //set our interest to reads
}
//Process any op changes.
while(!this.changeQueue.isEmpty()) {
SocketChannelWrapper wrapper = changeQueue.poll();
if (wrapper.hasWrites()) {
// System.out.println("HAS WRITES!");
key = wrapper.getChannel().keyFor(this.selector);
key.interestOps(SelectionKey.OP_WRITE);
} else if (wrapper.hasReads()) {
// System.out.println("HAS READS!");
key = wrapper.getChannel().keyFor(this.selector);
key.interestOps(SelectionKey.OP_READ);
} else {
key = wrapper.getChannel().keyFor(this.selector);
key.interestOps(0);
}
if (wrapper.isClosed()) {
continue;
}
}
// Wait for an event one of the registered channels
// log.info("SELECTOR WAITING>>>");
this.selector.select();
// Iterate over the set of keys for which events are available
Iterator<SelectionKey> selectedKeys = this.selector.selectedKeys().iterator();
while (selectedKeys.hasNext()) {
key = selectedKeys.next();
selectedKeys.remove();
if (!key.isValid()) {
continue;
}
// Check what event is available and deal with it
if (key.isReadable()) {
// log.info("READING!");
this.read(key);
} else if (key.isWritable()) {
// log.info("WRITING!");
this.write(key);
}
}
} catch (TrendrrDisconnectedException x) {
this.channels.get((SocketChannel)key.channel()).close();
} catch (IOException e) {
if (e.getMessage().equals("Broken pipe")) {
this.channels.get((SocketChannel)key.channel()).close();
}
} catch (CancelledKeyException x) {
//do nothing, will run through the ops and remove them.
} catch (Exception e) {
log.error("Caught", e);
}
}
}
/**
* unregisters the wrapper with this selector thread.
*
* it is unnecessary to call this if the wrapper.close() method is invoked.
*/
public void unregister(SocketChannelWrapper wrapper) {
this.channels.remove(wrapper.getChannel());
wrapper.getChannel().keyFor(this.selector).cancel();
}
private void read(SelectionKey key) throws IOException, TrendrrDisconnectedException, TrendrrException {
SocketChannel socketChannel = (SocketChannel) key.channel();
SocketChannelWrapper wrapper = this.channels.get(socketChannel);
try {
wrapper.doRead();
} finally {
if (!wrapper.hasReads()) {
if (wrapper.hasWrites()) {
//there are writes waiting, so we register that for next time.
this.registerChange(wrapper);
} else {
key.interestOps(0);
}
}
}
}
private void write(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
SocketChannelWrapper wrapper = this.channels.get(socketChannel);
if (!wrapper.hasWrites()) {
this.registerChange(wrapper);
return;
}
Queue<ByteBuffer> queue = wrapper.getWrites();
while (!queue.isEmpty()) {
//does not remove the head element until the buf is completely written
//maybe that's now, maybe later..
ByteBuffer buf = queue.peek();
// System.out.println("WRITING: " + buf);
if (buf == null) {
break; //queue is empty.
}
socketChannel.write(buf);
if (buf.remaining() > 0) {
// ... or the socket's buffer fills up
// System.out.println("buffer isn't totally written");
break;
}
queue.poll(); //remove the head element
wrapper.numQueued.decrementAndGet();
}
if (queue.isEmpty()) {
this.registerChange(wrapper);
}
}
}