/*
* Copyright 2012 The Netty Project
*
* The Netty Project licenses this file to you 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 io.netty.channel;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import io.netty.channel.local.LocalChannel;
import io.netty.util.concurrent.EventExecutor;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
public class SingleThreadEventLoopTest {
private static final Runnable NOOP = new Runnable() {
@Override
public void run() { }
};
private SingleThreadEventLoopA loopA;
private SingleThreadEventLoopB loopB;
@Before
public void newEventLoop() {
loopA = new SingleThreadEventLoopA();
loopB = new SingleThreadEventLoopB();
}
@After
public void stopEventLoop() {
if (!loopA.isShuttingDown()) {
loopA.shutdownGracefully(0, 0, TimeUnit.MILLISECONDS);
}
if (!loopB.isShuttingDown()) {
loopB.shutdownGracefully(0, 0, TimeUnit.MILLISECONDS);
}
while (!loopA.isTerminated()) {
try {
loopA.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
// Ignore
}
}
assertEquals(1, loopA.cleanedUp.get());
while (!loopB.isTerminated()) {
try {
loopB.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
// Ignore
}
}
}
@Test
@SuppressWarnings("deprecation")
public void shutdownBeforeStart() throws Exception {
loopA.shutdown();
assertRejection(loopA);
}
@Test
@SuppressWarnings("deprecation")
public void shutdownAfterStart() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
loopA.execute(new Runnable() {
@Override
public void run() {
latch.countDown();
}
});
// Wait for the event loop thread to start.
latch.await();
// Request the event loop thread to stop.
loopA.shutdown();
assertRejection(loopA);
assertTrue(loopA.isShutdown());
// Wait until the event loop is terminated.
while (!loopA.isTerminated()) {
loopA.awaitTermination(1, TimeUnit.DAYS);
}
}
private static void assertRejection(EventExecutor loop) {
try {
loop.execute(NOOP);
fail("A task must be rejected after shutdown() is called.");
} catch (RejectedExecutionException e) {
// Expected
}
}
@Test
public void scheduleTaskA() throws Exception {
testScheduleTask(loopA);
}
@Test
public void scheduleTaskB() throws Exception {
testScheduleTask(loopB);
}
private static void testScheduleTask(EventLoop loopA) throws InterruptedException, ExecutionException {
long startTime = System.nanoTime();
final AtomicLong endTime = new AtomicLong();
loopA.schedule(new Runnable() {
@Override
public void run() {
endTime.set(System.nanoTime());
}
}, 500, TimeUnit.MILLISECONDS).get();
assertTrue(endTime.get() - startTime >= TimeUnit.MILLISECONDS.toNanos(500));
}
@Test
public void scheduleTaskAtFixedRateA() throws Exception {
testScheduleTaskAtFixedRate(loopA);
}
@Test
public void scheduleTaskAtFixedRateB() throws Exception {
testScheduleTaskAtFixedRate(loopB);
}
private static void testScheduleTaskAtFixedRate(EventLoop loopA) throws InterruptedException {
final Queue<Long> timestamps = new LinkedBlockingQueue<Long>();
ScheduledFuture<?> f = loopA.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
timestamps.add(System.nanoTime());
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// Ignore
}
}
}, 100, 100, TimeUnit.MILLISECONDS);
Thread.sleep(550);
assertTrue(f.cancel(true));
assertEquals(5, timestamps.size());
// Check if the task was run without a lag.
Long previousTimestamp = null;
for (Long t: timestamps) {
if (previousTimestamp == null) {
previousTimestamp = t;
continue;
}
assertTrue(t.longValue() - previousTimestamp.longValue() >= TimeUnit.MILLISECONDS.toNanos(90));
previousTimestamp = t;
}
}
@Test
public void scheduleLaggyTaskAtFixedRateA() throws Exception {
testScheduleLaggyTaskAtFixedRate(loopA);
}
@Test
public void scheduleLaggyTaskAtFixedRateB() throws Exception {
testScheduleLaggyTaskAtFixedRate(loopB);
}
private static void testScheduleLaggyTaskAtFixedRate(EventLoop loopA) throws InterruptedException {
final Queue<Long> timestamps = new LinkedBlockingQueue<Long>();
ScheduledFuture<?> f = loopA.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
boolean empty = timestamps.isEmpty();
timestamps.add(System.nanoTime());
if (empty) {
try {
Thread.sleep(401);
} catch (InterruptedException e) {
// Ignore
}
}
}
}, 100, 100, TimeUnit.MILLISECONDS);
Thread.sleep(550);
assertTrue(f.cancel(true));
assertEquals(5, timestamps.size());
// Check if the task was run with lag.
int i = 0;
Long previousTimestamp = null;
for (Long t: timestamps) {
if (previousTimestamp == null) {
previousTimestamp = t;
continue;
}
long diff = t.longValue() - previousTimestamp.longValue();
if (i == 0) {
assertTrue(diff >= TimeUnit.MILLISECONDS.toNanos(400));
} else {
assertTrue(diff <= TimeUnit.MILLISECONDS.toNanos(10));
}
previousTimestamp = t;
i ++;
}
}
@Test
public void scheduleTaskWithFixedDelayA() throws Exception {
testScheduleTaskWithFixedDelay(loopA);
}
@Test
public void scheduleTaskWithFixedDelayB() throws Exception {
testScheduleTaskWithFixedDelay(loopB);
}
private static void testScheduleTaskWithFixedDelay(EventLoop loopA) throws InterruptedException {
final Queue<Long> timestamps = new LinkedBlockingQueue<Long>();
ScheduledFuture<?> f = loopA.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
timestamps.add(System.nanoTime());
try {
Thread.sleep(51);
} catch (InterruptedException e) {
// Ignore
}
}
}, 100, 100, TimeUnit.MILLISECONDS);
Thread.sleep(500);
assertTrue(f.cancel(true));
assertEquals(3, timestamps.size());
// Check if the task was run without a lag.
Long previousTimestamp = null;
for (Long t: timestamps) {
if (previousTimestamp == null) {
previousTimestamp = t;
continue;
}
assertTrue(t.longValue() - previousTimestamp.longValue() >= TimeUnit.MILLISECONDS.toNanos(150));
previousTimestamp = t;
}
}
@Test
@SuppressWarnings("deprecation")
public void shutdownWithPendingTasks() throws Exception {
final int NUM_TASKS = 3;
final AtomicInteger ranTasks = new AtomicInteger();
final CountDownLatch latch = new CountDownLatch(1);
final Runnable task = new Runnable() {
@Override
public void run() {
ranTasks.incrementAndGet();
while (latch.getCount() > 0) {
try {
latch.await();
} catch (InterruptedException e) {
// Ignored
}
}
}
};
for (int i = 0; i < NUM_TASKS; i ++) {
loopA.execute(task);
}
// At this point, the first task should be running and stuck at latch.await().
while (ranTasks.get() == 0) {
Thread.yield();
}
assertEquals(1, ranTasks.get());
// Shut down the event loop to test if the other tasks are run before termination.
loopA.shutdown();
// Let the other tasks run.
latch.countDown();
// Wait until the event loop is terminated.
while (!loopA.isTerminated()) {
loopA.awaitTermination(1, TimeUnit.DAYS);
}
// Make sure loop.shutdown() above triggered wakeup().
assertEquals(NUM_TASKS, ranTasks.get());
}
@Test(timeout = 10000)
@SuppressWarnings("deprecation")
public void testRegistrationAfterShutdown() throws Exception {
loopA.shutdown();
// Disable logging temporarily.
Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
List<Appender<ILoggingEvent>> appenders = new ArrayList<Appender<ILoggingEvent>>();
for (Iterator<Appender<ILoggingEvent>> i = root.iteratorForAppenders(); i.hasNext();) {
Appender<ILoggingEvent> a = i.next();
appenders.add(a);
root.detachAppender(a);
}
try {
ChannelFuture f = loopA.register(new LocalChannel());
f.awaitUninterruptibly();
assertFalse(f.isSuccess());
assertThat(f.cause(), is(instanceOf(RejectedExecutionException.class)));
assertFalse(f.channel().isOpen());
} finally {
for (Appender<ILoggingEvent> a: appenders) {
root.addAppender(a);
}
}
}
@Test(timeout = 10000)
@SuppressWarnings("deprecation")
public void testRegistrationAfterShutdown2() throws Exception {
loopA.shutdown();
final CountDownLatch latch = new CountDownLatch(1);
Channel ch = new LocalChannel();
ChannelPromise promise = ch.newPromise();
promise.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
latch.countDown();
}
});
// Disable logging temporarily.
Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
List<Appender<ILoggingEvent>> appenders = new ArrayList<Appender<ILoggingEvent>>();
for (Iterator<Appender<ILoggingEvent>> i = root.iteratorForAppenders(); i.hasNext();) {
Appender<ILoggingEvent> a = i.next();
appenders.add(a);
root.detachAppender(a);
}
try {
ChannelFuture f = loopA.register(ch, promise);
f.awaitUninterruptibly();
assertFalse(f.isSuccess());
assertThat(f.cause(), is(instanceOf(RejectedExecutionException.class)));
// Ensure the listener was notified.
assertFalse(latch.await(1, TimeUnit.SECONDS));
assertFalse(ch.isOpen());
} finally {
for (Appender<ILoggingEvent> a: appenders) {
root.addAppender(a);
}
}
}
@Test(timeout = 5000)
public void testGracefulShutdownQuietPeriod() throws Exception {
loopA.shutdownGracefully(1, Integer.MAX_VALUE, TimeUnit.SECONDS);
// Keep Scheduling tasks for another 2 seconds.
for (int i = 0; i < 20; i ++) {
Thread.sleep(100);
loopA.execute(NOOP);
}
long startTime = System.nanoTime();
assertThat(loopA.isShuttingDown(), is(true));
assertThat(loopA.isShutdown(), is(false));
while (!loopA.isTerminated()) {
loopA.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
}
assertTrue(System.nanoTime() - startTime >= TimeUnit.SECONDS.toNanos(1));
}
@Test(timeout = 5000)
public void testGracefulShutdownTimeout() throws Exception {
loopA.shutdownGracefully(2, 2, TimeUnit.SECONDS);
// Keep Scheduling tasks for another 3 seconds.
// Submitted tasks must be rejected after 2 second timeout.
for (int i = 0; i < 10; i ++) {
Thread.sleep(100);
loopA.execute(NOOP);
}
try {
for (int i = 0; i < 20; i ++) {
Thread.sleep(100);
loopA.execute(NOOP);
}
fail("shutdownGracefully() must reject a task after timeout.");
} catch (RejectedExecutionException e) {
// Expected
}
assertThat(loopA.isShuttingDown(), is(true));
assertThat(loopA.isShutdown(), is(true));
}
private static class SingleThreadEventLoopA extends SingleThreadEventLoop {
final AtomicInteger cleanedUp = new AtomicInteger();
SingleThreadEventLoopA() {
super(null, Executors.defaultThreadFactory(), true);
}
@Override
protected void run() {
for (;;) {
Runnable task = takeTask();
if (task != null) {
task.run();
updateLastExecutionTime();
}
if (confirmShutdown()) {
break;
}
}
}
@Override
protected void cleanup() {
cleanedUp.incrementAndGet();
}
}
private static class SingleThreadEventLoopB extends SingleThreadEventLoop {
SingleThreadEventLoopB() {
super(null, Executors.defaultThreadFactory(), false);
}
@Override
protected void run() {
for (;;) {
try {
Thread.sleep(TimeUnit.NANOSECONDS.toMillis(delayNanos(System.nanoTime())));
} catch (InterruptedException e) {
// Waken up by interruptThread()
}
runAllTasks();
if (confirmShutdown()) {
break;
}
}
}
@Override
protected void wakeup(boolean inEventLoop) {
interruptThread();
}
}
}