// Copyright 2016 Twitter. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.twitter.heron.common.basics; 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.time.Duration; import java.util.Iterator; import java.util.Set; /** * A NIOLooper, implementing WakeableLooper, is a class wrapping a Java NIO selector to dispatch events. * It extends WakeableLooper, so it will execute in a while loop unless the exitLoop() is called. * And in every execution, in tasksOnWakeup(), it will handle the selected keys. * The NIOLooper should start by calling {@code loop()} */ public class NIOLooper extends WakeableLooper { private final Selector selector; public NIOLooper() throws IOException { selector = Selector.open(); addNIOLooperTasks(); } private void addNIOLooperTasks() { Runnable task = new Runnable() { @Override public void run() { handleSelectedKeys(); } }; addTasksOnWakeup(task); } @Override public void doWait() { // If timer task exists, the doWait() should wait not later than the time timer to execute // It no timer exists, we consider it will wait forever until other threads call wakeUp() Duration nextTimeoutInterval = getNextTimeoutInterval(); // doWait(timeout), which in fact is implemented by selector.select(timeout), and it will // wake up, if other threads wake it up, it meets the timeout, one channel is selected, or // the current thread is interrupted. try { if (nextTimeoutInterval.toMillis() > 0) { // The select will take the timeout in unit of milli-seconds selector.select(nextTimeoutInterval.toMillis()); } else { selector.selectNow(); } } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); } } @Override public void wakeUp() { selector.wakeup(); } public void removeAllInterest(SelectableChannel channel) { SelectionKey key = channel.keyFor(selector); if (key != null) { key.cancel(); } } // Handle the selected keys private void handleSelectedKeys() { Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); keyIterator.remove(); ISelectHandler callback = (ISelectHandler) key.attachment(); if (!key.isValid()) { // This method key.channel() will continue to return the channel even after the // key is cancelled. callback.handleError(key.channel()); continue; } // We need to check whether the key is still valid since: // 1. The key could be cancelled by last operation // 2. The process might not fail-fast or throw exceptions after the key is cancelled if (key.isValid() && key.isWritable()) { callback.handleWrite(key.channel()); } if (key.isValid() && key.isReadable()) { callback.handleRead(key.channel()); } if (key.isValid() && key.isConnectable()) { callback.handleConnect(key.channel()); } if (key.isValid() && key.isAcceptable()) { callback.handleAccept(key.channel()); } } } public boolean isChannelValid(SelectableChannel channel) { SelectionKey key = channel.keyFor(selector); return key != null && key.isValid(); } /** * Followings are the register, unregister, isRegister for different operations for the selector and channel */ public void registerRead(SelectableChannel channel, ISelectHandler callback) throws ClosedChannelException { assert channel.keyFor(selector) == null || (channel.keyFor(selector).interestOps() & SelectionKey.OP_CONNECT) == 0; addInterest(channel, SelectionKey.OP_READ, callback); } public void unregisterRead(SelectableChannel channel) { removeInterest(channel, SelectionKey.OP_READ); } public boolean isReadRegistered(SelectableChannel channel) { return isInterestRegistered(channel, SelectionKey.OP_READ); } public void registerConnect(SelectableChannel channel, ISelectHandler callback) throws ClosedChannelException { // This channel should be first use assert channel.keyFor(selector) == null; addInterest(channel, SelectionKey.OP_CONNECT, callback); } public void unregisterConnect(SelectableChannel channel) { removeInterest(channel, SelectionKey.OP_CONNECT); } public boolean isConnectRegistered(SelectableChannel channel) { return isInterestRegistered(channel, SelectionKey.OP_CONNECT); } public void registerAccept(SelectableChannel channel, ISelectHandler callback) throws ClosedChannelException { addInterest(channel, SelectionKey.OP_ACCEPT, callback); } public void unregisterAccept(SelectableChannel channel) { removeInterest(channel, SelectionKey.OP_ACCEPT); } public boolean isAcceptRegistered(SelectableChannel channel) { return isInterestRegistered(channel, SelectionKey.OP_ACCEPT); } public void registerWrite(SelectableChannel channel, ISelectHandler callback) throws ClosedChannelException { addInterest(channel, SelectionKey.OP_WRITE, callback); } public void unregisterWrite(SelectableChannel channel) { removeInterest(channel, SelectionKey.OP_WRITE); } public boolean isWriteRegistered(SelectableChannel channel) { return isInterestRegistered(channel, SelectionKey.OP_WRITE); } /** * Register an operation interest on a SelectableChannel, with ISelectHandler as callback attachment * There are two cases when trying to register an interest * 1. The whole key does not exist; no interests ever registered for this channel * 2. The key exists due to other interests registered but not the one we are adding * <p> * In 1st case, we just register this channel with operation on the given Selector * In 2nd case, we have to make sure the state of NIOLooper is clean: * 1. Key has to be valid * 2. The interest has not yet been registered * 3. If old attached ISelectHandler exists, it has to be the same as new one * If any one of above 3 conditions are not met, RuntimeException would be thrown. * * @param channel The Selectable to register operation interest * @param operation The operation interest to register * @param callback The Callback to handle * @throws ClosedChannelException if Channel is closed when trying to register an interest */ private void addInterest(SelectableChannel channel, int operation, ISelectHandler callback) throws ClosedChannelException { SelectionKey key = channel.keyFor(selector); if (key == null) { channel.register(selector, operation, callback); } else if (!key.isValid()) { throw new RuntimeException( String.format("Unable to add %d in %s due to key is invalid", operation, channel)); } else { // Key is not null and key is valid if ((key.interestOps() & operation) != 0) { throw new RuntimeException( String.format("%d has been registered in %s", operation, channel)); } if (key.attachment() == null) { key.attach(callback); } else { if (callback != key.attachment()) { throw new RuntimeException("Unmatched SelectHandler has already been attached" + " for other operation"); } // If call == key.attachment // Just skip } key.interestOps(key.interestOps() | operation); } } /** * Remove one operation interest on a SelectableChannel. * The SelectableChannel has to be registered with Selector ahead. * Otherwise, NullPointerExceptions would throw * The key for SelectableChannel has to be valid. * Otherwise, InvalidValid Exception would throw. * * @param channel the SelectableChannel to remove operation interest * @param operation the interest to remove */ private void removeInterest(SelectableChannel channel, int operation) { SelectionKey key = channel.keyFor(selector); // Exception would be thrown if key is null or key is inValid // We do not need double check it ahead key.interestOps(key.interestOps() & (~operation)); } /** * Check whether an operation interest was registered on a SelectableChannel * There are two cases that interest is not registered * 1. The whole key does not exist; no interests ever registered for this channel * 2. The key exists due to other interests registered but not the one we are adding * If the key exists, the key for SelectableChannel has to be valid. * Otherwise, InvalidValid Exception would throw. * * @param channel The Selectable to check * @param operation The operation interest to check */ private boolean isInterestRegistered(SelectableChannel channel, int operation) { SelectionKey key = channel.keyFor(selector); return key != null && (key.interestOps() & operation) != 0; } }