/* * Copyright (c) 2008-2017, Hazelcast, Inc. 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.hazelcast.internal.networking.nio; import com.hazelcast.internal.networking.IOOutOfMemoryHandler; import com.hazelcast.logging.ILogger; import com.hazelcast.logging.Logger; import com.hazelcast.test.AssertTask; import com.hazelcast.test.ExpectedRuntimeException; import com.hazelcast.test.HazelcastTestSupport; import org.junit.After; import org.junit.Before; import org.junit.Test; import java.io.IOException; import java.nio.channels.CancelledKeyException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.spi.SelectorProvider; import java.util.HashSet; import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.mockito.Matchers.any; import static org.mockito.Matchers.isA; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * Tests the NioThread. A lot of the internal are tested using a mock-selector. This gives us full * control on testing the edge cases which are extremely hard to realize with a real selector. */ public abstract class NioThreadAbstractTest extends HazelcastTestSupport { private IOOutOfMemoryHandler oomeHandler; private ILogger logger; private MockSelector selector; private SelectionHandler handler; NioThread thread; @Before public void setup() { logger = Logger.getLogger(NioThread.class); oomeHandler = mock(IOOutOfMemoryHandler.class); selector = new MockSelector(); handler = mock(SelectionHandler.class); } @After public void tearDown() { if (thread != null) { thread.shutdown(); } } protected abstract SelectorMode selectorMode(); /** * Subclasses that need to do some setup after the IO thread was created but * before starting it should override this method. */ protected void beforeStartThread() { } private void startThread() { thread = new NioThread("foo", logger, oomeHandler, selectorMode(), selector, null); beforeStartThread(); thread.start(); } @Test public void whenValidSelectionKey_thenHandlerCalled() throws Exception { startThread(); SelectionKey selectionKey = mock(SelectionKey.class); selectionKey.attach(handler); when(selectionKey.isValid()).thenReturn(true); selector.scheduleSelectAction(selectionKey); assertTrueEventually(new AssertTask() { @Override public void run() throws Exception { verify(handler).handle(); } }); assertEquals(1, thread.getEventCount()); assertStillRunning(); } @Test public void whenInvalidSelectionKey_thenHandlerOnFailureCalledWithCancelledKeyException() throws Exception { startThread(); SelectionKey selectionKey = mock(SelectionKey.class); selectionKey.attach(handler); when(selectionKey.isValid()).thenReturn(false); selector.scheduleSelectAction(selectionKey); assertTrueEventually(new AssertTask() { @Override public void run() throws Exception { verify(handler).onFailure(isA(CancelledKeyException.class)); } }); assertStillRunning(); } @Test public void whenHandlerThrowException_thenHandlerOnFailureCalledWithThatException() throws Exception { startThread(); SelectionKey selectionKey = mock(SelectionKey.class); selectionKey.attach(handler); when(selectionKey.isValid()).thenReturn(true); doThrow(new ExpectedRuntimeException()).when(handler).handle(); selector.scheduleSelectAction(selectionKey); assertTrueEventually(new AssertTask() { @Override public void run() throws Exception { verify(handler).onFailure(isA(ExpectedRuntimeException.class)); } }); assertStillRunning(); } @Test public void whenSelectThrowsIOException_thenKeepRunning() { startThread(); selector.scheduleSelectThrowsIOException(); assertStillRunning(); } @Test public void whenSelectThrowsOOME_thenThreadTerminates() { startThread(); selector.scheduleSelectThrowsOOME(); assertTrueEventually(new AssertTask() { @Override public void run() throws Exception { assertFalse(thread.isAlive()); } }); verify(oomeHandler).handle(any(OutOfMemoryError.class)); } @Test public void testToString() { startThread(); assertEquals(thread.getName(), thread.toString()); } public void assertStillRunning() { // we verify that the thread is still running by scheduling a selection-key event and checking if the // handler is being called. final SelectionHandler handler = mock(SelectionHandler.class); SelectionKey selectionKey = mock(SelectionKey.class); selectionKey.attach(handler); when(selectionKey.isValid()).thenReturn(true); selector.scheduleSelectAction(selectionKey); assertTrueEventually(new AssertTask() { @Override public void run() throws Exception { verify(handler).handle(); } }); } class MockSelector extends Selector { final BlockingQueue<SelectorAction> actionQueue = new LinkedBlockingQueue<SelectorAction>(); Set<SelectionKey> pendingKeys; void scheduleSelectAction(SelectionKey selectionKey) { SelectorAction selectorAction = new SelectorAction(); selectorAction.keys.add(selectionKey); actionQueue.add(selectorAction); } void scheduleSelectThrowsIOException() { SelectorAction selectorAction = new SelectorAction(); selectorAction.selectThrowsIOException = true; actionQueue.add(selectorAction); } void scheduleSelectThrowsOOME() { SelectorAction selectorAction = new SelectorAction(); selectorAction.selectThrowsOOME = true; actionQueue.add(selectorAction); } @Override public boolean isOpen() { return true; } @Override public SelectorProvider provider() { throw new UnsupportedOperationException(); } @Override public Set<SelectionKey> keys() { throw new UnsupportedOperationException(); } @Override public Set<SelectionKey> selectedKeys() { if (pendingKeys == null) { throw new IllegalArgumentException(); } return pendingKeys; } @Override public int selectNow() throws IOException { return select(0); } @Override public int select(long timeout) throws IOException { try { SelectorAction action = actionQueue.poll(timeout, TimeUnit.MILLISECONDS); if (action == null) { // there was a timeout. return 0; } if (action.selectThrowsIOException) { throw new IOException(); } if (action.selectThrowsOOME) { throw new OutOfMemoryError(); } pendingKeys = action.keys; return pendingKeys.size(); } catch (InterruptedException e) { // should not happen, so lets propagate it. throw new RuntimeException(e); } } @Override public int select() throws IOException { // not needed for the time being. throw new UnsupportedOperationException(); } @Override public Selector wakeup() { actionQueue.add(new SelectorAction()); return this; } @Override public void close() throws IOException { } } static class SelectorAction { final Set<SelectionKey> keys = new HashSet<SelectionKey>(); boolean selectThrowsIOException = false; boolean selectThrowsOOME = false; } }