package org.deftserver.io; import static com.google.common.collect.Collections2.transform; import java.io.IOException; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import org.deftserver.io.callback.CallbackManager; import org.deftserver.io.callback.JMXDebuggableCallbackManager; import org.deftserver.io.timeout.JMXDebuggableTimeoutManager; import org.deftserver.io.timeout.Timeout; import org.deftserver.io.timeout.TimeoutManager; import org.deftserver.util.MXBeanUtil; import org.deftserver.web.AsyncCallback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.base.Function; import com.google.common.collect.Lists; import com.google.common.collect.Maps; public class IOLoop implements IOLoopMXBean { /* IOLoop singleton to use for convenience (otherwise you would have to pass around the * IOLoop instance explicitly, now you can simply use IOLoop.INSTANCE) */ public static final IOLoop INSTANCE = new IOLoop(); private boolean running = false; private final Logger logger = LoggerFactory.getLogger(IOLoop.class); private Selector selector; private final Map<SelectableChannel, IOHandler> handlers = Maps.newHashMap(); private final TimeoutManager tm = new JMXDebuggableTimeoutManager(); private final CallbackManager cm = new JMXDebuggableCallbackManager(); private static final AtomicInteger sequence = new AtomicInteger(); public IOLoop() { try { selector = Selector.open(); } catch (IOException e) { logger.error("Could not open selector: {}", e.getMessage()); } MXBeanUtil.registerMXBean(this, "IOLoop"); } /** * Start the io loop. The thread that invokes this method will be blocked (until {@link IOLoop#stop} is invoked) * and will be the io loop thread. */ public void start() { Thread.currentThread().setName("I/O-LOOP" + sequence.incrementAndGet()); running = true; long selectorTimeout = 250; // 250 ms while (running) { try { if (selector.select(selectorTimeout) == 0) { long ms = tm.execute(); selectorTimeout = Math.min(ms, /*selectorTimeout*/ 250); if (cm.execute()) { selectorTimeout = 1; } continue; } Iterator<SelectionKey> keys = selector.selectedKeys().iterator(); while (keys.hasNext()) { SelectionKey key = keys.next(); IOHandler handler = handlers.get(key.channel()); if (key.isAcceptable()) { handler.handleAccept(key); } if (key.isConnectable()) { handler.handleConnect(key); } if (key.isValid() && key.isReadable()) { handler.handleRead(key); } if (key.isValid() && key.isWritable()) { handler.handleWrite(key); } keys.remove(); } long ms = tm.execute(); selectorTimeout = Math.min(ms, /*selectorTimeout*/ 250); if (cm.execute()) { selectorTimeout = 1; } } catch (IOException e) { logger.error("Exception received in IOLoop: {}", e); } } } /** * Stop the io loop and release the thread (io loop thread) that invoked the {@link IOLoop#start} method. */ public void stop() { running = false; logger.debug("Stopping IOLoop..."); } /** * Registers a new {@code IOHandler} with this {@code IOLoop}. * * @param channel The {@code SelectableChannel} * @param handler {@code IOHandler that will receive the io callbacks.} * @param interestOps See {@link SelectionKey} for valid values. (Xor for multiple interests). * @param attachment The {@code attachment} that will be accessible from the returning {@code SelectionKey}s * attachment. * */ public SelectionKey addHandler(SelectableChannel channel, IOHandler handler, int interestOps, Object attachment) { handlers.put(channel, handler); return registerChannel(channel, interestOps, attachment); } /** * Unregisters the previously registered {@code IOHandler}. * @param channel The {@code SelectableChannel} that was registered with a user defined {@code IOHandler} */ public void removeHandler(SelectableChannel channel) { handlers.remove(channel); } /** * Update an earlier registered {@code SelectableChannel} * * @param channel The {@code SelectableChannel} * @param newInterestOps The complete new set of interest operations. */ public void updateHandler(SelectableChannel channel, int newInterestOps) { if (handlers.containsKey(channel)) { channel.keyFor(selector).interestOps(newInterestOps); } else { logger.warn("Tried to update interestOps for an unknown SelectableChannel."); } } /** * * @param channel * @param interestOps * @param attachment * @return */ private SelectionKey registerChannel(SelectableChannel channel, int interestOps, Object attachment) { try { return channel.register(selector, interestOps, attachment); } catch (ClosedChannelException e) { removeHandler(channel); logger.error("Could not register channel: {}", e.getMessage()); } return null; } public void addKeepAliveTimeout(SelectableChannel channel, Timeout keepAliveTimeout) { tm.addKeepAliveTimeout(channel, keepAliveTimeout); } public boolean hasKeepAliveTimeout(SelectableChannel channel) { return tm.hasKeepAliveTimeout(channel); } public void addTimeout(Timeout timeout) { tm.addTimeout(timeout); } /** * The callback will be invoked in the next iteration in the io loop. This is the only thread safe method that is * exposed by Deft. * This is a convenient way to return control to the io loop. */ public void addCallback(AsyncCallback callback) { cm.addCallback(callback); } // implements IOLoopMXBean @Override public int getNumberOfRegisteredIOHandlers() { return handlers.size(); } @Override public List<String> getRegisteredIOHandlers() { Map<SelectableChannel, IOHandler> defensive = new HashMap<SelectableChannel, IOHandler>(handlers); Collection<String> readables = transform(defensive.values(), new Function<IOHandler, String>() { @Override public String apply(IOHandler handler) { return handler.toString(); } }); return Lists.newLinkedList(readables); } }