com.netflix.dyno.connectionpool.impl.ConnectionPoolImpl Maven / Gradle / Ivy
/*******************************************************************************
* Copyright 2011 Netflix
*
* 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 com.netflix.dyno.connectionpool.impl;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import com.netflix.dyno.connectionpool.*;
import com.netflix.dyno.connectionpool.exception.FatalConnectionException;
import com.netflix.dyno.connectionpool.exception.PoolExhaustedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.netflix.dyno.connectionpool.exception.DynoException;
import com.netflix.dyno.connectionpool.exception.NoAvailableHostsException;
import com.netflix.dyno.connectionpool.impl.HostConnectionPoolFactory.Type;
import com.netflix.dyno.connectionpool.impl.health.ConnectionPoolHealthTracker;
import com.netflix.dyno.connectionpool.impl.lb.HostSelectionWithFallback;
import com.netflix.dyno.connectionpool.impl.utils.CollectionUtils;
import com.netflix.dyno.connectionpool.impl.utils.CollectionUtils.Predicate;
import javax.management.*;
/**
* Main implementation class for {@link ConnectionPool} The pool deals with a
* bunch of other components and brings together all the functionality for Dyno.
* Hence this is where all the action happens.
*
* Here are the top salient features of this class.
*
* 1. Manages a collection of {@link HostConnectionPool}s for all the
* {@link Host}s that it receives from the {@link HostSupplier}
*
* 2. Manages adding and removing hosts as dictated by the HostSupplier.
*
* 3. Enables execution of {@link Operation} using a {@link Connection} borrowed
* from the {@link HostConnectionPool}s
*
* 4. Employs a {@link HostSelectionStrategy} (basically Round Robin or Token
* Aware) when executing operations
*
* 5. Uses a health check monitor for tracking error rates from the execution of
* operations. The health check monitor can then decide to recycle a given
* HostConnectionPool, and execute requests using fallback HostConnectionPools
* for remote DCs.
*
* 6. Uses {@link RetryPolicy} when executing operations for better resilience
* against transient failures.
*
* @param
* @author poberai
* @see {@link HostSupplier} {@link Host} {@link HostSelectionStrategy}
* @see {@link Connection} {@link ConnectionFactory}
* {@link ConnectionPoolConfiguration} {@link ConnectionPoolMonitor}
* @see {@link ConnectionPoolHealthTracker}
*/
public class ConnectionPoolImpl implements ConnectionPool, TopologyView {
private static final Logger Logger = LoggerFactory.getLogger(ConnectionPoolImpl.class);
private final ConcurrentHashMap> cpMap = new ConcurrentHashMap>();
private final ConnectionPoolHealthTracker cpHealthTracker;
private final HostConnectionPoolFactory hostConnPoolFactory;
private final ConnectionFactory connFactory;
private final ConnectionPoolConfiguration cpConfiguration;
private final ConnectionPoolMonitor cpMonitor;
private final ScheduledExecutorService idleThreadPool = Executors.newSingleThreadScheduledExecutor();
private final HostsUpdater hostsUpdater;
private final ScheduledExecutorService connPoolThreadPool = Executors.newScheduledThreadPool(1);
private final AtomicBoolean started = new AtomicBoolean(false);
private final AtomicBoolean idling = new AtomicBoolean(false);
private HostSelectionWithFallback selectionStrategy;
private Type poolType;
public ConnectionPoolImpl(ConnectionFactory cFactory, ConnectionPoolConfiguration cpConfig,
ConnectionPoolMonitor cpMon) {
this(cFactory, cpConfig, cpMon, Type.Sync);
}
public ConnectionPoolImpl(ConnectionFactory cFactory, ConnectionPoolConfiguration cpConfig,
ConnectionPoolMonitor cpMon, Type type) {
this.connFactory = cFactory;
this.cpConfiguration = cpConfig;
this.cpMonitor = cpMon;
this.poolType = type;
this.cpHealthTracker = new ConnectionPoolHealthTracker(cpConfiguration, connPoolThreadPool);
switch (type) {
case Sync:
hostConnPoolFactory = new SyncHostConnectionPoolFactory();
break;
case Async:
hostConnPoolFactory = new AsyncHostConnectionPoolFactory();
break;
default:
throw new RuntimeException("unknown type");
}
this.hostsUpdater = new HostsUpdater(cpConfiguration.getHostSupplier(), cpConfiguration.getTokenSupplier());
}
public String getName() {
return cpConfiguration.getName();
}
public ConnectionPoolMonitor getMonitor() {
return cpMonitor;
}
public ConnectionPoolHealthTracker getHealthTracker() {
return cpHealthTracker;
}
@Override
public boolean isIdle() {
return idling.get();
}
@Override
public boolean addHost(Host host) {
return addHost(host, true);
}
public boolean addHost(Host host, boolean refreshLoadBalancer) {
//host.setPort(cpConfiguration.getPort());
HostConnectionPool connPool = cpMap.get(host);
if (connPool != null) {
if (Logger.isDebugEnabled()) {
Logger.debug("HostConnectionPool already exists for host: " + host + ", ignoring addHost");
}
return false;
}
final HostConnectionPool hostPool = hostConnPoolFactory.createHostConnectionPool(host, this);
HostConnectionPool prevPool = cpMap.putIfAbsent(host, hostPool);
if (prevPool == null) {
// This is the first time we are adding this pool.
Logger.info("Adding host connection pool for host: " + host);
try {
int primed = hostPool.primeConnections();
Logger.info("Successfully primed " + primed + " of " + cpConfiguration.getMaxConnsPerHost() + " to "
+ host);
if (hostPool.isActive()) {
if (refreshLoadBalancer) {
selectionStrategy.addHost(host, hostPool);
}
cpHealthTracker.initializePingHealthchecksForPool(hostPool);
cpMonitor.hostAdded(host, hostPool);
} else {
Logger.info(
"Failed to prime enough connections to host " + host + " for it take traffic; will retry");
cpMap.remove(host);
}
return primed > 0;
} catch (DynoException e) {
Logger.error("Failed to init host pool for host: " + host, e);
cpMap.remove(host);
return false;
}
} else {
return false;
}
}
@Override
public boolean removeHost(Host host) {
Logger.info(String.format("Removing host %s from selectionStrategy, cpHealthTracker, cpMonitor",
host.getHostAddress()));
// Since there are multiple data structures of host, token, connection pool etc, call removehost even
// if it is not found in the cpMap
selectionStrategy.removeHost(host);
cpHealthTracker.removeHost(host);
cpMonitor.hostRemoved(host);
HostConnectionPool hostPool = cpMap.remove(host);
if (hostPool != null) {
hostPool.shutdown();
Logger.info(String.format("Remove host: Successfully removed hostPool for host %s from connection pool",
host.getHostAddress()));
return true;
} else {
Logger.info(String.format("Remove host: Host pool for host %s NOT FOUND in the connection pool", host.getHostAddress()));
return false;
}
}
@Override
public boolean isHostUp(Host host) {
HostConnectionPool hostPool = cpMap.get(host);
return (hostPool != null) ? hostPool.isActive() : false;
}
@Override
public boolean hasHost(Host host) {
return cpMap.get(host) != null;
}
@Override
public List> getActivePools() {
return new ArrayList>(
CollectionUtils.filter(getPools(), new Predicate>() {
@Override
public boolean apply(HostConnectionPool hostPool) {
if (hostPool == null) {
return false;
}
return hostPool.isActive();
}
}));
}
@Override
public List> getPools() {
return new ArrayList>(cpMap.values());
}
@Override
public Future updateHosts(Collection hostsUp, Collection hostsDown) {
Logger.debug(String.format("Updating hosts: UP=%s, DOWN=%s", hostsUp, hostsDown));
boolean condition = false;
if (hostsUp != null && !hostsUp.isEmpty()) {
for (Host hostUp : hostsUp) {
condition |= addHost(hostUp);
}
}
if (hostsDown != null && !hostsDown.isEmpty()) {
for (Host hostDown : hostsDown) {
condition |= removeHost(hostDown);
}
}
return getEmptyFutureTask(condition);
}
@Override
public HostConnectionPool getHostPool(Host host) {
return cpMap.get(host);
}
@Override
public OperationResult executeWithFailover(Operation op) throws DynoException {
// Start recording the operation
long startTime = System.currentTimeMillis();
RetryPolicy retry = cpConfiguration.getRetryPolicyFactory().getRetryPolicy();
retry.begin();
DynoException lastException = null;
do {
Connection connection = null;
try {
connection = selectionStrategy.getConnectionUsingRetryPolicy(op,
cpConfiguration.getMaxTimeoutWhenExhausted(), TimeUnit.MILLISECONDS, retry);
connection.getContext().setMetadata("host", connection.getHost().getHostAddress());
connection.getContext().setMetadata("port", connection.getHost().getPort());
OperationResult result = connection.execute(op);
// Add context to the result from the successful execution
result.setNode(connection.getHost()).addMetadata(connection.getContext().getAll());
retry.success();
cpMonitor.incOperationSuccess(connection.getHost(), System.currentTimeMillis() - startTime);
return result;
} catch (NoAvailableHostsException e) {
cpMonitor.incOperationFailure(null, e);
throw e;
} catch (PoolExhaustedException e) {
Logger.warn("Pool exhausted: " + e.getMessage());
cpMonitor.incOperationFailure(null, e);
cpHealthTracker.trackConnectionError(e.getHostConnectionPool(), e);
} catch (DynoException e) {
retry.failure(e);
lastException = e;
if (connection != null) {
cpMonitor.incOperationFailure(connection.getHost(), e);
if (retry.allowRetry()) {
cpMonitor.incFailover(connection.getHost(), e);
}
// Track the connection health so that the pool can be
// purged at a later point
cpHealthTracker.trackConnectionError(connection.getParentConnectionPool(), lastException);
} else {
cpMonitor.incOperationFailure(null, e);
}
} catch (Throwable t) {
throw new RuntimeException(t);
} finally {
if (connection != null) {
if (connection.getLastException() != null
&& connection.getLastException() instanceof FatalConnectionException) {
Logger.warn("Received FatalConnectionException; closing connection "
+ connection.getContext().getAll() + " to host "
+ connection.getParentConnectionPool().getHost());
connection.getParentConnectionPool().recycleConnection(connection);
// note - don't increment connection closed metric here;
// it's done in closeConnection
} else {
connection.getContext().reset();
connection.getParentConnectionPool().returnConnection(connection);
}
}
}
} while (retry.allowRetry());
throw lastException;
}
@Override
public Collection> executeWithRing(TokenRackMapper tokenRackMapper, Operation op) throws DynoException {
// Start recording the operation
long startTime = System.currentTimeMillis();
Collection> connections = selectionStrategy
.getConnectionsToRing(tokenRackMapper, cpConfiguration.getMaxTimeoutWhenExhausted(), TimeUnit.MILLISECONDS);
LinkedBlockingQueue> connQueue = new LinkedBlockingQueue>();
connQueue.addAll(connections);
List> results = new ArrayList>();
DynoException lastException = null;
try {
while (!connQueue.isEmpty()) {
Connection connection = connQueue.poll();
RetryPolicy retry = cpConfiguration.getRetryPolicyFactory().getRetryPolicy();
retry.begin();
do {
try {
connection.getContext().setMetadata("host", connection.getHost().getHostAddress());
OperationResult result = connection.execute(op);
// Add context to the result from the successful
// execution
result.setNode(connection.getHost()).addMetadata(connection.getContext().getAll());
retry.success();
cpMonitor.incOperationSuccess(connection.getHost(), System.currentTimeMillis() - startTime);
results.add(result);
} catch (NoAvailableHostsException e) {
cpMonitor.incOperationFailure(null, e);
throw e;
} catch (DynoException e) {
retry.failure(e);
lastException = e;
cpMonitor.incOperationFailure(connection != null ? connection.getHost() : null, e);
// Track the connection health so that the pool can be
// purged at a later point
if (connection != null) {
cpHealthTracker.trackConnectionError(connection.getParentConnectionPool(), lastException);
}
} catch (Throwable t) {
throw new RuntimeException(t);
} finally {
connection.getContext().reset();
connection.getParentConnectionPool().returnConnection(connection);
}
} while (retry.allowRetry());
}
// we fail the entire operation on a partial failure. hence need to
// clean up the rest of the pending connections
} finally {
List> remainingConns = new ArrayList>();
connQueue.drainTo(remainingConns);
for (Connection connectionToClose : remainingConns) {
try {
connectionToClose.getContext().reset();
connectionToClose.getParentConnectionPool().returnConnection(connectionToClose);
} catch (Throwable t) {
}
}
}
if (lastException != null) {
throw lastException;
} else {
return results;
}
}
/**
* Use with EXTREME CAUTION. Connection that is borrowed must be returned,
* else we will have connection pool exhaustion
*
* @param baseOperation
* @return
*/
public Connection getConnectionForOperation(BaseOperation baseOperation) {
return selectionStrategy.getConnection(baseOperation, cpConfiguration.getMaxTimeoutWhenExhausted(),
TimeUnit.MILLISECONDS);
}
@Override
public void shutdown() {
if (started.get()) {
for (Host host : cpMap.keySet()) {
removeHost(host);
}
cpHealthTracker.stop();
hostsUpdater.stop();
connPoolThreadPool.shutdownNow();
deregisterMonitorConsoleMBean();
}
}
@Override
public Future start() throws DynoException {
if (started.get()) {
return getEmptyFutureTask(false);
}
HostSupplier hostSupplier = cpConfiguration.getHostSupplier();
if (hostSupplier == null) {
throw new DynoException("Host supplier not configured!");
}
HostStatusTracker hostStatus = hostsUpdater.refreshHosts();
cpMonitor.setHostCount(hostStatus.getHostCount());
Collection hostsUp = hostStatus.getActiveHosts();
if (hostsUp == null || hostsUp.isEmpty()) {
throw new NoAvailableHostsException("No available hosts when starting connection pool");
}
final ExecutorService threadPool = Executors.newFixedThreadPool(Math.max(10, hostsUp.size()));
final List> futures = new ArrayList>();
for (final Host host : hostsUp) {
// Add host connection pool, but don't init the load balancer yet
futures.add(threadPool.submit(new Callable() {
@Override
public Void call() throws Exception {
addHost(host, false);
return null;
}
}));
}
try {
for (Future future : futures) {
try {
future.get();
} catch (InterruptedException e) {
// do nothing
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
} finally {
threadPool.shutdownNow();
}
boolean success = started.compareAndSet(false, true);
if (success) {
idling.set(false);
idleThreadPool.shutdownNow();
selectionStrategy = initSelectionStrategy();
cpHealthTracker.start();
connPoolThreadPool.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
HostStatusTracker hostStatus = hostsUpdater.refreshHosts();
cpMonitor.setHostCount(hostStatus.getHostCount());
Logger.debug(hostStatus.toString());
updateHosts(hostStatus.getActiveHosts(), hostStatus.getInactiveHosts());
} catch (Throwable throwable) {
Logger.error("Failed to update hosts cache", throwable);
}
}
}, 15 * 1000, 30 * 1000, TimeUnit.MILLISECONDS);
MonitorConsole.getInstance().registerConnectionPool(this);
registerMonitorConsoleMBean(MonitorConsole.getInstance());
}
return getEmptyFutureTask(true);
}
@Override
public void idle() {
if (this.started.get()) {
throw new IllegalStateException("Cannot move from started to idle once the pool has been started");
}
if (idling.compareAndSet(false, true)) {
idleThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
if (!started.get()) {
try {
HostStatusTracker hostStatus = hostsUpdater.refreshHosts();
cpMonitor.setHostCount(hostStatus.getHostCount());
Collection hostsUp = hostStatus.getActiveHosts();
if (hostsUp.size() > 0) {
Logger.debug("Found hosts while IDLING; starting the connection pool");
start().get();
}
} catch (NoAvailableHostsException nah) {
Logger.debug("No hosts found, will continue IDLING");
} catch (DynoException de) {
Logger.warn("Attempt to start connection pool FAILED", de);
} catch (Exception e) {
Logger.warn("Attempt to start connection pool FAILED", e);
}
}
}
}, 30, 60, TimeUnit.SECONDS);
}
}
@Override
public ConnectionPoolConfiguration getConfiguration() {
return cpConfiguration;
}
private void registerMonitorConsoleMBean(MonitorConsoleMBean bean) {
final MBeanServer server = ManagementFactory.getPlatformMBeanServer();
try {
ObjectName objectName = new ObjectName(MonitorConsole.OBJECT_NAME);
if (!server.isRegistered(objectName)) {
server.registerMBean(bean, objectName);
Logger.info("registered mbean " + objectName);
} else {
Logger.info("mbean " + objectName + " has already been registered !");
}
} catch (MalformedObjectNameException | InstanceAlreadyExistsException | MBeanRegistrationException
| NotCompliantMBeanException ex) {
Logger.error("Unable to register MonitorConsole mbean ", ex);
}
}
private void deregisterMonitorConsoleMBean() {
final MBeanServer server = ManagementFactory.getPlatformMBeanServer();
try {
ObjectName objectName = new ObjectName(MonitorConsole.OBJECT_NAME);
if (server.isRegistered(objectName)) {
server.unregisterMBean(objectName);
Logger.info("deregistered mbean " + objectName);
}
} catch (MalformedObjectNameException | MBeanRegistrationException | InstanceNotFoundException ex) {
Logger.error("Unable to deregister MonitorConsole mbean ", ex);
}
}
private HostSelectionWithFallback initSelectionStrategy() {
if (cpConfiguration.getTokenSupplier() == null) {
throw new RuntimeException("TokenMapSupplier not configured");
}
HostSelectionWithFallback selection = new HostSelectionWithFallback(cpConfiguration, cpMonitor);
selection.initWithHosts(cpMap);
return selection;
}
private Future getEmptyFutureTask(final Boolean condition) {
FutureTask future = new FutureTask(new Callable() {
@Override
public Boolean call() throws Exception {
return condition;
}
});
future.run();
return future;
}
private class SyncHostConnectionPoolFactory implements HostConnectionPoolFactory {
@Override
public HostConnectionPool createHostConnectionPool(Host host, ConnectionPoolImpl parentPoolImpl) {
return new HostConnectionPoolImpl(host, connFactory, cpConfiguration, cpMonitor);
}
}
private class AsyncHostConnectionPoolFactory implements HostConnectionPoolFactory {
@Override
public HostConnectionPool createHostConnectionPool(Host host, ConnectionPoolImpl parentPoolImpl) {
return new SimpleAsyncConnectionPoolImpl(host, connFactory, cpConfiguration, cpMonitor);
}
}
@Override
public ListenableFuture> executeAsync(AsyncOperation op) throws DynoException {
DynoException lastException = null;
Connection connection = null;
long startTime = System.currentTimeMillis();
try {
connection = selectionStrategy.getConnection(op, cpConfiguration.getMaxTimeoutWhenExhausted(),
TimeUnit.MILLISECONDS);
ListenableFuture> futureResult = connection.executeAsync(op);
cpMonitor.incOperationSuccess(connection.getHost(), System.currentTimeMillis() - startTime);
return futureResult;
} catch (NoAvailableHostsException e) {
cpMonitor.incOperationFailure(null, e);
throw e;
} catch (DynoException e) {
lastException = e;
cpMonitor.incOperationFailure(connection != null ? connection.getHost() : null, e);
// Track the connection health so that the pool can be purged at a
// later point
if (connection != null) {
cpHealthTracker.trackConnectionError(connection.getParentConnectionPool(), lastException);
}
} catch (Throwable t) {
t.printStackTrace();
} finally {
if (connection != null) {
connection.getParentConnectionPool().returnConnection(connection);
}
}
return null;
}
public TokenPoolTopology getTopology() {
return selectionStrategy.getTokenPoolTopology();
}
@Override
public Map> getTopologySnapshot() {
return Collections.unmodifiableMap(selectionStrategy.getTokenPoolTopology().getAllTokens());
}
@Override
public Long getTokenForKey(String key) {
if (cpConfiguration
.getLoadBalancingStrategy() == ConnectionPoolConfiguration.LoadBalancingStrategy.TokenAware) {
return selectionStrategy.getTokenForKey(key);
}
return null;
}
}