/**
* diqube: Distributed Query Base.
*
* Copyright (C) 2015 Bastian Gloeckle
*
* This file is part of diqube.
*
* diqube is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.diqube.connection;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import org.apache.thrift.TException;
import org.apache.thrift.transport.TTransport;
import org.diqube.connection.integrity.IntegritySecretHelper;
import org.diqube.connection.integrity.IntegritySecretHelperTestUtil;
import org.diqube.queries.QueryUuid;
import org.diqube.remote.cluster.thrift.ClusterManagementService;
import org.diqube.remote.query.thrift.KeepAliveService;
import org.diqube.thrift.base.services.DiqubeThriftServiceInfoManager;
import org.diqube.thrift.base.services.DiqubeThriftServiceInfoManager.DiqubeThriftServiceInfo;
import org.diqube.thrift.base.thrift.RNodeAddress;
import org.diqube.thrift.base.thrift.RNodeDefaultAddress;
import org.mockito.Mockito;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
/**
* Tests for {@link ConnectionPool}.
*
* @author Bastian Gloeckle
*/
public class ConnectionPoolTest {
private static final RNodeAddress ADDR1 = createDefaultRNodeAddress("localhost", (short) 5101);
private static final RNodeAddress ADDR2 = createDefaultRNodeAddress("localhost", (short) 5102);
private ConnectionPool pool;
private TestConnectionFactory conFac;
@BeforeMethod
public void before() {
conFac = new TestConnectionFactory();
DiqubeThriftServiceInfoManager infoMgr = new DiqubeThriftServiceInfoManager();
infoMgr.initialize();
IntegritySecretHelper integritySecretHelper = new IntegritySecretHelper();
IntegritySecretHelperTestUtil.setMessageIntegritySecret(integritySecretHelper, "abc");
pool = new ConnectionPool();
pool.setDiqubeThriftServiceInfoManager(infoMgr);
pool.setIntegritySecretHelper(integritySecretHelper);
}
@AfterMethod
public void after() {
pool.cleanup();
}
private void initPool(int keepAliveMs, int connectionSoftLimit, int connectionIdleTimeMs, double earlyCloseLevel) {
pool.setKeepAliveMs(keepAliveMs);
pool.setConnectionSoftLimit(connectionSoftLimit);
pool.setConnectionIdleTimeMs(connectionIdleTimeMs);
pool.setEarlyCloseLevel(earlyCloseLevel);
pool.initialize();
pool.setConnectionFactory(conFac);
}
@Test
public void connectionReuse() throws ConnectionException, InterruptedException, IOException {
initPool(Integer.MAX_VALUE, 2, Integer.MAX_VALUE, .95);
TestConnection<ClusterManagementService.Iface> conn = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR1, null);
int connId = conn.getId();
Assert.assertEquals(conn.getServiceInfo().getServiceInterface(), ClusterManagementService.Iface.class,
"Correct service expected");
conn.close(); // release connection
// re-request address
conn = (TestConnection<ClusterManagementService.Iface>) pool.reserveConnection(ClusterManagementService.Iface.class,
ADDR1, null);
conn.close();
Assert.assertEquals(conn.getId(), connId, "Expected that connection is re-used");
Assert.assertEquals(conn.getServiceInfo().getServiceInterface(), ClusterManagementService.Iface.class,
"Correct service expected");
}
@Test
public void connectionUsableAfterReserve() throws ConnectionException, InterruptedException, IOException {
initPool(Integer.MAX_VALUE, 2, Integer.MAX_VALUE, .95);
TestConnection<ClusterManagementService.Iface> conn = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR1, null);
conn.getService();
// expected: No exception
}
@Test(expectedExceptions = IllegalStateException.class)
public void connectionUnusableAfterRelease() throws ConnectionException, InterruptedException, IOException {
initPool(Integer.MAX_VALUE, 2, Integer.MAX_VALUE, .95);
TestConnection<ClusterManagementService.Iface> conn = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR1, null);
conn.getService();
// expected: No exception
pool.releaseConnection(conn);
conn.getService();
}
@Test
public void connectionReleaseMultipleTimes() throws ConnectionException, InterruptedException, IOException {
initPool(Integer.MAX_VALUE, 2, Integer.MAX_VALUE, .95);
TestConnection<ClusterManagementService.Iface> conn = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR1, null);
conn.getService();
pool.releaseConnection(conn);
pool.releaseConnection(conn);
// expected: No exception
}
@Test
public void blockTimeoutNewConnectionHighEarlyCloseLevel()
throws ConnectionException, InterruptedException, IOException {
initPool(Integer.MAX_VALUE, //
1, // Only 1 connection simultaneously - we expect to block!
3_000, // 3s idle time.
2); // high earlyCloseLevel to force to block
TestConnection<ClusterManagementService.Iface> conn = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR1, null);
int connId = conn.getId();
Assert.assertEquals(conn.getServiceInfo().getServiceInterface(), ClusterManagementService.Iface.class,
"Correct service expected");
long beforeNanos = System.nanoTime();
conn.close(); // release connection
// request a connection to another host (so it won't get re-used), which should be blocked - the first connection
// was released, but it is effectively still open until it is closed automatically by the timeout!
TestConnection<ClusterManagementService.Iface> conn2 = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR2, null);
long afterNanos = System.nanoTime();
conn2.close();
// ensure that the "close" method on the first connection was called
Mockito.verify(conn.getTransport()).close();
Assert.assertTrue(afterNanos - beforeNanos >= 3_000_000_000L,
"Expected to have blocked at least the timeout time until we receive a new connection, but waited only "
+ (afterNanos - beforeNanos) + " nanos");
Assert.assertNotEquals(conn2.getId(), connId, "Expected that connection was not re-used, as first "
+ "connection should have been closed before the second was opened");
Assert.assertEquals(conn2.getServiceInfo().getServiceInterface(), ClusterManagementService.Iface.class,
"Correct service expected");
}
@Test
public void earlyCloseOfAvailableConnections() throws ConnectionException, InterruptedException, IOException {
initPool(Integer.MAX_VALUE, //
1, // Only 1 connection simultaneously
Integer.MAX_VALUE, //
.95);
TestConnection<ClusterManagementService.Iface> conn = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR1, null);
int connId = conn.getId();
Assert.assertEquals(conn.getServiceInfo().getServiceInterface(), ClusterManagementService.Iface.class,
"Correct service expected");
conn.close(); // release connection
// request a connection to another host (so it won't get re-used), which in our case should NOT be blocked, as the
// pool should reach the "early close" level and close the first connection right away without waiting for it to
// timeout.
TestConnection<ClusterManagementService.Iface> conn2 = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR2, null);
conn2.close();
// ensure that the "close" method on the first connection was called
Mockito.verify(conn.getTransport()).close();
Assert.assertNotEquals(conn2.getId(), connId, "Expected that connection was not re-used, as first "
+ "connection should have been closed before the second was opened");
Assert.assertEquals(conn2.getServiceInfo().getServiceInterface(), ClusterManagementService.Iface.class,
"Correct service expected");
}
@Test
public void blockKeepAliveDeadNewConnectionHighEarlyCloseLevel()
throws ConnectionException, InterruptedException, IOException {
initPool(1_000, // 1s keep alive - check approx every second for keep alives.
1, // Only 1 connection simultaneously - we expect to block!
Integer.MAX_VALUE, //
2); // high earlyCloseLevel to force to block
TestConnection<ClusterManagementService.Iface> conn = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR1, null);
int connId = conn.getId();
Assert.assertEquals(conn.getServiceInfo().getServiceInterface(), ClusterManagementService.Iface.class,
"Correct service expected");
long beforeNanos = System.nanoTime();
// adjust the morph of the connection to a KeepAliveService and make sure there is an exception thrown when the
// "ping" method is called.
conFac.adjustMorphOfConn(connId, new Consumer<TestConnection<?>>() {
@Override
public void accept(TestConnection<?> t) {
try {
if (t.getServiceInfo().getServiceInterface().equals(KeepAliveService.Iface.class)) {
// temp unpool to install our mock
t.pooledCAS(true, false);
Mockito.doThrow(TException.class).when(((KeepAliveService.Iface) t.getService())).ping();
t.pooledCAS(false, true);
}
} catch (TException e) {
throw new RuntimeException(e);
}
}
});
conn.close(); // release connection
// request a connection to another host (so it won't get re-used), which should be blocked - the first connection
// was released, but it is effectively still open until it is closed automatically by the timeout!
TestConnection<ClusterManagementService.Iface> conn2 = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR2, null);
long afterNanos = System.nanoTime();
conn2.close();
// ensure that the "close" method on the first connection was called
Mockito.verify(conn.getTransport()).close();
Assert.assertTrue(afterNanos - beforeNanos >= 1_000_000_000L,
"Expected to have blocked at least the keep-alive time until it was found that the first conn is dead and a "
+ "new conn was opened, but waited only " + (afterNanos - beforeNanos) + " nanos");
Assert.assertNotEquals(conn2.getId(), connId, "Expected that connection was not re-used, as "
+ "first connection should have been closed before the second was opened");
Assert.assertEquals(conn2.getServiceInfo().getServiceInterface(), ClusterManagementService.Iface.class,
"Correct service expected");
}
@Test
public void reuseDeadNewConnection() throws ConnectionException, InterruptedException, IOException {
initPool(Integer.MAX_VALUE, // high keep-alive so it won't get in the way...
1, //
Integer.MAX_VALUE, //
.95);
TestConnection<ClusterManagementService.Iface> conn = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR1, null);
int connId = conn.getId();
Assert.assertEquals(conn.getServiceInfo().getServiceInterface(), ClusterManagementService.Iface.class,
"Correct service expected");
// adjust the morph of the connection to a KeepAliveService and make sure there is an exception thrown when the
// "ping" method is called - this should be executed before re-using the connection above - marking the connection
// above as dead.
conFac.adjustMorphOfConn(connId, new Consumer<TestConnection<?>>() {
@Override
public void accept(TestConnection<?> t) {
try {
if (t.getServiceInfo().getServiceInterface().equals(KeepAliveService.Iface.class)) {
// temp unpool to install our mock
t.pooledCAS(true, false);
Mockito.doThrow(TException.class).when(((KeepAliveService.Iface) t.getService())).ping();
t.pooledCAS(false, true);
}
} catch (TException e) {
throw new RuntimeException(e);
}
}
});
conn.close(); // release connection
// request a connection to the same host (so it will get re-used), which should be blocked - the first connection
// was released, but it is effectively still open until it is closed automatically by the timeout!
TestConnection<ClusterManagementService.Iface> conn2 = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR1, null);
conn2.close();
// ensure that the "close" method on the first connection was called
Mockito.verify(conn.getTransport()).close();
Assert.assertNotEquals(conn2.getId(), connId, "Expected that connection was not re-used, as "
+ "first connection was tried to be re-used but appeared to have died");
Assert.assertEquals(conn2.getServiceInfo().getServiceInterface(), ClusterManagementService.Iface.class,
"Correct service expected");
}
@Test
public void nodeDiedOnConnection() throws ConnectionException, InterruptedException, IOException {
initPool(Integer.MAX_VALUE, // high keep-alive so it won't get in the way...
1, //
Integer.MAX_VALUE, //
.95);
TestConnection<ClusterManagementService.Iface> conn = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR1, null);
int connId = conn.getId();
Assert.assertEquals(conn.getServiceInfo().getServiceInterface(), ClusterManagementService.Iface.class,
"Correct service expected");
conn.close(); // release connection
// simulate that some other part of diqube identified ADDR1 to be down.
pool.nodeDied(ADDR1);
// request another connection, which should open a new one!
TestConnection<ClusterManagementService.Iface> conn2 = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR1, null);
conn2.close();
// ensure that the "close" method on the first connection was called
Mockito.verify(conn.getTransport()).close();
Assert.assertNotEquals(conn2.getId(), connId,
"Expected that connection was not re-used, as " + "node died in between");
Assert.assertEquals(conn2.getServiceInfo().getServiceInterface(), ClusterManagementService.Iface.class,
"Correct service expected");
}
@Test
public void executionGetsAnotherConn() throws ConnectionException, InterruptedException, IOException {
initPool(Integer.MAX_VALUE, // high keep-alive so it won't get in the way...
1, // only one node, we don't want to block, though!
Integer.MAX_VALUE, //
.95);
try {
QueryUuid.setCurrentQueryUuidAndExecutionUuid(UUID.randomUUID(), UUID.randomUUID());
TestConnection<ClusterManagementService.Iface> conn = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR1, null);
int connId = conn.getId();
Assert.assertEquals(conn.getServiceInfo().getServiceInterface(), ClusterManagementService.Iface.class,
"Correct service expected");
// request another connection to another node. We would break the limit, though we want that connection for the
// same executionUuid, therefore we should get it without blocking.
TestConnection<ClusterManagementService.Iface> conn2 = (TestConnection<ClusterManagementService.Iface>) pool
.reserveConnection(ClusterManagementService.Iface.class, ADDR2, null);
conn2.close();
conn.close();
Assert.assertNotEquals(conn2.getId(), connId,
"Expected that connection was not re-used, as both connections should be open at the same time");
Assert.assertEquals(conn2.getServiceInfo().getServiceInterface(), ClusterManagementService.Iface.class,
"Correct service expected");
} finally {
QueryUuid.clearCurrent();
}
}
/**
* A {@link ConnectionFactory} used in this test.
*/
private class TestConnectionFactory implements ConnectionFactory {
private AtomicInteger nextId = new AtomicInteger(0);
private Map<Integer, Consumer<TestConnection<?>>> morphConsumers = new HashMap<>();
public void adjustMorphOfConn(int connId, Consumer<TestConnection<?>> adjuster) {
morphConsumers.put(connId, adjuster);
}
@Override
public <T, U> Connection<U> createConnection(Connection<T> oldConnection, DiqubeThriftServiceInfo<U> serviceInfo)
throws ConnectionException {
TestConnection<U> res = new TestConnection<>(serviceInfo, ((TestConnection<?>) oldConnection).getId(),
((TestConnection<?>) oldConnection).getAddress(), ((TestConnection<?>) oldConnection).getTransport());
res.setTimeout(oldConnection.getTimeout());
res.setExecutionUuid(oldConnection.getExecutionUuid());
if (morphConsumers.containsKey(res.getId()))
morphConsumers.get(res.getId()).accept(res);
return res;
}
@Override
public <T> Connection<T> createConnection(DiqubeThriftServiceInfo<T> serviceInfo, RNodeAddress addr,
SocketListener socketListener) throws ConnectionException {
return new TestConnection<>(serviceInfo, nextId.getAndIncrement(), addr);
}
}
/**
* {@link Connection} class used in test.
*/
private class TestConnection<T> extends Connection<T> {
private int id;
TestConnection(DiqubeThriftServiceInfo<T> serviceInfo, int id, RNodeAddress addr, TTransport transport) {
super(pool, serviceInfo, Mockito.mock(serviceInfo.getServiceInterface(), Mockito.RETURNS_MOCKS), transport, addr);
this.id = id;
}
TestConnection(DiqubeThriftServiceInfo<T> serviceInfo, int id, RNodeAddress addr) {
this(serviceInfo, id, addr, Mockito.mock(TTransport.class));
}
public int getId() {
return id;
}
}
private static RNodeAddress createDefaultRNodeAddress(String host, short port) {
RNodeAddress res = new RNodeAddress();
res.setDefaultAddr(new RNodeDefaultAddress());
res.getDefaultAddr().setHost(host);
res.getDefaultAddr().setPort(port);
return res;
}
}