io.reactivex.netty.client.ConnectionPoolTest.java Source code

Java tutorial

Introduction

Here is the source code for io.reactivex.netty.client.ConnectionPoolTest.java

Source

/*
 * Copyright 2014 Netflix, Inc.
 *
 * 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 io.reactivex.netty.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.reactivex.netty.ChannelCloseListener;
import io.reactivex.netty.RxNetty;
import io.reactivex.netty.channel.ConnectionHandler;
import io.reactivex.netty.channel.ObservableConnection;
import io.reactivex.netty.metrics.MetricEventsSubject;
import io.reactivex.netty.pipeline.PipelineConfigurator;
import io.reactivex.netty.pipeline.PipelineConfiguratorComposite;
import io.reactivex.netty.pipeline.PipelineConfigurators;
import io.reactivex.netty.server.RxServer;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import rx.Observable;
import rx.Subscriber;
import rx.functions.Action0;

import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static io.reactivex.netty.client.RxClient.ClientConfig.Builder.newDefaultConfig;

/**
 * @author Nitesh Kant
 */
public class ConnectionPoolTest {

    @Rule
    public TestName name = new TestName();

    public static final int MAX_IDLE_TIME_MILLIS = 10000;

    private ConnectionPoolImpl<String, String> pool;
    private RxClient.ServerInfo serverInfo;
    private Bootstrap clientBootstrap;
    private TrackableMetricEventsListener metricEventsListener;
    private MaxConnectionsBasedStrategy strategy;
    private RxServer<String, String> server;
    private final ChannelCloseListener channelCloseListener = new ChannelCloseListener();
    private PoolStats stats;
    private ConnectionHandlerImpl serverConnHandler;
    private PipelineConfigurator<String, String> pipelineConfigurator;
    private String testId;
    private ClientChannelFactoryImpl<String, String> factory;
    private PoolConfig poolConfig;

    @Before
    public void setUp() throws Exception {
        testId = name.getMethodName();
        long currentTime = System.currentTimeMillis();
        System.out.println("Time: " + currentTime + ". Setting up test id: " + testId);
        serverConnHandler = new ConnectionHandlerImpl(testId);
        server = RxNetty.createTcpServer(0, PipelineConfigurators.textOnlyConfigurator(), serverConnHandler)
                .start();
        serverInfo = new RxClient.ServerInfo("localhost", server.getServerPort());
        metricEventsListener = new TrackableMetricEventsListener();
        strategy = new MaxConnectionsBasedStrategy(1);
        clientBootstrap = new Bootstrap().group(new NioEventLoopGroup(4)).channel(NioSocketChannel.class);
        pipelineConfigurator = new PipelineConfiguratorComposite<String, String>(
                PipelineConfigurators.textOnlyConfigurator(), new PipelineConfigurator() {
                    @Override
                    public void configureNewPipeline(ChannelPipeline pipeline) {
                        channelCloseListener.reset();
                        pipeline.addFirst(channelCloseListener);
                    }
                });

        pipelineConfigurator = PipelineConfigurators.createClientConfigurator(pipelineConfigurator,
                newDefaultConfig());

        clientBootstrap.handler(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(Channel ch) throws Exception {
                pipelineConfigurator.configureNewPipeline(ch.pipeline());
            }
        });

        poolConfig = new PoolConfig(MAX_IDLE_TIME_MILLIS);
        MetricEventsSubject<ClientMetricsEvent<?>> eventsSubject = new MetricEventsSubject<ClientMetricsEvent<?>>();
        factory = new ClientChannelFactoryImpl<String, String>(clientBootstrap, eventsSubject);
        pool = new ConnectionPoolImpl<String, String>(serverInfo, poolConfig, strategy, null, factory,
                eventsSubject);
        pool.subscribe(metricEventsListener);
        stats = new PoolStats();
        pool.subscribe(stats);
    }

    @After
    public void tearDown() throws Exception {
        long currentTime = System.currentTimeMillis();
        System.out.println("Time: " + currentTime + ". Tearing down test id: " + testId);
        if (null != pool) {
            pool.shutdown();
        }
        if (null != clientBootstrap) {
            clientBootstrap.group().shutdownGracefully();
        }
        if (null != server) {
            serverConnHandler.closeNewConnectionsOnReceive(false); // reset state after test. Close New should be explicit.
            try {
                serverConnHandler.closeAllClientConnections();
            } catch (IllegalStateException e) {
                // Do nothing if there are no connections
            }
            server.shutdown();
            server.waitTillShutdown(1, TimeUnit.MINUTES);
        }
    }

    @Test
    public void testAcquireRelease() throws Exception {
        serverConnHandler.closeNewConnectionsOnReceive(true);
        ObservableConnection<String, String> conn = acquireAndTestStats();
        conn.close();
        waitForClose();
        assertAllConnectionsReturned();
    }

    @Test
    public void testReleaseAfterClose() throws Exception {
        serverConnHandler.closeNewConnectionsOnReceive(true);
        ObservableConnection<String, String> conn = acquireAndTestStats();
        waitForClose();
        conn.close();
        assertAllConnectionsReturned();
    }

    @Test
    public void testReuse() throws Exception {
        serverConnHandler.closeNewConnectionsOnReceive(false);
        ObservableConnection<String, String> connection = acquireAndTestStats();

        connection.close();

        ObservableConnection<String, String> reusedConn = acquireAndTestStats();

        Assert.assertEquals("Connection reuse callback not received.", 1, metricEventsListener.getReuseCount());
        Assert.assertEquals("Connection not reused.", connection, reusedConn);

        serverConnHandler.closeAllClientConnections();

        waitForClose();
        assertAllConnectionsReturned();
    }

    @Test
    public void testCloseExpiredConnection() throws Exception {
        serverConnHandler.closeNewConnectionsOnReceive(false);
        PooledConnection<String, String> connection = (PooledConnection<String, String>) acquireAndTestStats();

        connection.setLastReturnToPoolTimeMillis(
                System.currentTimeMillis() - PoolConfig.DEFAULT_CONFIG.getMaxIdleTimeMillis());

        connection.close();

        assertAllConnectionsReturned();
    }

    @Test
    public void testDiscard() throws Exception {
        serverConnHandler.closeNewConnectionsOnReceive(false);
        PooledConnection<String, String> connection = (PooledConnection<String, String>) acquireAndTestStats();

        pool.discard(connection);

        Assert.assertEquals("Unexpected pool idle count.", 0, stats.getIdleCount());
        Assert.assertEquals("Unexpected pool in-use count.", 1, stats.getInUseCount());
        Assert.assertEquals("Unexpected pool total connections count.", 1, stats.getTotalConnectionCount());
        Assert.assertEquals("Unexpected eviction count post close.", 0, metricEventsListener.getEvictionCount()); // Since it wasn't idle, there isn't an eviction.
    }

    @Test
    public void testDiscardPostRelease() throws Exception {
        serverConnHandler.closeNewConnectionsOnReceive(false);
        PooledConnection<String, String> connection = (PooledConnection<String, String>) acquireAndTestStats();

        connection.close();

        Assert.assertEquals("Unexpected pool idle count.", 1, stats.getIdleCount());
        Assert.assertEquals("Unexpected pool in-use count.", 0, stats.getInUseCount());
        Assert.assertEquals("Unexpected pool total connections count.", 1, stats.getTotalConnectionCount());
        Assert.assertEquals("Unexpected eviction count post close.", 0, metricEventsListener.getEvictionCount());

        pool.discard(connection);

        Assert.assertEquals("Unexpected pool idle count post discard.", 0, stats.getIdleCount());
        Assert.assertEquals("Unexpected pool in-use count post discard.", 0, stats.getInUseCount());
        Assert.assertEquals("Unexpected pool total connections count post discard.", 0,
                stats.getTotalConnectionCount());
        Assert.assertEquals("Unexpected eviction count post discard.", 1, metricEventsListener.getEvictionCount());

        assertAllConnectionsReturned();
    }

    @Test
    public void testShutdown() throws Exception {
        serverConnHandler.closeNewConnectionsOnReceive(false);
        strategy.incrementMaxConnections(2);

        ObservableConnection<String, String> connection1 = pool.acquire().toBlocking().last();
        ObservableConnection<String, String> connection2 = pool.acquire().toBlocking().last();
        ObservableConnection<String, String> connection3 = pool.acquire().toBlocking().last();

        Assert.assertEquals("Unexpected pool idle count.", 0, stats.getIdleCount());
        Assert.assertEquals("Unexpected pool in-use count.", 3, stats.getInUseCount());
        Assert.assertEquals("Unexpected pool total connections count.", 3, stats.getTotalConnectionCount());

        connection1.close();

        Assert.assertEquals("Unexpected pool idle count.", 1, stats.getIdleCount());
        Assert.assertEquals("Unexpected pool in-use count.", 2, stats.getInUseCount());
        Assert.assertEquals("Unexpected pool total connections count.", 3, stats.getTotalConnectionCount());

        connection2.close();
        connection3.close();

        Assert.assertEquals("Unexpected pool idle count post shutdown.", 3, stats.getIdleCount());
        Assert.assertEquals("Unexpected pool in-use count post shutdown.", 0, stats.getInUseCount());
        Assert.assertEquals("Unexpected pool total connections count post shutdown.", 3,
                stats.getTotalConnectionCount());

        pool.shutdown();

        assertAllConnectionsReturned();
    }

    @Test
    public void testConnectFail() throws Exception {
        serverConnHandler.closeNewConnectionsOnReceive(false);
        RxClient.ServerInfo unavailableServer = new RxClient.ServerInfo("trampledunderfoot", 999);
        MetricEventsSubject<ClientMetricsEvent<?>> eventsSubject = new MetricEventsSubject<ClientMetricsEvent<?>>();
        factory = new ClientChannelFactoryImpl<String, String>(clientBootstrap, eventsSubject);

        pool = new ConnectionPoolImpl<String, String>(unavailableServer, poolConfig, strategy, null, factory,
                eventsSubject);
        pool.subscribe(metricEventsListener);

        try {
            pool.acquire().toBlocking().last();
            throw new AssertionError("Connect to a nonexistent server did not fail.");
        } catch (Exception e) {
            // Expected.
            Assert.assertEquals("Unexpected idle connections count.", 0, stats.getIdleCount());
            Assert.assertEquals("Unexpected in-use connections count.", 0, stats.getInUseCount());
            Assert.assertEquals("Unexpected total connections count.", 0, stats.getTotalConnectionCount());
            Assert.assertEquals("Did not receive a connect failed callback.", 1,
                    metricEventsListener.getFailedCount());
        }
    }

    @Test
    public void testCallbacks() throws Exception {
        serverConnHandler.closeNewConnectionsOnReceive(false);
        strategy.incrementMaxConnections(1);

        PooledConnection<String, String> conn = (PooledConnection<String, String>) pool.acquire().toBlocking()
                .last();
        Assert.assertEquals("Unexpected acquire attempted count.", 1,
                metricEventsListener.getAcquireAttemptedCount());
        Assert.assertEquals("Unexpected acquire succeeded count.", 1,
                metricEventsListener.getAcquireSucceededCount());
        Assert.assertEquals("Unexpected acquire failed count.", 0, metricEventsListener.getAcquireFailedCount());
        Assert.assertEquals("Unexpected create connection count.", 1, metricEventsListener.getCreationCount());

        conn.close();

        Assert.assertEquals("Unexpected release attempted count.", 1,
                metricEventsListener.getReleaseAttemptedCount());
        Assert.assertEquals("Unexpected release succeeded count.", 1,
                metricEventsListener.getReleaseSucceededCount());
        Assert.assertEquals("Unexpected release failed count.", 0, metricEventsListener.getReleaseFailedCount());
        Assert.assertEquals("Unexpected create connection count.", 1, metricEventsListener.getCreationCount());

        PooledConnection<String, String> reusedConn = (PooledConnection<String, String>) pool.acquire().toBlocking()
                .last();

        Assert.assertEquals("Reused connection not same as original.", conn, reusedConn);

        Assert.assertEquals("Unexpected acquire attempted count.", 2,
                metricEventsListener.getAcquireAttemptedCount());
        Assert.assertEquals("Unexpected acquire succeeded count.", 2,
                metricEventsListener.getAcquireSucceededCount());
        Assert.assertEquals("Unexpected acquire failed count.", 0, metricEventsListener.getAcquireFailedCount());
        Assert.assertEquals("Unexpected create connection count.", 1, metricEventsListener.getCreationCount());
        Assert.assertEquals("Unexpected connection reuse count.", 1, metricEventsListener.getReuseCount());

        reusedConn.close();

        Assert.assertEquals("Unexpected release attempted count.", 2,
                metricEventsListener.getReleaseAttemptedCount());
        Assert.assertEquals("Unexpected release succeeded count.", 2,
                metricEventsListener.getReleaseSucceededCount());
        Assert.assertEquals("Unexpected release failed count.", 0, metricEventsListener.getReleaseFailedCount());
        Assert.assertEquals("Unexpected create connection count.", 1, metricEventsListener.getCreationCount());

        pool.discard(reusedConn);

        Assert.assertEquals("Unexpected release attempted count.", 2,
                metricEventsListener.getReleaseAttemptedCount());
        Assert.assertEquals("Unexpected release succeeded count.", 2,
                metricEventsListener.getReleaseSucceededCount());
        Assert.assertEquals("Unexpected release failed count.", 0, metricEventsListener.getReleaseFailedCount());
        Assert.assertEquals("Unexpected create connection count.", 1, metricEventsListener.getCreationCount());
        Assert.assertEquals("Unexpected evict connection count.", 1, metricEventsListener.getEvictionCount());

    }

    @Test
    public void testPoolExhaustion() throws Exception {
        serverConnHandler.closeNewConnectionsOnReceive(false);

        PooledConnection<String, String> connection = (PooledConnection<String, String>) acquireAndTestStats();

        try {
            pool.acquire().toBlocking().last();
            throw new AssertionError("Pool did not exhaust.");
        } catch (Exception e) {
            // expected
            if (e instanceof PoolExhaustedException) {
                Assert.assertEquals("Unexpected idle connection count on pool exhaustion", 0, stats.getIdleCount());
                Assert.assertEquals("Unexpected used connection count on pool exhaustion", 1,
                        stats.getInUseCount());
                Assert.assertEquals("Unexpected total connection count on pool exhaustion", 1,
                        stats.getTotalConnectionCount());
            }
        }

        // try release and acquire now.
        connection.close();

        PooledConnection<String, String> connection1 = (PooledConnection<String, String>) acquireAndTestStats();
        connection1.close();
        pool.discard(connection1);
        waitForClose();

        assertAllConnectionsReturned();
    }

    @Test
    public void testIdleCleanupThread() throws Exception {
        serverConnHandler.closeNewConnectionsOnReceive(false);
        pool.shutdown();
        MetricEventsSubject<ClientMetricsEvent<?>> eventsSubject = new MetricEventsSubject<ClientMetricsEvent<?>>();
        factory.useMetricEventsSubject(eventsSubject);
        pool = new ConnectionPoolImpl<String, String>(serverInfo, PoolConfig.DEFAULT_CONFIG, strategy,
                Executors.newScheduledThreadPool(1), factory, eventsSubject);

        stats = new PoolStats();
        pool.subscribe(stats);

        ObservableConnection<String, String> connection = acquireAndTestStats();
        connection.close();

        serverConnHandler.closeAllClientConnections();

        waitForClose();

        assertAllConnectionsReturned();
    }

    @Test
    public void testIdleTimeout() throws Exception {
        serverConnHandler.closeNewConnectionsOnReceive(false);
        PooledConnection<String, String> connection = (PooledConnection<String, String>) acquireAndTestStats();

        connection.close();
        Assert.assertTrue("Pooled connection is unusable after close.", connection.isUsable());

        Thread.sleep(MAX_IDLE_TIME_MILLIS + 10); // Wait for idle timeout.

        Assert.assertFalse("Pooled connection should have been unusable after idle timeout", connection.isUsable());
    }

    private void waitForClose() throws InterruptedException {
        if (!channelCloseListener.waitForClose(3, TimeUnit.MINUTES)) {
            throw new AssertionError("Client channel not closed after sufficient wait.");
        }
    }

    private void assertAllConnectionsReturned() {
        Assert.assertEquals("Unexpected pool idle count, post release.", 0, stats.getIdleCount());
        Assert.assertEquals("Unexpected pool in-use count, post release.", 0, stats.getInUseCount());
        Assert.assertEquals("Unexpected pool total connections count, post release.", 0,
                stats.getTotalConnectionCount());
    }

    private ObservableConnection<String, String> acquireAndTestStats() throws InterruptedException {
        ObservableConnection<String, String> conn = pool.acquire().toBlocking().last();
        Assert.assertEquals("Unexpected pool idle count.", 0, stats.getIdleCount());
        Assert.assertEquals("Unexpected pool in-use count.", 1, stats.getInUseCount());
        Assert.assertEquals("Unexpected pool total connections count.", 1, stats.getTotalConnectionCount());
        final CountDownLatch writeFinishLatch = new CountDownLatch(1);
        conn.writeAndFlush("Hi").finallyDo(new Action0() {
            @Override
            public void call() {
                writeFinishLatch.countDown();
            }
        });
        writeFinishLatch.await(1, TimeUnit.SECONDS);
        return conn;
    }

    private static class ConnectionHandlerImpl implements ConnectionHandler<String, String> {

        private final String testId;
        private volatile boolean closeConnectionOnReceive = true;
        private final ConcurrentLinkedQueue<ObservableConnection<String, String>> lastReceivedConnection = new ConcurrentLinkedQueue<ObservableConnection<String, String>>();

        private ConnectionHandlerImpl(String testId) {
            this.testId = testId;
        }

        @Override
        public Observable<Void> handle(final ObservableConnection<String, String> newConnection) {
            long currentTime = System.currentTimeMillis();
            lastReceivedConnection.add(newConnection);
            System.out.println(
                    "Time: " + currentTime + ". Test Id: " + testId + ". Added a new connection on the server.");
            if (closeConnectionOnReceive) {
                System.out.println("Time: " + currentTime + ". Test Id: " + testId
                        + ". Closed the newly created connection on the server.");
                return newConnection.close();
            } else {
                return Observable.create(new Observable.OnSubscribe<Void>() {
                    @Override
                    public void call(Subscriber<? super Void> subscriber) {
                        // keeps the connection alive forever.
                    }
                });
            }
        }

        public void closeNewConnectionsOnReceive(boolean closeConnectionOnReceive) {
            this.closeConnectionOnReceive = closeConnectionOnReceive;
        }

        public void closeAllClientConnections() {
            long currentTime = System.currentTimeMillis();
            if (lastReceivedConnection.size() <= 0) {
                throw new IllegalStateException(
                        "Time: " + currentTime + ". No connections on the server to close.");
            }
            Iterator<ObservableConnection<String, String>> iterator = lastReceivedConnection.iterator();
            while (iterator.hasNext()) {
                ObservableConnection<String, String> next = iterator.next();
                next.close();
                System.out.println("Time: " + currentTime + ". Test Id: " + testId
                        + ". Removed a connection from the server.");
                iterator.remove();
            }
        }
    }
}