package org.limewire.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketTimeoutException; import java.nio.ByteBuffer; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import junit.framework.Test; import org.limewire.nio.observer.StubAcceptChannelObserver; import org.limewire.nio.observer.StubConnectObserver; import org.limewire.nio.observer.StubReadConnectObserver; import org.limewire.nio.observer.StubReadObserver; import org.limewire.util.BaseTestCase; import org.limewire.util.OSUtils; import org.limewire.util.PrivilegedAccessor; import org.limewire.util.StringUtils; public class NIODispatcherTest extends BaseTestCase { private int LISTEN_PORT = 9999; private ServerSocket LISTEN_SOCKET; private InetSocketAddress LISTEN_ADDR; public NIODispatcherTest(String name) { super(name); } public static Test suite() { return buildTestSuite(NIODispatcherTest.class); } @Override public void setUp() throws Exception { LISTEN_SOCKET = new ServerSocket(); LISTEN_SOCKET.setReuseAddress(true); LISTEN_ADDR = new InetSocketAddress("127.0.0.1", LISTEN_PORT); LISTEN_SOCKET.bind(LISTEN_ADDR, 0); } @Override public void tearDown() throws Exception { try { LISTEN_SOCKET.close(); } catch(IOException ignored) {} } // note: this simulates a connect timeout by never actually trying to connect (except on linux). // it's really frickin' hard to try a connect and have it timeout because: // a) if you have a socket listening & try to connect, even if it isn't accepting, it will connect // b) if you don't have a socket listening and try to connect, it will immediate throw a ConnectException // c) on linux, if the socket isn't trying to connect to someone, it will throw a NoConnectPendingException public void testSimpleConnectTimeout() throws Exception { StubConnectObserver observer = new StubConnectObserver(); SocketChannel channel = observer.getChannel(); if(OSUtils.isLinux()) channel.connect(new InetSocketAddress("www.google.com", 9999)); NIODispatcher.instance().registerConnect(channel, observer, 3000); assertNull(observer.getIoException()); assertNull(observer.getSocket()); assertFalse(observer.isShutdown()); Thread.sleep(3500); assertFalse(channel.isConnected()); assertNull(observer.getSocket()); Exception iox = observer.getIoException(); assertNotNull(iox); assertInstanceof(SocketTimeoutException.class, iox); assertEquals("operation timed out (3000)", iox.getMessage()); // stringent, but useful. assertTrue(observer.isShutdown()); channel.close(); } public void testMultipleConnectTimeout() throws Exception { StubConnectObserver o1 = new StubConnectObserver(); SocketChannel c1 = o1.getChannel(); StubConnectObserver o2 = new StubConnectObserver(); SocketChannel c2 = o2.getChannel(); if(OSUtils.isLinux()) { c1.connect(new InetSocketAddress("www.google.com", 9999)); c2.connect(new InetSocketAddress("www.google.com", 9999)); } NIODispatcher.instance().registerConnect(c1, o1, 3000); NIODispatcher.instance().registerConnect(c2, o2, 2000); Exception x1; Exception x2; Thread.sleep(2500); x2 = o2.getIoException(); assertFalse(c2.isConnected()); assertNotNull(x2); assertInstanceof(SocketTimeoutException.class, x2); assertEquals("operation timed out (2000)", x2.getMessage()); // stringent, but useful. assertNull(o2.getSocket()); assertTrue(o2.isShutdown()); assertFalse(o1.isShutdown()); assertNull(o1.getIoException()); assertNull(o1.getSocket()); Thread.sleep(1000); x1 = o1.getIoException(); assertFalse(c1.isConnected()); assertNotNull(x1); assertInstanceof(SocketTimeoutException.class, x1); assertEquals("operation timed out (3000)", x1.getMessage()); // stringent, but useful. assertNull(o1.getSocket()); assertTrue(o1.isShutdown()); c1.close(); c2.close(); } public void testConnectPreventsTimeout() throws Exception { if(OSUtils.isLinux()) return; // it is impossible to run this test on linux. StubConnectObserver observer = new StubConnectObserver(); SocketChannel channel = observer.getChannel(); NIODispatcher.instance().registerConnect(channel, observer, 3000); channel.connect(LISTEN_ADDR); Thread.sleep(1000); assertTrue(channel.isConnected()); assertNull(observer.getIoException()); assertNotNull(observer.getSocket()); assertFalse(observer.isShutdown()); Thread.sleep(2500); assertTrue(channel.isConnected()); assertEquals(0, interestOps(channel)); Exception iox = observer.getIoException(); assertNull(iox); channel.close(); } public void testAcceptChannel() throws Exception { if(OSUtils.isLinux()) return; // it is impossible to run this test on linux. LISTEN_SOCKET.close(); ServerSocketChannel channel = ServerSocketChannel.open(); LISTEN_SOCKET = channel.socket(); channel.configureBlocking(false); LISTEN_SOCKET.setReuseAddress(true); LISTEN_SOCKET.bind(new InetSocketAddress(LISTEN_PORT)); StubAcceptChannelObserver observer = new StubAcceptChannelObserver(); NIODispatcher.instance().registerAccept(channel, observer); SocketChannel c1 = new StubConnectObserver().getChannel(); SocketChannel c2 = new StubConnectObserver().getChannel(); SocketChannel c3 = new StubConnectObserver().getChannel(); assertEquals(0, observer.getChannels().size()); NIODispatcher.instance().registerConnect(c1, new StubConnectObserver(), 0); c1.connect(LISTEN_ADDR); Thread.sleep(300); assertEquals(1, observer.getChannels().size()); SocketChannel r1 = observer.getNextSocketChannel(); assertEquals(c1.socket().getLocalPort(), r1.socket().getPort()); assertFalse(r1.isBlocking()); assertTrue(r1.isConnected()); c1.close(); // make sure we can handle more'n one connect at a time. NIODispatcher.instance().registerConnect(c2, new StubConnectObserver(), 0); NIODispatcher.instance().registerConnect(c3, new StubConnectObserver(), 0); c2.connect(LISTEN_ADDR); c3.connect(LISTEN_ADDR); Thread.sleep(300); assertEquals(2, observer.getChannels().size()); SocketChannel r2 = observer.getNextSocketChannel(); SocketChannel r3 = observer.getNextSocketChannel(); // swap r2 & r3 if we read them out of order. if(c2.socket().getLocalPort() != r2.socket().getPort()) { SocketChannel temp = r3; r3 = r2; r2 = temp; } assertEquals(c2.socket().getLocalPort(), r2.socket().getPort()); assertFalse(r2.isBlocking()); assertTrue(r2.isConnected()); assertEquals(c3.socket().getLocalPort(), r3.socket().getPort()); assertFalse(r3.isBlocking()); assertTrue(r3.isConnected()); c2.close(); c3.close(); } public void testLevelTriggeredness() throws Exception { StubReadConnectObserver o1 = new StubReadConnectObserver(); SocketChannel c1 = o1.getChannel(); connect(c1, LISTEN_ADDR, o1); Socket accepted = LISTEN_SOCKET.accept(); // First make sure we get notified of data & can read it. accepted.getOutputStream().write(StringUtils.toAsciiBytes("OUT")); accepted.getOutputStream().flush(); NIODispatcher.instance().registerRead(c1, o1); // Let the NIO thread pump a few times to make sure we read it. NIOTestUtils.waitForNIO(); NIOTestUtils.waitForNIO(); // Really this should be handled in 1 notify, but there's no harm // in the network sucking and it taking more. assertGreaterThan(0, o1.getReadsHandled()); assertLessThanOrEquals(3, o1.getReadsHandled()); ByteBuffer buffer = o1.getReadBuffer(); assertEquals(3, buffer.position()); assertEquals("OUT", StringUtils.getASCIIString(buffer.array(), 0, 3)); // Now that we've read everything, wait a for more cycle and make sure // no more events came in. int priorReadsHandled = o1.getReadsHandled(); NIOTestUtils.waitForNIO(); NIOTestUtils.waitForNIO(); assertEquals(priorReadsHandled, o1.getReadsHandled()); // Okay, our precondition is pretty well defined now... // Tell the reader to ignore reading from the actual buffer, // but keep interest on and see how many read notifications we get... o1.setIgnoreReadData(true); accepted.getOutputStream().write(StringUtils.toAsciiBytes("A")); accepted.getOutputStream().flush(); for(int i = 0; i < 10; i++) NIOTestUtils.waitForNIO(); // Wait a whole bunch of cycles... // We should be notified about this one pending byte of read data // many, many times. // (If the selector was edge-triggered, we would only be notified once). assertGreaterThan(priorReadsHandled + 5, o1.getReadsHandled()); // Just to do some stricter testing, also make sure that if we turn // interest off & back on again, we'll continue to get notifications // about the existing data... NIODispatcher.instance().interestRead(c1, false); NIOTestUtils.waitForNIO(); priorReadsHandled = o1.getReadsHandled(); for(int i = 0; i < 10; i++) NIOTestUtils.waitForNIO(); //Make sure we got no notifications while reading was off assertEquals(priorReadsHandled, o1.getReadsHandled()); // Now turn it back on and make sure notifications skyrocket again. NIODispatcher.instance().interestRead(c1, true); for(int i = 0; i < 10; i++) NIOTestUtils.waitForNIO(); assertGreaterThan(priorReadsHandled + 5, o1.getReadsHandled()); c1.close(); accepted.close(); } public void testLevelTriggerednessIfChannelHadSomeReadingDone() throws Exception { StubReadConnectObserver o1 = new StubReadConnectObserver(); SocketChannel c1 = o1.getChannel(); connect(c1, LISTEN_ADDR, o1); Socket accepted = LISTEN_SOCKET.accept(); // First make sure we get notified of data & can read it. accepted.getOutputStream().write(StringUtils.toAsciiBytes("OUT")); accepted.getOutputStream().flush(); NIODispatcher.instance().registerRead(c1, o1); // Let the NIO thread pump a few times to make sure we read it. NIOTestUtils.waitForNIO(); NIOTestUtils.waitForNIO(); // Really this should be handled in 1 notify, but there's no harm // in the network sucking and it taking more. assertGreaterThan(0, o1.getReadsHandled()); assertLessThanOrEquals(3, o1.getReadsHandled()); ByteBuffer buffer = o1.getReadBuffer(); assertEquals(3, buffer.position()); assertEquals("OUT", StringUtils.getASCIIString(buffer.array(), 0, 3)); // Now that we've read everything, wait a for more cycle and make sure // no more events came in. int priorReadsHandled = o1.getReadsHandled(); NIOTestUtils.waitForNIO(); NIOTestUtils.waitForNIO(); assertEquals(priorReadsHandled, o1.getReadsHandled()); // Okay, our precondition is pretty well defined now... // Tell the reader to ignore reading from the actual buffer, // but keep interest on and see how many read notifications we get... o1.getReadBuffer().clear(); o1.setAmountToRead(1); accepted.getOutputStream().write(StringUtils.toAsciiBytes("ABC")); accepted.getOutputStream().flush(); for(int i = 0; i < 10; i++) NIOTestUtils.waitForNIO(); // Wait a whole bunch of cycles... // We should be notified about this one pending byte of read data // many, many times. // (If the selector was edge-triggered, we would only be notified once). assertGreaterThan(priorReadsHandled + 5, o1.getReadsHandled()); // Make sure that when we consumed data, it was on the first notification assertEquals(priorReadsHandled + 1, o1.getReadsHandledAtLastConsume()); assertEquals(1, o1.getReadBuffer().position()); assertEquals("A", StringUtils.getASCIIString(o1.getReadBuffer().array(), 0, 1)); // Just to do some stricter testing, also make sure that if we turn // interest off & back on again, we'll continue to get notifications // about the existing data... NIODispatcher.instance().interestRead(c1, false); NIOTestUtils.waitForNIO(); priorReadsHandled = o1.getReadsHandled(); for(int i = 0; i < 10; i++) NIOTestUtils.waitForNIO(); //Make sure we got no notifications while reading was off assertEquals(priorReadsHandled, o1.getReadsHandled()); // Now turn it back on and make sure notifications skyrocket again. o1.getReadBuffer().clear(); NIODispatcher.instance().interestRead(c1, true); for(int i = 0; i < 10; i++) NIOTestUtils.waitForNIO(); assertGreaterThan(priorReadsHandled + 5, o1.getReadsHandled()); assertEquals(priorReadsHandled + 1, o1.getReadsHandledAtLastConsume()); assertEquals(1, o1.getReadBuffer().position()); assertEquals("B", StringUtils.getASCIIString(o1.getReadBuffer().array(), 0, 1)); c1.close(); accepted.close(); } public void testSimpleReadTimeout() throws Exception { StubReadObserver o1 = new StubReadObserver(); SocketChannel c1 = o1.getChannel(); c1.connect(LISTEN_ADDR); o1.setReadTimeout(1023); NIODispatcher.instance().registerRead(c1, o1); o1.waitForIOException(2000); assertInstanceof(SocketTimeoutException.class, o1.getIox()); assertEquals("operation timed out (1023)", o1.getIox().getMessage()); assertTrue(o1.isShutdown()); assertEquals(0, o1.getReadsHandled()); c1.close(); } public void testNoTimeoutIfInterestOff() throws Exception { StubReadObserver o1 = new StubReadObserver(); SocketChannel c1 = o1.getChannel(); c1.connect(LISTEN_ADDR); o1.setReadTimeout(1000); NIODispatcher.instance().registerRead(c1, o1); Thread.sleep(200); // let it register. NIODispatcher.instance().interestRead(c1, false); o1.waitForIOException(2000); assertNull(o1.getIox()); assertFalse(o1.isShutdown()); assertEquals(0, o1.getReadsHandled()); c1.close(); } public void testChangingTimeoutByInterest() throws Exception { StubReadObserver o1 = new StubReadObserver(); SocketChannel c1 = o1.getChannel(); c1.connect(LISTEN_ADDR); o1.setReadTimeout(1000); NIODispatcher.instance().registerRead(c1, o1); Thread.sleep(200); // let it register. o1.setReadTimeout(2005); NIODispatcher.instance().interestRead(c1, true); o1.waitForIOException(4000); assertInstanceof(SocketTimeoutException.class, o1.getIox()); assertEquals("operation timed out (2005)", o1.getIox().getMessage()); assertTrue(o1.isShutdown()); assertEquals(0, o1.getReadsHandled()); c1.close(); } public void testTimeoutUpsAfterReads() throws Exception { StubReadConnectObserver o1 = new StubReadConnectObserver(); SocketChannel c1 = o1.getChannel(); if(OSUtils.isLinux()) { connect(c1, LISTEN_ADDR, o1); // can't do a portion of the test on linux. } else { o1.setReadTimeout(1000); NIODispatcher.instance().registerConnect(c1, o1, 1000); c1.connect(LISTEN_ADDR); o1.waitForEvent(1000); assertEquals(c1.socket(), o1.getSocket()); assertTrue(c1.isConnected()); } // Now accept on the server socket. LISTEN_SOCKET.setSoTimeout(5000); Socket accepted = LISTEN_SOCKET.accept(); // Wait 2 seconds & make sure it didn't disco Thread.sleep(2000); assertTrue(c1.isConnected()); assertFalse(o1.isShutdown()); assertNull(o1.getIoException()); assertEquals(0, o1.getReadsHandled()); // Turn interest on in reading & send some data. NIODispatcher.instance().interestRead(c1, true); o1.setReadTimeout(2000); // change timeout length so we can make sure the exception matched accepted.getOutputStream().write(new byte[100]); Thread.sleep(500); assertGreaterThanOrEquals(1, o1.getReadsHandled()); // Now make sure that since interest is still on, it'll IOX after a second. Thread.sleep(2500); assertInstanceof(SocketTimeoutException.class, o1.getIoException()); assertEquals("operation timed out (2000)", o1.getIoException().getMessage()); assertGreaterThanOrEquals(o1.getLastReadTime() + 1000, o1.getIoxTime()); c1.close(); } public void testTimeoutBecomesSmaller() throws Exception { StubReadConnectObserver o1 = new StubReadConnectObserver(); SocketChannel c1 = o1.getChannel(); if(OSUtils.isLinux()) connect(c1, LISTEN_ADDR, o1); // can't do a portion of the test on linux. else { NIODispatcher.instance().registerConnect(c1, o1, 1000); c1.connect(LISTEN_ADDR); o1.waitForEvent(1000); assertEquals(c1.socket(), o1.getSocket()); assertTrue(c1.isConnected()); } // Now accept on the server socket. LISTEN_SOCKET.setSoTimeout(5000); Socket accepted = LISTEN_SOCKET.accept(); // Wait 2 seconds & make sure it didn't disco Thread.sleep(2000); assertTrue(c1.isConnected()); assertFalse(o1.isShutdown()); assertNull(o1.getIoException()); assertEquals(0, o1.getReadsHandled()); // Turn interest on in reading & send some data. o1.setReadTimeout(5000); NIODispatcher.instance().interestRead(c1, true); o1.setReadTimeout(1000); // change timeout length so we can make sure the exception matched accepted.getOutputStream().write(new byte[100]); Thread.sleep(500); assertGreaterThanOrEquals(1, o1.getReadsHandled()); // Now make sure that since interest is still on, it'll IOX after a second. Thread.sleep(1500); assertInstanceof(SocketTimeoutException.class, o1.getIoException()); assertEquals("operation timed out (1000)", o1.getIoException().getMessage()); // Wait a bit longer and make sure the msg didn't change on the IOX Thread.sleep(5500); assertEquals("operation timed out (1000)", o1.getIoException().getMessage()); c1.close(); } public void testOtherSelectors() throws Exception { Selector stub = new StubSelector(); NIODispatcher.instance().registerSelector(stub, StubChannel.class); try { StubReadConnectObserver observer = new StubReadConnectObserver(); StubChannel channel = new StubChannel(); observer.setChannel(channel); Socket socket = new Socket(); channel.setSocket(socket); NIODispatcher.instance().registerConnect(channel, observer, 0); NIODispatcher.instance().wakeup(); Thread.sleep(300); assertEquals(SelectionKey.OP_CONNECT, interestOps(channel)); channel.setReadyOps(SelectionKey.OP_CONNECT); NIODispatcher.instance().wakeup(); observer.waitForEvent(1000); assertSame(socket, channel.socket()); assertEquals(0, interestOps(channel)); assertEquals(0, observer.getReadsHandled()); NIODispatcher.instance().interestRead(channel, true); assertEquals(SelectionKey.OP_READ, interestOps(channel)); channel.setReadyOps(SelectionKey.OP_READ); NIODispatcher.instance().wakeup(); observer.waitForEvent(2000); assertGreaterThanOrEquals(1, observer.getReadsHandled()); channel.close(); } finally { NIODispatcher.instance().removeSelector(stub); } } /** * tests that scheduling tasks works and that they are executed on * the dispatcher thread. */ public void testSchedulingTasks() throws Exception { // first get a ref to the dispatch thread final AtomicReference<Thread>t = new AtomicReference<Thread>(); NIODispatcher.instance().getScheduledExecutorService().execute(new Runnable() { public void run() { t.set(Thread.currentThread()); } }); while(t.get() == null) Thread.sleep(10); final CountDownLatch executed = new CountDownLatch(1); Future<?> f = NIODispatcher.instance().getScheduledExecutorService().schedule(new Runnable() { public void run() { // should be executed on the dispatch thread assertSame(t.get(), Thread.currentThread()); executed.countDown(); } }, 500, TimeUnit.MILLISECONDS); // and not get executed until the timeout elapses assertFalse(executed.await(480,TimeUnit.MILLISECONDS)); assertFalse(f.isDone()); assertTrue(executed.await(40,TimeUnit.MILLISECONDS)); assertTrue(f.isDone()); } /** Tests that re-registration discards old attachments. */ public void testReRegisterDiscardsOldAttachment() throws Exception { StubReadObserver o1 = new StubReadObserver(); SocketChannel c1 = o1.getChannel(); c1.connect(LISTEN_ADDR); o1.setReadTimeout(1023); NIODispatcher.instance().registerRead(c1, o1); StubReadObserver o2 = new StubReadObserver(); o2.setReadTimeout(1001); NIODispatcher.instance().registerRead(c1, o2); o2.waitForIOException(2000); assertInstanceof(SocketTimeoutException.class, o2.getIox()); assertEquals("operation timed out (1001)", o2.getIox().getMessage()); assertTrue(o2.isShutdown()); assertEquals(0, o2.getReadsHandled()); assertNull(o1.getIox()); assertFalse(o1.isShutdown()); assertEquals(0, o1.getReadsHandled()); c1.close(); } private void connect(SocketChannel c, SocketAddress a, StubReadConnectObserver o) throws Exception { if(!c.connect(a)) { NIODispatcher.instance().registerConnect(c, o, 1000); o.waitForEvent(1000); assertEquals(c.socket(), o.getSocket()); assertTrue(c.isConnected()); } } private int interestOps(final SelectableChannel channel) throws Exception { Future<Integer> f = NIODispatcher.instance().getScheduledExecutorService().submit(new Callable<Integer>() { public Integer call() throws Exception { Selector selector = (Selector)PrivilegedAccessor.invokeMethod( NIODispatcher.instance(), "getSelectorFor", new Object[] {channel }, new Class[] { SelectableChannel.class }); return channel.keyFor(selector).interestOps(); } }); return f.get(); } }