content: /*
   * Copyright (c) 2007, 2021, Oracle and/or its affiliates.
   *
   * This program is free software; you can redistribute it and/or modify it under
   * the terms of the GNU General Public License, version 2.0, as published by the
   * Free Software Foundation.
   *
   * This program is also distributed with certain software (including but not
   * limited to OpenSSL) that is licensed under separate terms, as designated in a
   * particular file or component or in included license documentation. The
   * authors of MySQL hereby grant you an additional permission to link the
   * program and your derivative works with the separately licensed software that
   * they have included with MySQL.
   *
   * Without limiting anything contained in the foregoing, this file, which is
   * part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
   * version 1.0, a copy of which can be found at
   * http://oss.oracle.com/licenses/universal-foss-exception.
   *
   * 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 General Public License, version 2.0,
   * for more details.
   *
   * You should have received a copy of the GNU General Public License along with
   * this program; if not, write to the Free Software Foundation, Inc.,
   * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
   */
  
  package com.mysql.cj.jdbc.ha;
  
  import static com.mysql.cj.util.StringUtils.isNullOrEmpty;
  
  import java.lang.reflect.InvocationHandler;
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.sql.Connection;
  import java.sql.SQLException;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Properties;
  import java.util.Set;
  import java.util.concurrent.Executor;
  import java.util.stream.Collectors;
  
  import com.mysql.cj.Messages;
  import com.mysql.cj.PingTarget;
  import com.mysql.cj.conf.ConnectionUrl;
  import com.mysql.cj.conf.HostInfo;
  import com.mysql.cj.conf.PropertyKey;
  import com.mysql.cj.conf.url.LoadBalanceConnectionUrl;
  import com.mysql.cj.exceptions.CJCommunicationsException;
  import com.mysql.cj.exceptions.CJException;
  import com.mysql.cj.exceptions.ExceptionFactory;
  import com.mysql.cj.exceptions.MysqlErrorNumbers;
  import com.mysql.cj.interceptors.QueryInterceptor;
  import com.mysql.cj.jdbc.ConnectionGroup;
  import com.mysql.cj.jdbc.ConnectionGroupManager;
  import com.mysql.cj.jdbc.ConnectionImpl;
  import com.mysql.cj.jdbc.JdbcConnection;
  import com.mysql.cj.jdbc.exceptions.SQLError;
  import com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping;
  import com.mysql.cj.util.Util;
  
  /**
   * A proxy for a dynamic com.mysql.cj.jdbc.JdbcConnection implementation that load balances requests across a series of MySQL JDBC connections, where the
   * balancing
   * takes place at transaction commit.
   * 
   * Therefore, for this to work (at all), you must use transactions, even if only reading data.
   * 
   * This implementation will invalidate connections that it detects have had communication errors when processing a request. Problematic hosts will be added to a
   * global blocklist for loadBalanceBlocklistTimeout ms, after which they will be removed from the blocklist and made eligible once again to be selected for new
   * connections.
   * 
   * This implementation is thread-safe, but it's questionable whether sharing a connection instance amongst threads is a good idea, given that transactions are
   * scoped to connections in JDBC.
   */
  public class LoadBalancedConnectionProxy extends MultiHostConnectionProxy implements PingTarget {
      private ConnectionGroup connectionGroup = null;
      private long connectionGroupProxyID = 0;
  
      protected Map<String, ConnectionImpl> liveConnections;
      private Map<String, Integer> hostsToListIndexMap;
      private Map<ConnectionImpl, String> connectionsToHostsMap;
      private long totalPhysicalConnections = 0;
      private long[] responseTimes;
  
      private int retriesAllDown;
      private BalanceStrategy balancer;
  
      private int globalBlocklistTimeout = 0;
      private static Map<String, Long> globalBlocklist = new HashMap<>();
      private int hostRemovalGracePeriod = 0;
      // host:port pairs to be considered as removed (definitely blocklisted) from the original hosts list.
      private Set<String> hostsToRemove = new HashSet<>();
  
      private boolean inTransaction = false;
      private long transactionStartTime = 0;
      private long transactionCount = 0;
  
      private LoadBalanceExceptionChecker exceptionChecker;
  
      private static Class<?>[] INTERFACES_TO_PROXY = new Class<?>[] { LoadBalancedConnection.class, JdbcConnection.class };
  
      /**
       * Static factory to create {@link LoadBalancedConnection} instances.
       * 
       * @param connectionUrl
       *            The connection URL containing the hosts in a load-balance setup.
       * @return A {@link LoadBalancedConnection} proxy.
       * @throws SQLException
       *             if an error occurs
       */
      public static LoadBalancedConnection createProxyInstance(ConnectionUrl connectionUrl) throws SQLException {
          LoadBalancedConnectionProxy connProxy = new LoadBalancedConnectionProxy(connectionUrl);
          return (LoadBalancedConnection) java.lang.reflect.Proxy.newProxyInstance(LoadBalancedConnection.class.getClassLoader(), INTERFACES_TO_PROXY, connProxy);
      }
  
      /**
       * Creates a proxy for java.sql.Connection that routes requests between the hosts in the connection URL.
       * 
       * @param connectionUrl
       *            The connection URL containing the hosts to load balance.
       * @throws SQLException
       *             if an error occurs
       */
      public LoadBalancedConnectionProxy(ConnectionUrl connectionUrl) throws SQLException {
          super();
  
          List<HostInfo> hosts;
          Properties props = connectionUrl.getConnectionArgumentsAsProperties();
  
          String group = props.getProperty(PropertyKey.loadBalanceConnectionGroup.getKeyName(), null);
          boolean enableJMX = false;
          String enableJMXAsString = props.getProperty(PropertyKey.ha_enableJMX.getKeyName(), "false");
          try {
              enableJMX = Boolean.parseBoolean(enableJMXAsString);
          } catch (Exception e) {
              throw SQLError.createSQLException(Messages.getString("MultihostConnection.badValueForHaEnableJMX", new Object[] { enableJMXAsString }),
                      MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, null);
          }
  
          if (!isNullOrEmpty(group) && LoadBalanceConnectionUrl.class.isAssignableFrom(connectionUrl.getClass())) {
              this.connectionGroup = ConnectionGroupManager.getConnectionGroupInstance(group);
              if (enableJMX) {
                  ConnectionGroupManager.registerJmx();
              }
              this.connectionGroupProxyID = this.connectionGroup.registerConnectionProxy(this,
                      ((LoadBalanceConnectionUrl) connectionUrl).getHostInfoListAsHostPortPairs());
              hosts = ((LoadBalanceConnectionUrl) connectionUrl).getHostInfoListFromHostPortPairs(this.connectionGroup.getInitialHosts());
          } else {
              hosts = connectionUrl.getHostsList();
          }
  
          // hosts specifications may have been reset with settings from a previous connection group
          int numHosts = initializeHostsSpecs(connectionUrl, hosts);
  
          this.liveConnections = new HashMap<>(numHosts);
          this.hostsToListIndexMap = new HashMap<>(numHosts);
          for (int i = 0; i < numHosts; i++) {
              this.hostsToListIndexMap.put(this.hostsList.get(i).getHostPortPair(), i);
          }
          this.connectionsToHostsMap = new HashMap<>(numHosts);
          this.responseTimes = new long[numHosts];
  
          String retriesAllDownAsString = props.getProperty(PropertyKey.retriesAllDown.getKeyName(), "120");
          try {
              this.retriesAllDown = Integer.parseInt(retriesAllDownAsString);
          } catch (NumberFormatException nfe) {
              throw SQLError.createSQLException(
                      Messages.getString("LoadBalancedConnectionProxy.badValueForRetriesAllDown", new Object[] { retriesAllDownAsString }),
                      MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, null);
          }
  
          String blocklistTimeoutAsString = props.getProperty(PropertyKey.loadBalanceBlocklistTimeout.getKeyName(), "0");
          try {
              this.globalBlocklistTimeout = Integer.parseInt(blocklistTimeoutAsString);
          } catch (NumberFormatException nfe) {
              throw SQLError.createSQLException(
                      Messages.getString("LoadBalancedConnectionProxy.badValueForLoadBalanceBlocklistTimeout", new Object[] { blocklistTimeoutAsString }),
                      MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, null);
          }
  
          String hostRemovalGracePeriodAsString = props.getProperty(PropertyKey.loadBalanceHostRemovalGracePeriod.getKeyName(), "15000");
          try {
              this.hostRemovalGracePeriod = Integer.parseInt(hostRemovalGracePeriodAsString);
          } catch (NumberFormatException nfe) {
              throw SQLError.createSQLException(Messages.getString("LoadBalancedConnectionProxy.badValueForLoadBalanceHostRemovalGracePeriod",
                      new Object[] { hostRemovalGracePeriodAsString }), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, null);
          }
  
          String strategy = props.getProperty(PropertyKey.ha_loadBalanceStrategy.getKeyName(), "random");
          try {
              switch (strategy) {
                  case "random":
                      this.balancer = new RandomBalanceStrategy();
                      break;
                  case "bestResponseTime":
                      this.balancer = new BestResponseTimeBalanceStrategy();
                      break;
                  case "serverAffinity":
                      this.balancer = new ServerAffinityStrategy(props.getProperty(PropertyKey.serverAffinityOrder.getKeyName(), null));
                      break;
                  default:
                      this.balancer = (BalanceStrategy) Class.forName(strategy).newInstance();
              }
          } catch (Throwable t) {
              throw SQLError.createSQLException(Messages.getString("InvalidLoadBalanceStrategy", new Object[] { strategy }),
                      MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, t, null);
          }
  
          String autoCommitSwapThresholdAsString = props.getProperty(PropertyKey.loadBalanceAutoCommitStatementThreshold.getKeyName(), "0");
          try {
              Integer.parseInt(autoCommitSwapThresholdAsString);
          } catch (NumberFormatException nfe) {
              throw SQLError.createSQLException(Messages.getString("LoadBalancedConnectionProxy.badValueForLoadBalanceAutoCommitStatementThreshold",
                      new Object[] { autoCommitSwapThresholdAsString }), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, null);
          }
  
          String autoCommitSwapRegex = props.getProperty(PropertyKey.loadBalanceAutoCommitStatementRegex.getKeyName(), "");
          if (!("".equals(autoCommitSwapRegex))) {
              try {
                  "".matches(autoCommitSwapRegex);
              } catch (Exception e) {
                  throw SQLError.createSQLException(
                          Messages.getString("LoadBalancedConnectionProxy.badValueForLoadBalanceAutoCommitStatementRegex", new Object[] { autoCommitSwapRegex }),
                          MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, null);
              }
          }
  
          try {
              String lbExceptionChecker = props.getProperty(PropertyKey.loadBalanceExceptionChecker.getKeyName(),
                      StandardLoadBalanceExceptionChecker.class.getName());
              this.exceptionChecker = (LoadBalanceExceptionChecker) Util.getInstance(lbExceptionChecker, new Class<?>[0], new Object[0], null,
                      Messages.getString("InvalidLoadBalanceExceptionChecker"));
              this.exceptionChecker.init(props);
  
          } catch (CJException e) {
              throw SQLExceptionsMapping.translateException(e, null);
          }
  
          pickNewConnection();
      }
  
      /**
       * Wraps this object with a new load balanced Connection instance.
       * 
       * @return
       *         The connection object instance that wraps 'this'.
       * @throws SQLException
       *             if an error occurs
       */
      @Override
      JdbcConnection getNewWrapperForThisAsConnection() throws SQLException {
          return new LoadBalancedMySQLConnection(this);
      }
  
      /**
       * Propagates the connection proxy down through all live connections.
       * 
       * @param proxyConn
       *            The top level connection in the multi-host connections chain.
       */
      @Override
      protected void propagateProxyDown(JdbcConnection proxyConn) {
          for (JdbcConnection c : this.liveConnections.values()) {
              c.setProxy(proxyConn);
          }
      }
  
      @Deprecated
      public boolean shouldExceptionTriggerFailover(Throwable t) {
          return shouldExceptionTriggerConnectionSwitch(t);
      }
  
      /**
       * Consults the registered LoadBalanceExceptionChecker if the given exception should trigger a connection fail-over.
       * 
       * @param t
       *            The Exception instance to check.
       * @return true if the given exception should trigger a connection fail-over
       */
      @Override
      boolean shouldExceptionTriggerConnectionSwitch(Throwable t) {
          return t instanceof SQLException && this.exceptionChecker.shouldExceptionTriggerFailover(t);
      }
  
      /**
       * Always returns 'true' as there are no "sources" and "replicas" in this type of connection.
       */
      @Override
      boolean isSourceConnection() {
          return true;
      }
  
      /**
       * Closes specified connection and removes it from required mappings.
       * 
       * @param conn
       *            connection
       * @throws SQLException
       *             if an error occurs
       */
      @Override
      synchronized void invalidateConnection(JdbcConnection conn) throws SQLException {
          super.invalidateConnection(conn);
  
          // add host to the global blocklist, if enabled
          if (this.isGlobalBlocklistEnabled()) {
              String host = this.connectionsToHostsMap.get(conn);
              if (host != null) {
                  addToGlobalBlocklist(host);
              }
          }
  
          // remove from liveConnections
          this.liveConnections.remove(this.connectionsToHostsMap.get(conn));
          Object mappedHost = this.connectionsToHostsMap.remove(conn);
          if (mappedHost != null && this.hostsToListIndexMap.containsKey(mappedHost)) {
              int hostIndex = this.hostsToListIndexMap.get(mappedHost);
              // reset the statistics for the host
              synchronized (this.responseTimes) {
                  this.responseTimes[hostIndex] = 0;
              }
          }
      }
  
      /**
       * Picks the "best" connection to use for the next transaction based on the BalanceStrategy in use.
       * 
       * @throws SQLException
       *             if an error occurs
       */
      @Override
      public synchronized void pickNewConnection() throws SQLException {
          if (this.isClosed && this.closedExplicitly) {
              return;
          }
  
          List<String> hostPortList = Collections.unmodifiableList(this.hostsList.stream().map(hi -> hi.getHostPortPair()).collect(Collectors.toList()));
  
          if (this.currentConnection == null) { // startup
              this.currentConnection = this.balancer.pickConnection(this, hostPortList, Collections.unmodifiableMap(this.liveConnections),
                      this.responseTimes.clone(), this.retriesAllDown);
              return;
          }
  
          if (this.currentConnection.isClosed()) {
              invalidateCurrentConnection();
          }
  
          int pingTimeout = this.currentConnection.getPropertySet().getIntegerProperty(PropertyKey.loadBalancePingTimeout).getValue();
          boolean pingBeforeReturn = this.currentConnection.getPropertySet().getBooleanProperty(PropertyKey.loadBalanceValidateConnectionOnSwapServer).getValue();
  
          for (int hostsTried = 0, hostsToTry = this.hostsList.size(); hostsTried < hostsToTry; hostsTried++) {
              ConnectionImpl newConn = null;
              try {
                  newConn = (ConnectionImpl) this.balancer.pickConnection(this, hostPortList, Collections.unmodifiableMap(this.liveConnections),
                          this.responseTimes.clone(), this.retriesAllDown);
  
                  if (this.currentConnection != null) {
                      if (pingBeforeReturn) {
                          newConn.pingInternal(true, pingTimeout);
                      }
  
                      syncSessionState(this.currentConnection, newConn);
                  }
  
                  this.currentConnection = newConn;
                  return;
  
              } catch (SQLException e) {
                  if (shouldExceptionTriggerConnectionSwitch(e) && newConn != null) {
                      // connection error, close up shop on current connection
                      invalidateConnection(newConn);
                  }
              }
          }
  
          // no hosts available to swap connection to, close up.
          this.isClosed = true;
          this.closedReason = "Connection closed after inability to pick valid new connection during load-balance.";
      }
  
      /**
       * Creates a new physical connection for the given {@link HostInfo} and updates required internal mappings and statistics for that connection.
       * 
       * @param hostInfo
       *            The host info instance.
       * @return
       *         The new Connection instance.
       * @throws SQLException
       *             if an error occurs
       */
      @Override
      public synchronized ConnectionImpl createConnectionForHost(HostInfo hostInfo) throws SQLException {
          ConnectionImpl conn = super.createConnectionForHost(hostInfo);
  
          this.liveConnections.put(hostInfo.getHostPortPair(), conn);
          this.connectionsToHostsMap.put(conn, hostInfo.getHostPortPair());
  
          removeFromGlobalBlocklist(hostInfo.getHostPortPair());
  
          this.totalPhysicalConnections++;
  
          for (QueryInterceptor stmtInterceptor : conn.getQueryInterceptorsInstances()) {
              if (stmtInterceptor instanceof LoadBalancedAutoCommitInterceptor) {
                  ((LoadBalancedAutoCommitInterceptor) stmtInterceptor).resumeCounters();
                  break;
              }
          }
  
          return conn;
      }
  
      @Override
      void syncSessionState(JdbcConnection source, JdbcConnection target, boolean readOnly) throws SQLException {
          LoadBalancedAutoCommitInterceptor lbAutoCommitStmtInterceptor = null;
          for (QueryInterceptor stmtInterceptor : target.getQueryInterceptorsInstances()) {
              if (stmtInterceptor instanceof LoadBalancedAutoCommitInterceptor) {
                  lbAutoCommitStmtInterceptor = (LoadBalancedAutoCommitInterceptor) stmtInterceptor;
                  lbAutoCommitStmtInterceptor.pauseCounters();
                  break;
              }
          }
          super.syncSessionState(source, target, readOnly);
          if (lbAutoCommitStmtInterceptor != null) {
              lbAutoCommitStmtInterceptor.resumeCounters();
          }
      }
  
      /**
       * Creates a new physical connection for the given host:port info. If the this connection's connection URL knows about this host:port then its host info is
       * used, otherwise a new host info based on current connection URL defaults is spawned.
       * 
       * @param hostPortPair
       *            The host:port pair identifying the host to connect to.
       * @return
       *         The new Connection instance.
       * @throws SQLException
       *             if an error occurs
       */
      public synchronized ConnectionImpl createConnectionForHost(String hostPortPair) throws SQLException {
          for (HostInfo hi : this.hostsList) {
              if (hi.getHostPortPair().equals(hostPortPair)) {
                  return createConnectionForHost(hi);
              }
          }
          return null;
      }
  
      /**
       * Closes all live connections.
       */
      private synchronized void closeAllConnections() {
          // close all underlying connections
          for (Connection c : this.liveConnections.values()) {
              try {
                  c.close();
              } catch (SQLException e) {
              }
          }
  
          if (!this.isClosed) {
              if (this.connectionGroup != null) {
                  this.connectionGroup.closeConnectionProxy(this);
              }
          }
  
          this.liveConnections.clear();
          this.connectionsToHostsMap.clear();
      }
  
      /**
       * Closes all live connections.
       */
      @Override
      synchronized void doClose() {
          closeAllConnections();
      }
  
      /**
       * Aborts all live connections
       */
      @Override
      synchronized void doAbortInternal() {
          // abort all underlying connections
          for (JdbcConnection c : this.liveConnections.values()) {
              try {
                  c.abortInternal();
              } catch (SQLException e) {
              }
          }
  
          if (!this.isClosed) {
              if (this.connectionGroup != null) {
                  this.connectionGroup.closeConnectionProxy(this);
              }
          }
  
          this.liveConnections.clear();
          this.connectionsToHostsMap.clear();
      }
  
      /**
       * Aborts all live connections, using the provided Executor.
       */
      @Override
      synchronized void doAbort(Executor executor) {
          // close all underlying connections
          for (Connection c : this.liveConnections.values()) {
              try {
                  c.abort(executor);
              } catch (SQLException e) {
              }
          }
  
          if (!this.isClosed) {
              if (this.connectionGroup != null) {
                  this.connectionGroup.closeConnectionProxy(this);
              }
          }
  
          this.liveConnections.clear();
          this.connectionsToHostsMap.clear();
      }
  
      /**
       * Proxies method invocation on the java.sql.Connection interface, trapping "close", "isClosed" and "commit/rollback" to switch connections for load
       * balancing.
       * This is the continuation of MultiHostConnectionProxy#invoke(Object, Method, Object[]).
       */
      @Override
      Object invokeMore(Object proxy, Method method, Object[] args) throws Throwable {
          String methodName = method.getName();
  
          if (this.isClosed && !allowedOnClosedConnection(method) && method.getExceptionTypes().length > 0) { // TODO remove method.getExceptionTypes().length ?
              if (this.autoReconnect && !this.closedExplicitly) {
                  // try to reconnect first!
                  this.currentConnection = null;
                  pickNewConnection();
                  this.isClosed = false;
                  this.closedReason = null;
              } else {
                  String reason = "No operations allowed after connection closed.";
                  if (this.closedReason != null) {
                      reason += " " + this.closedReason;
                  }
  
                  for (Class<?> excls : method.getExceptionTypes()) {
                      if (SQLException.class.isAssignableFrom(excls)) {
                          throw SQLError.createSQLException(reason, MysqlErrorNumbers.SQL_STATE_CONNECTION_NOT_OPEN,
                                  null /* no access to an interceptor here... */);
                      }
                  }
                  throw ExceptionFactory.createException(CJCommunicationsException.class, reason);
              }
          }
  
          if (!this.inTransaction) {
              this.inTransaction = true;
              this.transactionStartTime = System.nanoTime();
              this.transactionCount++;
          }
  
          Object result = null;
  
          try {
              result = method.invoke(this.thisAsConnection, args);
  
              if (result != null) {
                  if (result instanceof com.mysql.cj.jdbc.JdbcStatement) {
                      ((com.mysql.cj.jdbc.JdbcStatement) result).setPingTarget(this);
                  }
                  result = proxyIfReturnTypeIsJdbcInterface(method.getReturnType(), result);
              }
  
          } catch (InvocationTargetException e) {
              dealWithInvocationException(e);
  
          } finally {
              if ("commit".equals(methodName) || "rollback".equals(methodName)) {
                  this.inTransaction = false;
  
                  // Update stats
                  String host = this.connectionsToHostsMap.get(this.currentConnection);
                  // avoid NPE if the connection has already been removed from connectionsToHostsMap in invalidateCurrenctConnection()
                  if (host != null) {
                      synchronized (this.responseTimes) {
                          Integer hostIndex = (this.hostsToListIndexMap.get(host));
  
                          if (hostIndex != null && hostIndex < this.responseTimes.length) {
                              this.responseTimes[hostIndex] = System.nanoTime() - this.transactionStartTime;
                          }
                      }
                  }
                  pickNewConnection();
              }
          }
  
          return result;
      }
  
      /**
       * Pings live connections.
       * 
       * @throws SQLException
       *             if an error occurs
       */
      @Override
      public synchronized void doPing() throws SQLException {
          SQLException se = null;
          boolean foundHost = false;
          int pingTimeout = this.currentConnection.getPropertySet().getIntegerProperty(PropertyKey.loadBalancePingTimeout).getValue();
  
          synchronized (this) {
              for (HostInfo hi : this.hostsList) {
                  String host = hi.getHostPortPair();
                  ConnectionImpl conn = this.liveConnections.get(host);
                  if (conn == null) {
                      continue;
                  }
                  try {
                      if (pingTimeout == 0) {
                          conn.ping();
                      } else {
                          conn.pingInternal(true, pingTimeout);
                      }
                      foundHost = true;
                  } catch (SQLException e) {
                      // give up if it is the current connection, otherwise NPE faking resultset later.
                      if (host.equals(this.connectionsToHostsMap.get(this.currentConnection))) {
                          // clean up underlying connections, since connection pool won't do it
                          closeAllConnections();
                          this.isClosed = true;
                          this.closedReason = "Connection closed because ping of current connection failed.";
                          throw e;
                      }
  
                      // if the Exception is caused by ping connection lifetime checks, don't add to blocklist
                      if (e.getMessage().equals(Messages.getString("Connection.exceededConnectionLifetime"))) {
                          // only set the return Exception if it's null
                          if (se == null) {
                              se = e;
                          }
                      } else {
                          // overwrite the return Exception no matter what
                          se = e;
                          if (isGlobalBlocklistEnabled()) {
                              addToGlobalBlocklist(host);
                          }
                      }
                      // take the connection out of the liveConnections Map
                      this.liveConnections.remove(this.connectionsToHostsMap.get(conn));
                  }
              }
          }
          // if there were no successful pings
          if (!foundHost) {
              closeAllConnections();
              this.isClosed = true;
              this.closedReason = "Connection closed due to inability to ping any active connections.";
              // throw the stored Exception, if exists
              if (se != null) {
                  throw se;
              }
              // or create a new SQLException and throw it, must be no liveConnections
              ((ConnectionImpl) this.currentConnection).throwConnectionClosedException();
          }
      }
  
      /**
       * Adds a host to the blocklist with the given timeout.
       * 
       * @param host
       *            The host to be blocklisted.
       * @param timeout
       *            The blocklist timeout for this entry.
       */
      public void addToGlobalBlocklist(String host, long timeout) {
          if (isGlobalBlocklistEnabled()) {
              synchronized (globalBlocklist) {
                  globalBlocklist.put(host, timeout);
              }
          }
      }
  
      /**
       * Removes a host from the blocklist.
       *
       * @param host
       *            The host to be removed from the blocklist.
       */
      public void removeFromGlobalBlocklist(String host) {
          if (isGlobalBlocklistEnabled() && globalBlocklist.containsKey(host)) {
              synchronized (globalBlocklist) {
                  globalBlocklist.remove(host);
              }
          }
      }
  
      /**
       * Use {@link #removeFromGlobalBlocklist(String)} instead.
       * 
       * @param host
       *            host
       * @deprecated
       */
      @Deprecated
      public void removeFromGlobalBlacklist(String host) {
          this.removeFromGlobalBlocklist(host);
      }
  
      /**
       * Use {@link #addToGlobalBlocklist(String, long)} instead.
       * 
       * @param host
       *            The host to be blocklisted.
       * @param timeout
       *            The blocklist timeout for this entry.
       * @deprecated
       */
      @Deprecated
      public void addToGlobalBlacklist(String host, long timeout) {
          this.addToGlobalBlocklist(host, timeout);
      }
  
      /**
       * Adds a host to the blocklist.
       * 
       * @param host
       *            The host to be blocklisted.
       */
      public void addToGlobalBlocklist(String host) {
          addToGlobalBlocklist(host, System.currentTimeMillis() + this.globalBlocklistTimeout);
      }
  
      /**
       * Use {@link #addToGlobalBlocklist(String)} instead.
       * 
       * @param host
       *            The host to be blocklisted.
       * @deprecated
       */
      @Deprecated
      public void addToGlobalBlacklist(String host) {
          this.addToGlobalBlocklist(host);
      }
  
      /**
       * Checks if host blocklist management was enabled.
       * 
       * @return true if host blocklist management was enabled
       */
      public boolean isGlobalBlocklistEnabled() {
          return (this.globalBlocklistTimeout > 0);
      }
  
      /**
       * Use {@link #isGlobalBlocklistEnabled()} instead.
       * 
       * @return true if host blocklist management was enabled
       * @deprecated
       */
      @Deprecated
      public boolean isGlobalBlacklistEnabled() {
          return this.isGlobalBlocklistEnabled();
      }
  
      /**
       * Returns a local hosts blocklist, while cleaning up expired records from the global blocklist, or a blocklist with the hosts to be removed.
       * 
       * @return
       *         A local hosts blocklist.
       */
      public synchronized Map<String, Long> getGlobalBlocklist() {
          if (!isGlobalBlocklistEnabled()) {
              if (this.hostsToRemove.isEmpty()) {
                  return new HashMap<>(1);
              }
              HashMap<String, Long> fakedBlocklist = new HashMap<>();
              for (String h : this.hostsToRemove) {
                  fakedBlocklist.put(h, System.currentTimeMillis() + 5000);
              }
              return fakedBlocklist;
          }
  
          // Make a local copy of the blocklist
          Map<String, Long> blocklistClone = new HashMap<>(globalBlocklist.size());
          // Copy everything from synchronized global blocklist to local copy for manipulation
          synchronized (globalBlocklist) {
              blocklistClone.putAll(globalBlocklist);
          }
          Set<String> keys = blocklistClone.keySet();
  
          // We're only interested in blocklisted hosts that are in the hostList
          keys.retainAll(this.hostsList.stream().map(hi -> hi.getHostPortPair()).collect(Collectors.toList()));
  
          // Don't need to synchronize here as we using a local copy
          for (Iterator<String> i = keys.iterator(); i.hasNext();) {
              String host = i.next();
              // OK if null is returned because another thread already purged Map entry.
              Long timeout = globalBlocklist.get(host);
              if (timeout != null && timeout < System.currentTimeMillis()) {
                  // Timeout has expired, remove from blocklist
                  synchronized (globalBlocklist) {
                      globalBlocklist.remove(host);
                  }
                  i.remove();
              }
  
          }
          if (keys.size() == this.hostsList.size()) {
              // return an empty blocklist, let the BalanceStrategy implementations try to connect to everything since it appears that all hosts are
              // unavailable - we don't want to wait for loadBalanceBlocklistTimeout to expire.
              return new HashMap<>(1);
          }
  
          return blocklistClone;
      }
  
      /**
       * Use {@link #getGlobalBlocklist()} instead.
       * 
       * @return
       *         A local hosts blocklist.
       * @deprecated
       */
      @Deprecated
      public synchronized Map<String, Long> getGlobalBlacklist() {
          return getGlobalBlocklist();
      }
  
      /**
       * Removes a host from the host list, allowing it some time to be released gracefully if needed.
       * 
       * @param hostPortPair
       *            The host to be removed.
       * @throws SQLException
       *             if an error occurs
       */
      public void removeHostWhenNotInUse(String hostPortPair) throws SQLException {
          if (this.hostRemovalGracePeriod <= 0) {
              removeHost(hostPortPair);
              return;
          }
  
          int timeBetweenChecks = this.hostRemovalGracePeriod > 1000 ? 1000 : this.hostRemovalGracePeriod;
  
          synchronized (this) {
              addToGlobalBlocklist(hostPortPair, System.currentTimeMillis() + this.hostRemovalGracePeriod + timeBetweenChecks);
  
              long cur = System.currentTimeMillis();
  
              while (System.currentTimeMillis() < cur + this.hostRemovalGracePeriod) {
                  this.hostsToRemove.add(hostPortPair);
  
                  if (!hostPortPair.equals(this.currentConnection.getHostPortPair())) {
                      removeHost(hostPortPair);
                      return;
                  }
  
                  try {
                      Thread.sleep(timeBetweenChecks);
                  } catch (InterruptedException e) {
                      // better to swallow this and retry.
                  }
              }
          }
  
          removeHost(hostPortPair);
      }
  
      /**
       * Removes a host from the host list.
       * 
       * @param hostPortPair
       *            The host to be removed.
       * @throws SQLException
       *             if an error occurs
       */
      public synchronized void removeHost(String hostPortPair) throws SQLException {
          if (this.connectionGroup != null) {
              if (this.connectionGroup.getInitialHosts().size() == 1 && this.connectionGroup.getInitialHosts().contains(hostPortPair)) {
                  throw SQLError.createSQLException(Messages.getString("LoadBalancedConnectionProxy.0"), null);
              }
          }
  
          this.hostsToRemove.add(hostPortPair);
  
          this.connectionsToHostsMap.remove(this.liveConnections.remove(hostPortPair));
          if (this.hostsToListIndexMap.remove(hostPortPair) != null) {
              long[] newResponseTimes = new long[this.responseTimes.length - 1];
              int newIdx = 0;
              for (HostInfo hostInfo : this.hostsList) {
                  String host = hostInfo.getHostPortPair();
                  if (!this.hostsToRemove.contains(host)) {
                      Integer idx = this.hostsToListIndexMap.get(host);
                      if (idx != null && idx < this.responseTimes.length) {
                          newResponseTimes[newIdx] = this.responseTimes[idx];
                      }
                      this.hostsToListIndexMap.put(host, newIdx++);
                  }
              }
              this.responseTimes = newResponseTimes;
          }
  
          if (hostPortPair.equals(this.currentConnection.getHostPortPair())) {
              invalidateConnection(this.currentConnection);
              pickNewConnection();
          }
      }
  
      /**
       * Adds a host to the hosts list.
       * 
       * @param hostPortPair
       *            The host to be added.
       * @return true if host was added and false if the host list already contains it
       */
      public synchronized boolean addHost(String hostPortPair) {
          if (this.hostsToListIndexMap.containsKey(hostPortPair)) {
              return false;
          }
  
          long[] newResponseTimes = new long[this.responseTimes.length + 1];
          System.arraycopy(this.responseTimes, 0, newResponseTimes, 0, this.responseTimes.length);
  
          this.responseTimes = newResponseTimes;
          if (this.hostsList.stream().noneMatch(hi -> hostPortPair.equals(hi.getHostPortPair()))) {
              this.hostsList.add(this.connectionUrl.getHostOrSpawnIsolated(hostPortPair));
          }
          this.hostsToListIndexMap.put(hostPortPair, this.responseTimes.length - 1);
          this.hostsToRemove.remove(hostPortPair);
  
          return true;
      }
  
      public synchronized boolean inTransaction() {
          return this.inTransaction;
      }
  
      public synchronized long getTransactionCount() {
          return this.transactionCount;
      }
  
      public synchronized long getActivePhysicalConnectionCount() {
          return this.liveConnections.size();
      }
  
      public synchronized long getTotalPhysicalConnectionCount() {
          return this.totalPhysicalConnections;
      }
  
      public synchronized long getConnectionGroupProxyID() {
          return this.connectionGroupProxyID;
      }
  
      public synchronized String getCurrentActiveHost() {
          JdbcConnection c = this.currentConnection;
          if (c != null) {
              Object o = this.connectionsToHostsMap.get(c);
              if (o != null) {
                  return o.toString();
              }
          }
          return null;
      }
  
      public synchronized long getCurrentTransactionDuration() {
          if (this.inTransaction && this.transactionStartTime > 0) {
              return System.nanoTime() - this.transactionStartTime;
          }
          return 0;
      }
  
      /**
       * A LoadBalancedConnection proxy that provides null-functionality. It can be used as a replacement of the <b>null</b> keyword in the places where a
       * LoadBalancedConnection object cannot be effectively <b>null</b> because that would be a potential source of NPEs.
       */
      private static class NullLoadBalancedConnectionProxy implements InvocationHandler {
          public NullLoadBalancedConnectionProxy() {
          }
  
          @Override
          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
              SQLException exceptionToThrow = SQLError.createSQLException(Messages.getString("LoadBalancedConnectionProxy.unusableConnection"),
                      MysqlErrorNumbers.SQL_STATE_INVALID_TRANSACTION_STATE, MysqlErrorNumbers.ERROR_CODE_NULL_LOAD_BALANCED_CONNECTION, true, null);
              Class<?>[] declaredException = method.getExceptionTypes();
              for (Class<?> declEx : declaredException) {
                  if (declEx.isAssignableFrom(exceptionToThrow.getClass())) {
                      throw exceptionToThrow;
                  }
              }
              throw new IllegalStateException(exceptionToThrow.getMessage(), exceptionToThrow);
          }
      }
  
      private static LoadBalancedConnection nullLBConnectionInstance = null;
  
      static synchronized LoadBalancedConnection getNullLoadBalancedConnectionInstance() {
          if (nullLBConnectionInstance == null) {
              nullLBConnectionInstance = (LoadBalancedConnection) java.lang.reflect.Proxy.newProxyInstance(LoadBalancedConnection.class.getClassLoader(),
                      INTERFACES_TO_PROXY, new NullLoadBalancedConnectionProxy());
          }
          return nullLBConnectionInstance;
      }
  }
  