/* * Copyright 2016 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.pool; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.logging.LoggingHandler; import io.reactivex.netty.channel.Connection; import io.reactivex.netty.channel.ConnectionImpl; import io.reactivex.netty.client.ClientConnectionToChannelBridge; import io.reactivex.netty.client.ConnectionProvider; import io.reactivex.netty.client.Host; import io.reactivex.netty.client.HostConnector; import io.reactivex.netty.client.events.ClientEventListener; import io.reactivex.netty.events.EventAttributeKeys; import io.reactivex.netty.events.EventPublisher; import io.reactivex.netty.events.EventSource; import io.reactivex.netty.test.util.MockEventPublisher; import io.reactivex.netty.test.util.TrackableMetricEventsListener; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.ExternalResource; import org.junit.runner.Description; import org.junit.runners.model.Statement; import rx.Observable; import rx.Observable.OnSubscribe; import rx.Subscriber; import rx.observers.TestSubscriber; import rx.schedulers.Schedulers; import rx.schedulers.TestScheduler; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.net.InetSocketAddress; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import static io.reactivex.netty.client.pool.MaxConnectionsBasedStrategy.*; import static java.lang.annotation.ElementType.*; import static org.hamcrest.MatcherAssert.*; import static org.hamcrest.Matchers.*; public class PooledConnectionProviderImplTest { @Rule public ExpectedException thrown= ExpectedException.none(); @Rule public final PooledFactoryRule pooledFactoryRule = new PooledFactoryRule(); @Test(timeout = 6000000) public void testConnect() throws Exception { pooledFactoryRule.getAConnection(); pooledFactoryRule.assertNoIdleConnection(); } @MaxConnections(1) @Test(timeout = 60000) public void testReuse() throws Exception { final PooledConnection<String, String> conn1 = pooledFactoryRule.getAConnection(); pooledFactoryRule.returnToIdle(conn1); PooledConnection<String, String> conn2 = pooledFactoryRule.getAConnection(); assertThat("Connection not reused.", conn2, is(conn1)); } @Test(timeout = 60000) public void testRelease() throws Exception { _testRelease(); } @Test(timeout = 60000) public void testDiscard() throws Exception { final Connection<String, String> connection = pooledFactoryRule.getAConnection(); assertThat("Connection is null.", connection, notNullValue()); pooledFactoryRule.assertNoIdleConnection(); /*This attribute will discard on close*/ connection.unsafeNettyChannel().attr(ClientConnectionToChannelBridge.DISCARD_CONNECTION).set(true); /* Close will discard */ pooledFactoryRule.closeAndAwait(connection); /*Throw error or close quietly*/ pooledFactoryRule.assertNoIdleConnection(); } @Test(timeout = 60000) public void testExpired() throws Exception { final PooledConnection<String, String> connection = pooledFactoryRule.getAConnection(); assertThat("Connection is null.", connection, notNullValue()); pooledFactoryRule.assertNoIdleConnection(); /*This attribute will discard on close*/ connection.setLastReturnToPoolTimeMillis(System.currentTimeMillis() - 30000); /* Close will discard */ pooledFactoryRule.closeAndAwait(connection); /*Throw error or close quietly*/ pooledFactoryRule.assertNoIdleConnection(); } @Test(timeout = 60000000) public void testIdleConnectionCleanup() throws Exception { PooledConnection<String, String> idleConnection = _testRelease(); /*Force discard by next idle connection reap*/ idleConnection.unsafeNettyChannel().attr(ClientConnectionToChannelBridge.DISCARD_CONNECTION).set(true); pooledFactoryRule.testScheduler.advanceTimeBy(1, TimeUnit.MINUTES); pooledFactoryRule.assertNoIdleConnection(); } @MaxConnections(1) @Test(timeout = 60000) public void testPoolExhaustion() throws Exception { thrown.expectCause(isA(PoolExhaustedException.class)); pooledFactoryRule.getAConnection(); pooledFactoryRule.getProvider().newConnectionRequest().toBlocking().single(); } @Test(timeout = 60000) public void testConnectFailed() throws Exception { PooledConnectionProvider<String, String> factory; PoolConfig<String, String> config = new PoolConfig<>(); config.idleConnectionsHolder(pooledFactoryRule.holder); MockEventPublisher<ClientEventListener> publisher = MockEventPublisher.disabled(); ClientEventListener listener = new ClientEventListener(); EmbeddedConnectionProvider connectionProvider = new EmbeddedConnectionProvider(publisher, true, listener); Host host = new Host(new InetSocketAddress("127.0.0.1", 0)); HostConnector<String, String> connector = new HostConnector<>(host, connectionProvider, publisher, publisher, listener); factory = new PooledConnectionProviderImpl<>(config, connector); TestSubscriber<Object> subscriber = new TestSubscriber<>(); factory.newConnectionRequest().subscribe(subscriber); subscriber.assertTerminalEvent(); assertThat("Error not returned to connect.", subscriber.getOnCompletedEvents(), is(empty())); assertThat("Error not returned to connect.", subscriber.getOnNextEvents(), is(empty())); } @Test(timeout = 60000) public void testMetricEventCallback() throws Throwable { TrackableMetricEventsListener eventsListener = new TrackableMetricEventsListener(); pooledFactoryRule.init(DEFAULT_MAX_CONNECTIONS, MockEventPublisher.<ClientEventListener>enabled(), MockEventPublisher.enabled(), eventsListener); final PooledConnection<String, String> connection = pooledFactoryRule.getAConnection(); assertThat("Unexpected acquire attempted count.", eventsListener.getAcquireAttemptedCount(), is(1L)); assertThat("Unexpected acquire succedded count.", eventsListener.getAcquireSucceededCount(), is(1L)); assertThat("Unexpected acquire failed count.", eventsListener.getAcquireFailedCount(), is(0L)); pooledFactoryRule.returnToIdle(connection); assertThat("Unexpected release attempted count.", eventsListener.getReleaseAttemptedCount(), is(1L)); assertThat("Unexpected release succeeded count.", eventsListener.getReleaseSucceededCount(), is(1L)); assertThat("Unexpected release failed count.", eventsListener.getReleaseFailedCount(), is(0L)); final PooledConnection<String, String> reusedConn = pooledFactoryRule.getAConnection(); Assert.assertEquals("Reused connection not same as original.", connection, reusedConn); assertThat("Unexpected acquire attempted count.", eventsListener.getAcquireAttemptedCount(), is(2L)); assertThat("Unexpected acquire succedded count.", eventsListener.getAcquireSucceededCount(), is(2L)); assertThat("Unexpected acquire failed count.", eventsListener.getAcquireFailedCount(), is(0L)); assertThat("Unexpected reuse count.", eventsListener.getReuseCount(), is(1L)); pooledFactoryRule.closeAndAwait(reusedConn); assertThat("Unexpected release attempted count.", eventsListener.getReleaseAttemptedCount(), is(2L)); assertThat("Unexpected release succeeded count.", eventsListener.getReleaseSucceededCount(), is(2L)); assertThat("Unexpected release failed count.", eventsListener.getReleaseFailedCount(), is(0L)); pooledFactoryRule.provider.discard(reusedConn).toBlocking().lastOrDefault(null); assertThat("Unexpected release attempted count.", eventsListener.getReleaseAttemptedCount(), is(2L)); assertThat("Unexpected release succeeded count.", eventsListener.getReleaseSucceededCount(), is(2L)); assertThat("Unexpected release failed count.", eventsListener.getReleaseFailedCount(), is(0L)); assertThat("Unexpected connection eviction count.", eventsListener.getEvictionCount(), is(1L)); } private PooledConnection<String, String> _testRelease() throws Exception { final Connection<String, String> connection = pooledFactoryRule.getAConnection(); pooledFactoryRule.assertNoIdleConnection(); /* Close will release */ pooledFactoryRule.closeAndAwait(connection); /*Throw error or close quietly*/ PooledConnection<String, String> connIdle = pooledFactoryRule.holder.peek().defaultIfEmpty(null).toBlocking().single(); assertThat("Release did not add to idle.", connIdle, not(nullValue())); return connIdle; } public static class PooledFactoryRule extends ExternalResource { private PooledConnectionProvider<String, String> provider; private TestScheduler testScheduler; private FIFOIdleConnectionsHolder<String, String> holder; @Override public Statement apply(final Statement base, final Description description) { return new Statement() { @Override public void evaluate() throws Throwable { MaxConnections maxConnections1 = description.getAnnotation(MaxConnections.class); ClientEventListener listener = new ClientEventListener(); final MockEventPublisher<ClientEventListener> publisher = MockEventPublisher.disabled(); int maxConnections = null == maxConnections1? DEFAULT_MAX_CONNECTIONS : maxConnections1.value(); init(maxConnections, publisher, publisher, listener); base.evaluate(); } }; } protected void init(int maxConnections, EventSource<? extends ClientEventListener> eventSource, EventPublisher publisher, ClientEventListener clientListener) { testScheduler = Schedulers.test(); Observable<Long> idleConnCleaner = Observable.timer(1, TimeUnit.MINUTES, testScheduler); holder = new FIFOIdleConnectionsHolder<>(); PoolConfig<String, String> config = new PoolConfig<>(); config.idleConnectionsCleanupTimer(idleConnCleaner) .maxConnections(maxConnections) .idleConnectionsHolder(holder); Host host = new Host(new InetSocketAddress("127.0.0.1", 0)); ConnectionProvider<String, String> cp = new EmbeddedConnectionProvider(publisher, clientListener); HostConnector<String, String> connector = new HostConnector<>(host, cp, eventSource, publisher, clientListener); provider = new PooledConnectionProviderImpl<>(config, connector); } public PooledConnectionProvider<String, String> getProvider() { return provider; } public PooledConnection<String, String> getAConnection(Observable<Connection<String, String>> connectionObservable) throws InterruptedException, ExecutionException, TimeoutException { TestSubscriber<Connection<String, String>> connSub = new TestSubscriber<>(); connectionObservable.subscribe(connSub); connSub.awaitTerminalEvent(); connSub.assertNoErrors(); assertThat("Unexpected connections returned on connect.", connSub.getOnNextEvents(), hasSize(1)); Connection<String, String> connection = connSub.getOnNextEvents().get(0); assertThat("Connection is null.", connection, notNullValue()); return (PooledConnection<String, String>) connection; } public PooledConnection<String, String> getAConnection() throws InterruptedException, ExecutionException, TimeoutException { return getAConnection(getProvider().newConnectionRequest()); } public void closeAndAwait(Connection<String, String> toClose) throws Exception { EmbeddedChannel embeddedChannel= (EmbeddedChannel) toClose.unsafeNettyChannel(); final TestSubscriber<Void> testSubscriber = new TestSubscriber<>(); toClose.close().subscribe(testSubscriber); embeddedChannel.runPendingTasks(); testSubscriber.awaitTerminalEvent(); testSubscriber.assertNoErrors(); } public void assertNoIdleConnection() { final TestSubscriber<PooledConnection<String, String>> subscriber = new TestSubscriber<>(); holder.peek().subscribe(subscriber); subscriber.awaitTerminalEvent(); subscriber.assertNoErrors(); assertThat("Idle connection available", subscriber.getOnNextEvents(), is(empty())); } public void returnToIdle(PooledConnection<String, String> conn1) { conn1.closeNow(); EmbeddedChannel embeddedChannel= (EmbeddedChannel) conn1.unsafeNettyChannel(); embeddedChannel.runPendingTasks(); TestSubscriber<PooledConnection<String, String>> subscriber = new TestSubscriber<>(); holder.peek().take(1).subscribe(subscriber); subscriber.awaitTerminalEvent(); assertThat("Unexpected number of idle connections post release.", subscriber.getOnNextEvents(), hasSize(1)); assertThat("Connection not returned to idle holder on close.", subscriber.getOnNextEvents().get(0), is(conn1)); } } private static class EmbeddedConnectionProvider implements ConnectionProvider<String, String> { private final EventPublisher publisher; private final boolean failConnect; private final ClientEventListener clientListener; public EmbeddedConnectionProvider(EventPublisher publisher, boolean failConnect, ClientEventListener clientListener) { this.publisher = publisher; this.failConnect = failConnect; this.clientListener = clientListener; } public EmbeddedConnectionProvider(EventPublisher publisher, ClientEventListener clientListener) { this(publisher, false, clientListener); } @Override public Observable<Connection<String, String>> newConnectionRequest() { if (failConnect) { return Observable.error(new IllegalStateException("Deliberate connect failure")); } return Observable.create(new OnSubscribe<Connection<String, String>>() { @Override public void call(Subscriber<? super Connection<String, String>> s) { EmbeddedChannel c = new EmbeddedChannel(new LoggingHandler()); c.attr(EventAttributeKeys.EVENT_PUBLISHER).set(publisher); if (publisher.publishingEnabled()) { c.attr(EventAttributeKeys.CLIENT_EVENT_LISTENER).set(clientListener); c.attr(EventAttributeKeys.CONNECTION_EVENT_LISTENER).set(clientListener); } ClientConnectionToChannelBridge.addToPipeline(c.pipeline(), false); s.onNext(ConnectionImpl.<String, String>fromChannel(c)); s.onCompleted(); } }); } } @Retention(RetentionPolicy.RUNTIME) @Target(METHOD) public @interface MaxConnections { int value() default DEFAULT_MAX_CONNECTIONS; } }