/* * Copyright 2016 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.handler.timeout; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelOutboundBuffer; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.util.ReferenceCountUtil; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.junit.Test; public class IdleStateHandlerTest { @Test public void testReaderIdle() throws Exception { TestableIdleStateHandler idleStateHandler = new TestableIdleStateHandler( false, 1L, 0L, 0L, TimeUnit.SECONDS); // We start with one FIRST_READER_IDLE_STATE_EVENT, followed by an infinite number of READER_IDLE_STATE_EVENTs anyIdle(idleStateHandler, IdleStateEvent.FIRST_READER_IDLE_STATE_EVENT, IdleStateEvent.READER_IDLE_STATE_EVENT, IdleStateEvent.READER_IDLE_STATE_EVENT); } @Test public void testWriterIdle() throws Exception { TestableIdleStateHandler idleStateHandler = new TestableIdleStateHandler( false, 0L, 1L, 0L, TimeUnit.SECONDS); anyIdle(idleStateHandler, IdleStateEvent.FIRST_WRITER_IDLE_STATE_EVENT, IdleStateEvent.WRITER_IDLE_STATE_EVENT, IdleStateEvent.WRITER_IDLE_STATE_EVENT); } @Test public void testAllIdle() throws Exception { TestableIdleStateHandler idleStateHandler = new TestableIdleStateHandler( false, 0L, 0L, 1L, TimeUnit.SECONDS); anyIdle(idleStateHandler, IdleStateEvent.FIRST_ALL_IDLE_STATE_EVENT, IdleStateEvent.ALL_IDLE_STATE_EVENT, IdleStateEvent.ALL_IDLE_STATE_EVENT); } private void anyIdle(TestableIdleStateHandler idleStateHandler, Object... expected) throws Exception { assertTrue("The number of expected events must be >= 1", expected.length >= 1); final List<Object> events = new ArrayList<Object>(); ChannelInboundHandlerAdapter handler = new ChannelInboundHandlerAdapter() { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { events.add(evt); } }; EmbeddedChannel channel = new EmbeddedChannel(idleStateHandler, handler); try { // For each expected event advance the ticker and run() the task. Each // step should yield in an IdleStateEvent because we haven't written // or read anything from the channel. for (int i = 0; i < expected.length; i++) { idleStateHandler.tickRun(); } assertEquals(expected.length, events.size()); // Compare the expected with the actual IdleStateEvents for (int i = 0; i < expected.length; i++) { Object evt = events.get(i); assertSame("Element " + i + " is not matching", expected[i], evt); } } finally { channel.finishAndReleaseAll(); } } @Test public void testReaderNotIdle() throws Exception { TestableIdleStateHandler idleStateHandler = new TestableIdleStateHandler( false, 1L, 0L, 0L, TimeUnit.SECONDS); Action action = new Action() { @Override public void run(EmbeddedChannel channel) throws Exception { channel.writeInbound("Hello, World!"); } }; anyNotIdle(idleStateHandler, action, IdleStateEvent.FIRST_READER_IDLE_STATE_EVENT); } @Test public void testWriterNotIdle() throws Exception { TestableIdleStateHandler idleStateHandler = new TestableIdleStateHandler( false, 0L, 1L, 0L, TimeUnit.SECONDS); Action action = new Action() { @Override public void run(EmbeddedChannel channel) throws Exception { channel.writeAndFlush("Hello, World!"); } }; anyNotIdle(idleStateHandler, action, IdleStateEvent.FIRST_WRITER_IDLE_STATE_EVENT); } @Test public void testAllNotIdle() throws Exception { // Reader... TestableIdleStateHandler idleStateHandler = new TestableIdleStateHandler( false, 0L, 0L, 1L, TimeUnit.SECONDS); Action reader = new Action() { @Override public void run(EmbeddedChannel channel) throws Exception { channel.writeInbound("Hello, World!"); } }; anyNotIdle(idleStateHandler, reader, IdleStateEvent.FIRST_ALL_IDLE_STATE_EVENT); // Writer... idleStateHandler = new TestableIdleStateHandler( false, 0L, 0L, 1L, TimeUnit.SECONDS); Action writer = new Action() { @Override public void run(EmbeddedChannel channel) throws Exception { channel.writeAndFlush("Hello, World!"); } }; anyNotIdle(idleStateHandler, writer, IdleStateEvent.FIRST_ALL_IDLE_STATE_EVENT); } private void anyNotIdle(TestableIdleStateHandler idleStateHandler, Action action, Object expected) throws Exception { final List<Object> events = new ArrayList<Object>(); ChannelInboundHandlerAdapter handler = new ChannelInboundHandlerAdapter() { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { events.add(evt); } }; EmbeddedChannel channel = new EmbeddedChannel(idleStateHandler, handler); try { idleStateHandler.tick(1L, TimeUnit.NANOSECONDS); action.run(channel); // Advance the ticker by some fraction and run() the task. // There shouldn't be an IdleStateEvent getting fired because // we've just performed an action on the channel that is meant // to reset the idle task. long delayInNanos = idleStateHandler.delay(TimeUnit.NANOSECONDS); assertNotEquals(0L, delayInNanos); idleStateHandler.tickRun(delayInNanos / 2L, TimeUnit.NANOSECONDS); assertEquals(0, events.size()); // Advance the ticker by the full amount and it should yield // in an IdleStateEvent. idleStateHandler.tickRun(); assertEquals(1, events.size()); assertSame(expected, events.get(0)); } finally { channel.finishAndReleaseAll(); } } @Test public void testObserveWriterIdle() throws Exception { observeOutputIdle(true); } @Test public void testObserveAllIdle() throws Exception { observeOutputIdle(false); } private void observeOutputIdle(boolean writer) throws Exception { long writerIdleTime = 0L; long allIdleTime = 0L; IdleStateEvent expected; if (writer) { writerIdleTime = 5L; expected = IdleStateEvent.FIRST_WRITER_IDLE_STATE_EVENT; } else { allIdleTime = 5L; expected = IdleStateEvent.FIRST_ALL_IDLE_STATE_EVENT; } TestableIdleStateHandler idleStateHandler = new TestableIdleStateHandler( true, 0L, writerIdleTime, allIdleTime, TimeUnit.SECONDS); final List<Object> events = new ArrayList<Object>(); ChannelInboundHandlerAdapter handler = new ChannelInboundHandlerAdapter() { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { events.add(evt); } }; ObservableChannel channel = new ObservableChannel(idleStateHandler, handler); try { // We're writing 3 messages that will be consumed at different rates! channel.writeAndFlush(Unpooled.wrappedBuffer(new byte[] { 1 })); channel.writeAndFlush(Unpooled.wrappedBuffer(new byte[] { 2 })); channel.writeAndFlush(Unpooled.wrappedBuffer(new byte[] { 3 })); // Establish a baseline. We're not consuming anything and let it idle once. idleStateHandler.tickRun(); assertEquals(1, events.size()); assertSame(expected, events.get(0)); events.clear(); // Our ticker should be at second 5 assertEquals(5L, idleStateHandler.tick(TimeUnit.SECONDS)); // Consume one message in 4 seconds, then be idle for 2 seconds, // then run the task and we shouldn't get an IdleStateEvent because // we haven't been idle for long enough! idleStateHandler.tick(4L, TimeUnit.SECONDS); assertNotNullAndRelease(channel.consume()); idleStateHandler.tickRun(2L, TimeUnit.SECONDS); assertEquals(0, events.size()); assertEquals(11L, idleStateHandler.tick(TimeUnit.SECONDS)); // 5s + 4s + 2s // Consume one message in 3 seconds, then be idle for 4 seconds, // then run the task and we shouldn't get an IdleStateEvent because // we haven't been idle for long enough! idleStateHandler.tick(3L, TimeUnit.SECONDS); assertNotNullAndRelease(channel.consume()); idleStateHandler.tickRun(4L, TimeUnit.SECONDS); assertEquals(0, events.size()); assertEquals(18L, idleStateHandler.tick(TimeUnit.SECONDS)); // 11s + 3s + 4s // Don't consume a message and be idle for 5 seconds. // We should get an IdleStateEvent! idleStateHandler.tickRun(5L, TimeUnit.SECONDS); assertEquals(1, events.size()); assertEquals(23L, idleStateHandler.tick(TimeUnit.SECONDS)); // 18s + 5s events.clear(); // Consume one message in 2 seconds, then be idle for 1 seconds, // then run the task and we shouldn't get an IdleStateEvent because // we haven't been idle for long enough! idleStateHandler.tick(2L, TimeUnit.SECONDS); assertNotNullAndRelease(channel.consume()); idleStateHandler.tickRun(1L, TimeUnit.SECONDS); assertEquals(0, events.size()); assertEquals(26L, idleStateHandler.tick(TimeUnit.SECONDS)); // 23s + 2s + 1s // There are no messages left! Advance the ticker by 3 seconds, // attempt a consume() but it will be null, then advance the // ticker by an another 2 seconds and we should get an IdleStateEvent // because we've been idle for 5 seconds. idleStateHandler.tick(3L, TimeUnit.SECONDS); assertNull(channel.consume()); idleStateHandler.tickRun(2L, TimeUnit.SECONDS); assertEquals(1, events.size()); assertEquals(31L, idleStateHandler.tick(TimeUnit.SECONDS)); // 26s + 3s + 2s // q.e.d. } finally { channel.finishAndReleaseAll(); } } private static void assertNotNullAndRelease(Object msg) { assertNotNull(msg); ReferenceCountUtil.release(msg); } private interface Action { void run(EmbeddedChannel channel) throws Exception; } private static class TestableIdleStateHandler extends IdleStateHandler { private Runnable task; private long delayInNanos; private long ticksInNanos; public TestableIdleStateHandler(boolean observeOutput, long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit) { super(observeOutput, readerIdleTime, writerIdleTime, allIdleTime, unit); } public long delay(TimeUnit unit) { return unit.convert(delayInNanos, TimeUnit.NANOSECONDS); } public void run() { task.run(); } public void tickRun() { tickRun(delayInNanos, TimeUnit.NANOSECONDS); } public void tickRun(long delay, TimeUnit unit) { tick(delay, unit); run(); } /** * Advances the current ticker by the given amount. */ public void tick(long delay, TimeUnit unit) { ticksInNanos += unit.toNanos(delay); } /** * Returns {@link #ticksInNanos()} in the given {@link TimeUnit}. */ public long tick(TimeUnit unit) { return unit.convert(ticksInNanos(), TimeUnit.NANOSECONDS); } @Override long ticksInNanos() { return ticksInNanos; } @Override ScheduledFuture<?> schedule(ChannelHandlerContext ctx, Runnable task, long delay, TimeUnit unit) { this.task = task; this.delayInNanos = unit.toNanos(delay); return null; } } private static class ObservableChannel extends EmbeddedChannel { public ObservableChannel(ChannelHandler... handlers) { super(handlers); } @Override protected void doWrite(ChannelOutboundBuffer in) throws Exception { // Overridden to change EmbeddedChannel's default behavior. We went to keep // the messages in the ChannelOutboundBuffer. } public Object consume() { ChannelOutboundBuffer buf = unsafe().outboundBuffer(); if (buf != null) { Object msg = buf.current(); if (msg != null) { ReferenceCountUtil.retain(msg); buf.remove(); return msg; } } return null; } } }