package lsr.common.nio; import java.io.IOException; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import lsr.common.KillOnExceptionHandler; import org.slf4j.LoggerFactory; /** * This class handles all keys registered in underlying selector. It is possible * to register new channels and changing interests in it. * <p> * Most methods has two version: normal and scheduled. Normal methods can only * be called from this thread. To invoke method from other thread, scheduled * version should be used. * * @see Selector */ public final class SelectorThread extends Thread { private final Selector selector; /** lock for tasks object; tasks cannot be used, as it is recreated often */ private final Object taskLock = new Object(); /** list of active tasks waiting for execution in selector thread */ private List<Runnable> tasks = new ArrayList<Runnable>(); /** * Initializes new thread responsible for handling channels. * * @throws IOException if an I/O error occurs */ public SelectorThread(int i) throws IOException { super("ClientIO-" + i); setDaemon(true); setDefaultUncaughtExceptionHandler(new KillOnExceptionHandler()); selector = Selector.open(); } /** * main loop which process all active keys in selector */ public void run() { LoggerFactory.getLogger(SelectorThread.class).info("Selector started."); // run main loop until thread is interrupted while (!Thread.interrupted()) { runScheduleTasks(); try { // FIXME: JK investigate if it is better to poll the tasks or to // use event-driven approach. Now polling is made, the previous // comment say it's better int selectedCount = selector.select(10); if (selectedCount > 0) { processSelectedKeys(); } } catch (IOException e) { // it shouldn't happen in normal situation so print stack trace // and kill the application throw new RuntimeException(e); } } } /** * Processes all keys currently selected by selector. Ready interest set is * always erased before handling it, so handler has to renew its interest. */ private void processSelectedKeys() { Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { // remove the selected key to not process it twice SelectionKey key = it.next(); it.remove(); // erase flags of ready operation key.interestOps(key.interestOps() & ~key.readyOps()); if (key.isAcceptable()) { ((AcceptHandler) key.attachment()).handleAccept(); if (!key.isValid()) continue; } if (key.isReadable()) { ((ReadWriteHandler) key.attachment()).handleRead(); if (!key.isValid()) continue; } if (key.isWritable()) { ((ReadWriteHandler) key.attachment()).handleWrite(); if (!key.isValid()) continue; } if (key.isConnectable()) { ((ConnectHandler) key.attachment()).handleConnect(); } } } /** * Invokes specified task asynchronously in <code>SelectorThread</code>. The * methods returns immediately. * * @param task - task to run in <code>SelectorThread</code> */ public void beginInvoke(Runnable task) { synchronized (taskLock) { tasks.add(task); // Do not wakeup the Selector thread by calling selector.wakeup(). // Instead, the selector will periodically poll the array with tasks // // selector.wakeup(); } } /** * Sets the interest set of specified channel(the old interest will be * erased). This method can be called from any thread. * * @param channel - the channel to change interest set for * @param operations - new interest set */ public void scheduleSetChannelInterest(final SelectableChannel channel, final int operations) { // Minimize locking time: create the object outside the critical // section. Runnable task = new Runnable() { public void run() { setChannelInterest(channel, operations); } }; synchronized (taskLock) { tasks.add(task); } } /** * Sets the interest set of specified channel (the old interest will be * erased). This method has to be call from <code>SelectorThread</code>. * * @param channel - the channel to change interest set for * @param operations - new interest set */ public void setChannelInterest(SelectableChannel channel, int operations) { assert this == Thread.currentThread() : "Method not called from selector thread"; SelectionKey key = channel.keyFor(selector); if (key != null && key.isValid()) { key.interestOps(operations); } } /** * Adds the interest set to specified channel. This method can be called * from any thread. * * @param channel - the channel to add interest set for * @param operations - new interest set */ public void scheduleAddChannelInterest(final SocketChannel channel, final int operations) { beginInvoke(new Runnable() { public void run() { addChannelInterest(channel, operations); } }); } /** * Adds the interest set to specified channel. This method has to be call * from <code>SelectorThread</code>. * * @param channel - the channel to add interest set for * @param operations - new interest set */ public void addChannelInterest(SelectableChannel channel, int operations) { assert this == Thread.currentThread() : "Method not called from selector thread"; SelectionKey key = channel.keyFor(selector); if (key != null && key.isValid()) { key.interestOps(key.interestOps() | operations); } } /** * Removes the interest set from specified channel. This method can be * called from any thread. * * @param channel - the channel to remove interest set for * @param operations - interests to remove */ public void scheduleRemoveChannelInterest(final SocketChannel channel, final int operations) { beginInvoke(new Runnable() { public void run() { removeChannelInterest(channel, operations); } }); } /** * Removes the interest set from specified channel. This method has to be * call from <code>SelectorThread</code>. * * @param channel - the channel to remove interest set for * @param operations - interests to remove */ public void removeChannelInterest(SelectableChannel channel, int operations) { assert this == Thread.currentThread() : "Method not called from selector thread"; SelectionKey key = channel.keyFor(selector); if (key != null && key.isValid()) { key.interestOps(key.interestOps() & ~operations); } } /** * Registers specified channel and handler to underlying selector. This * method has to be call from <code>SelectorThread</code>. * * @param channel - channel to register in selector * @param operations - the initial interest operations for channel * @param handler - notified about every ready operation on channel * * @throws IOException if an I/O error occurs */ public void scheduleRegisterChannel(final SelectableChannel channel, final int operations, final Object handler) { beginInvoke(new Runnable() { public void run() { try { registerChannel(channel, operations, handler); } catch (IOException e) { e.printStackTrace(); System.exit(1); } } }); } /** * Registers specified channel and handler to underlying selector. This * method can only be called from selector thread. * * @param channel - channel to register in selector * @param operations - the initial interest operations for channel * @param handler - notified about every ready operation on channel * * @throws IOException if an I/O error occurs */ public void registerChannel(SelectableChannel channel, int operations, Object handler) throws IOException { assert this == Thread.currentThread() : "Method not called from selector thread"; if (!channel.isOpen()) { throw new IOException("Channel is closed"); } if (channel.isRegistered()) { SelectionKey key = channel.keyFor(selector); assert key != null : "The channel is not registered to selector?"; key.interestOps(operations); key.attach(handler); } else { channel.configureBlocking(false); channel.register(selector, operations, handler); } } /** Runs all schedule tasks in selector thread. */ private void runScheduleTasks() { // To minimize the time the lock is held, make a copy of the array // with the tasks while holding the lock then release the lock and // execute the tasks List<Runnable> tasksCopy; synchronized (taskLock) { if (tasks.isEmpty()) { return; } tasksCopy = tasks; tasks = new ArrayList<Runnable>(4); } for (Runnable task : tasksCopy) { task.run(); } } public boolean amIInSelector() { return Thread.currentThread() == this; } }