All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.threadly.db.aurora.DelegatingAuroraConnection Maven / Gradle / Ivy

package org.threadly.db.aurora;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.threadly.db.AbstractDelegatingConnection;
import org.threadly.db.ErrorValidSqlConnection;
import org.threadly.db.aurora.DelegatingAuroraConnection.ConnectionStateManager.ConnectionHolder;
import org.threadly.util.Clock;
import org.threadly.util.Pair;
import org.threadly.util.StringUtils;

/**
 * Implementation of {@link Connection} which under the hood delegates to one of several 
 * connections to an Aurora server.  This uses the {@link AuroraClusterMonitor} to monitor the 
 * provided cluster, and intelligently choose which connection to delegate to.
 * 

* From an external perspective this is just a single connection doing some magic to fairly * distribute load if possible, and to handle fail over conditions with minimal impact. */ public class DelegatingAuroraConnection extends AbstractDelegatingConnection implements Connection { /** * Used with {@link #setClientInfo(String, String)} and {@link #setClientInfo(Properties)} to * control which delegate connection should be used. Possible values are: *

    *
  • {@link #CLIENT_INFO_VALUE_DELEGATE_CHOICE_SMART} (default) - Will provide a replica server if likely safe *
  • {@link #CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY} - Will provide any server available *
  • {@link #CLIENT_INFO_VALUE_DELEGATE_CHOICE_MASTER_PREFERED} - Will try to provide the master *
  • {@link #CLIENT_INFO_VALUE_DELEGATE_CHOICE_MASTER_ONLY} - Will only provide a healthy master *
  • {@link #CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY_REPLICA_PREFERED} - Will try to provide a random replica *
  • {@link #CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY_REPLICA_ONLY} - Will only provide a healthy replica *
*/ public static final String CLIENT_INFO_NAME_DELEGATE_CHOICE = "DelegateChoice"; /** * Possible value for {@link #CLIENT_INFO_NAME_DELEGATE_CHOICE} to * {@link #setClientInfo(String, String)}. *

* This value will randomly choose either a master or replica server in a normalized distribution. */ public static final String CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY = "Any"; /** * Possible value for {@link #CLIENT_INFO_NAME_DELEGATE_CHOICE} to * {@link #setClientInfo(String, String)}. *

* This value will choose a replica server if the connection is read only and the driver believes * it is safe to do so. If a the preferred server type is unhealthy it will fallback to either * the master or a replica if possible. */ public static final String CLIENT_INFO_VALUE_DELEGATE_CHOICE_SMART = "Smart"; /** * Possible value for {@link #CLIENT_INFO_NAME_DELEGATE_CHOICE} to * {@link #setClientInfo(String, String)}. *

* This value will try to always provide the master, unless it is unhealthy, in which case a * replica will be used. If no servers are healthy a {@link NoAuroraServerException} will be * thrown. */ public static final String CLIENT_INFO_VALUE_DELEGATE_CHOICE_MASTER_PREFERED = "MasterPrefered"; /** * Possible value for {@link #CLIENT_INFO_NAME_DELEGATE_CHOICE} to * {@link #setClientInfo(String, String)}. *

* This value will ONLY provide the master server, choosing to fail the request with a * {@link NoAuroraServerException} if the master is unhealthy. */ public static final String CLIENT_INFO_VALUE_DELEGATE_CHOICE_MASTER_ONLY = "MasterOnly"; /** * Possible value for {@link #CLIENT_INFO_NAME_DELEGATE_CHOICE} to * {@link #setClientInfo(String, String)}. *

* This value will try to always provide a random replica, unless none are healthy, in which case * the master will be used. If no servers are healthy a {@link NoAuroraServerException} will be * thrown. */ public static final String CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY_REPLICA_PREFERED = "ReplicaPrefered"; /** * Possible value for {@link #CLIENT_INFO_NAME_DELEGATE_CHOICE} to * {@link #setClientInfo(String, String)}. *

* This value will ONLY provide a random replica server, choosing to fail the request with a * {@link NoAuroraServerException} if there are no healthy replicas. */ public static final String CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY_REPLICA_ONLY = "ReplicaOnly"; /** * Possible value for {@link #CLIENT_INFO_NAME_DELEGATE_CHOICE} to * {@link #setClientInfo(String, String)}. *

* This value will ONLY provide a random replica server from the first half of the available * cluster. If there is no available replica's the request will fail with a * {@link NoAuroraServerException}. */ public static final String CLIENT_INFO_VALUE_DELEGATE_CHOICE_HALF_1_REPLICA_ONLY = "ReplicaPart1Only"; /** * Possible value for {@link #CLIENT_INFO_NAME_DELEGATE_CHOICE} to * {@link #setClientInfo(String, String)}. *

* This value will ONLY provide a random replica server from the second half of the available * cluster. This requires a minimum of {@code 2} healthy replica servers. If one or no replicas * are healthy a {@link NoAuroraServerException} will be thrown. */ public static final String CLIENT_INFO_VALUE_DELEGATE_CHOICE_HALF_2_REPLICA_ONLY = "ReplicaPart2Only"; protected static final String CLIENT_INFO_VALUE_DELEGATE_CHOICE_DEFAULT = // may be set by providing null or empty CLIENT_INFO_VALUE_DELEGATE_CHOICE_SMART; protected static final Logger LOG = Logger.getLogger(AuroraClusterMonitor.class.getSimpleName()); /** * Check if a given URL is accepted by this connection. * * @param url URL to test * @return {@code true} if this connection is able to handled the provided url */ public static boolean acceptsURL(String url) { return DelegateAuroraDriver.driverForArcUrl(url) != null; } /* Updates for frequently changed state values is managed by the `ConnectionStateManager`. * Updates which need to applied immediately, or which are unlikely to be performance concerns * are in contrast updated while synchronizing on the `connections` array and applied to all * in the calling thread. If any of these synchronized states show to be performance sensitive * areas we may move them into the `ConnectionStateManager`, but we want to avoid doing that * unnecessarily so that it does not become burdened with too much state tracking. */ protected final ConnectionStateManager connectionStateManager; protected final AuroraServer[] servers; protected volatile String delegateChoice; // visible for testing private final String driverName; private final ConnectionHolder[] connections; private final Connection referenceConnection; // also stored in map, just used to global settings private final AuroraClusterMonitor clusterMonitor; private final AtomicBoolean closed; private volatile Pair stickyConnection; /** * Construct a new connection with the provided url and info. Aurora servers should be * delineated by {@code ','}. Ultimately this will depend on {@link DelegateAuroraDriver} to delegate * to an implementation based off the provided {@code url}. * * @param url Servers and connect properties to connect to * @param info Info properties * @throws SQLException Thrown if issue in establishing delegate connections */ public DelegatingAuroraConnection(String url, Properties info) throws SQLException { DelegateAuroraDriver dDriver = DelegateAuroraDriver.driverForArcUrl(url); if (dDriver == null) { throw new IllegalArgumentException("Invalid URL: " + url); } driverName = dDriver.getDriverName(); int endDelim = url.indexOf('/', dDriver.getArcPrefix().length()); if (endDelim < 0) { throw new IllegalArgumentException("Invalid URL: " + url); } String urlArgs = url.substring(endDelim); // TODO - lookup individual servers from a single cluster URL // maybe derived from AuroraClusterMonitor to help ensure things remain consistent String[] servers = url.substring(dDriver.getArcPrefix().length(), endDelim).split(","); if (servers.length == 0) { throw new IllegalArgumentException("Invalid URL: " + url); } // de-duplicate servers if needed int serverCount = servers.length; // can't reference .length again, may contain duplicate records for (int i1 = 0; i1 < serverCount; i1++) { for (int i2 = i1 + 1; i2 < serverCount; i2++) { if (servers[i1].equals(servers[i2])) { serverCount--; System.arraycopy(servers, i2 + 1, servers, i2, serverCount - i2); i2--; } } } if (urlArgs.contains("optimizedStateUpdates=true")) { connectionStateManager = new OptimizedConnectionStateManager(); } else { connectionStateManager = new SafeConnectionStateManager(); } delegateChoice = CLIENT_INFO_VALUE_DELEGATE_CHOICE_DEFAULT; ConnectionHolder firstConnectionHolder = null; this.servers = new AuroraServer[serverCount]; this.connections = new ConnectionHolder[serverCount]; // if we have an error connecting we still build the map (with empty values) so we can alert // other connections to check the status Pair connectException = null; for (int i = 0; i < serverCount; i++) { this.servers[i] = new AuroraServer(servers[i], info); try { connections[i] = connectionStateManager.wrapConnection(dDriver.connect(servers[i] + urlArgs, info)); if (firstConnectionHolder == null) { firstConnectionHolder = connections[i]; } } catch (SQLException e) { LOG.log(Level.WARNING, "Delaying connect error for server: " + this.servers[i], e); if (connectException == null) { connectException = new Pair<>(this.servers[i], e); } connections[i] = new ConnectionStateManager.UnverifiedConnectionHolder( new ErrorValidSqlConnection(this::closeSilently, e)); } catch (RuntimeException e) { LOG.log(Level.WARNING, "Delaying connect error for server: " + this.servers[i], e); if (connectException == null) { connectException = new Pair<>(this.servers[i], new SQLException(e)); } connections[i] = new ConnectionStateManager.UnverifiedConnectionHolder( new ErrorValidSqlConnection(this::closeSilently, e)); } } clusterMonitor = AuroraClusterMonitor.getMonitor(dDriver, this.servers); if (connectException != null) { clusterMonitor.expediteServerCheck(connectException.getLeft()); if (firstConnectionHolder == null) { // all connections are in error, throw now throw connectException.getRight(); } } referenceConnection = firstConnectionHolder.uncheckedState(); closed = new AtomicBoolean(); stickyConnection = null; } @Override protected String getDriverName() { return driverName; } @Override protected void resetStickyConnection() { stickyConnection = null; } @Override protected Connection getReferenceConnection() { return referenceConnection; } @Override public String toString() { // TODO - I think we can produce a better string than this return DelegatingAuroraConnection.class.getSimpleName() + ":" + Arrays.toString(servers); } protected void closeSilently() { try { close(); } catch (SQLException e) { // ignored } } @Override public void close() throws SQLException { if (closed.compareAndSet(false, true)) { SQLException sqlError = null; RuntimeException runtimeError = null; synchronized (connections) { for (ConnectionHolder ch : connections) { try { ch.uncheckedState().close(); } catch (SQLException e) { sqlError = e; } catch (RuntimeException e) { runtimeError = e; } } } if (sqlError != null) { throw sqlError; } else if (runtimeError != null) { throw runtimeError; } } } @Override public boolean isClosed() throws SQLException { if (closed.get()) { return true; } else { // TODO - would be nice to find a way to be push notified of closed events rather than lazily checking for (ConnectionHolder ch : connections) { if (ch.uncheckedState().isClosed()) { // TODO - do we want to ensure other connections are closed? return true; } } return false; } } @Override public void setReadOnly(boolean readOnly) throws SQLException { connectionStateManager.setReadOnly(readOnly); } @Override public boolean isReadOnly() { return connectionStateManager.isReadOnly(); } @Override public DatabaseMetaData getMetaData() throws SQLException { return new DelegatingAuroraDatabaseMetaData(referenceConnection.getMetaData()); } @Override protected R processOnDelegate(SQLOperation action) throws SQLException { Pair p = getDelegate(); try { return action.run(p.getRight().verifiedState()); } catch (SQLException e) { clusterMonitor.expediteServerCheck(p.getLeft()); throw e; } } protected Pair getDelegate() throws SQLException { // TODO - optimize without a lock, concern is non-auto commit in parallel returning two // different connections. In addition we use the `this` lock when setting the auto // commit to `true` (ensuring the sticky connection is only cleared in a safe way) synchronized (this) { if (stickyConnection != null) { // at a point that state must be preserved return stickyConnection; } AuroraServer server; if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_SMART.equals(delegateChoice)) { server = getAuroraServerSmart(); } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_MASTER_PREFERED.equals(delegateChoice)) { server = getAuroraServerMasterPrefered(); } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY_REPLICA_PREFERED.equals(delegateChoice)) { server = getAuroraServerReplicaPrefered(); } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_MASTER_ONLY.equals(delegateChoice)) { server = getAuroraServerMasterOnly(); } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY_REPLICA_ONLY.equals(delegateChoice)) { server = getAuroraServerReplicaOnly(); } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_HALF_1_REPLICA_ONLY.equals(delegateChoice)) { server = getAuroraServerReplicaHalf1Only(); } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_HALF_2_REPLICA_ONLY.equals(delegateChoice)) { server = getAuroraServerReplicaHalf2Only(); } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY.equals(delegateChoice)) { server = getAuroraServerAny(); } else { delegateChoice = CLIENT_INFO_VALUE_DELEGATE_CHOICE_DEFAULT; // reset to prevent further failures throw new IllegalStateException("Unknown delegate choice: " + delegateChoice); } // server should be guaranteed to be in map for (int i = 0; i < servers.length; i++) { if (servers[i].equals(server)) { Pair result = new Pair<>(server, connections[i]); if (! connectionStateManager.isAutoCommit()) { stickyConnection = result; } return result; } } throw new IllegalStateException("Cluster monitor provided unknown server: " + server); } } protected AuroraServer getAuroraServerSmart() throws SQLException { if (connectionStateManager.isReadOnly()) { return getAuroraServerReplicaPrefered(); } else { return getAuroraServerMasterPrefered(); } } protected AuroraServer getAuroraServerMasterPrefered() throws SQLException { AuroraServer server = clusterMonitor.getCurrentMaster(); if (server == null) { // we will _try_ to use a read only replica, since no master exists, lets hope this is a read server = clusterMonitor.getRandomReadReplica(); } if (server == null) { throw new NoAuroraServerException("No healthy servers"); } return server; } protected AuroraServer getAuroraServerMasterOnly() throws SQLException { AuroraServer server = clusterMonitor.getCurrentMaster(); if (server == null) { throw new NoAuroraServerException("No healthy master server"); } return server; } protected AuroraServer getAuroraServerReplicaPrefered() throws SQLException { AuroraServer server = clusterMonitor.getRandomReadReplica(); if (server == null) { server = clusterMonitor.getCurrentMaster(); } if (server == null) { throw new NoAuroraServerException("No healthy servers"); } return server; } protected AuroraServer getAuroraServerReplicaOnly() throws SQLException { AuroraServer server = clusterMonitor.getRandomReadReplica(); if (server == null) { throw new NoAuroraServerException("No healthy replica servers"); } return server; } protected AuroraServer getAuroraServerReplicaHalf1Only() throws SQLException { int secondaryCount = clusterMonitor.getHealthyReplicaCount(); AuroraServer server; if (secondaryCount > 2) { server = clusterMonitor.getReadReplica(ThreadLocalRandom.current().nextInt(secondaryCount / 2)); } else { server = clusterMonitor.getReadReplica(0); } if (server == null) { throw new NoAuroraServerException("No healthy replica servers"); } return server; } protected AuroraServer getAuroraServerReplicaHalf2Only() throws SQLException { int secondaryCount = clusterMonitor.getHealthyReplicaCount(); AuroraServer server = null; if (secondaryCount > 2) { int halfStart = secondaryCount / 2; server = clusterMonitor.getReadReplica(halfStart + ThreadLocalRandom.current().nextInt(halfStart)); } else { server = clusterMonitor.getReadReplica(1); } if (server == null) { if (secondaryCount == 0) { throw new NoAuroraServerException("No healthy replica servers"); } else { throw new NoAuroraServerException("Only one healthy replica server"); } } return server; } protected AuroraServer getAuroraServerAny() throws SQLException { int secondaryCount = clusterMonitor.getHealthyReplicaCount(); AuroraServer server; if (secondaryCount == 0 || ThreadLocalRandom.current().nextInt(secondaryCount + 1) == 0) { server = clusterMonitor.getCurrentMaster(); if (server == null) { server = clusterMonitor.getRandomReadReplica(); // retry with secondary } } else { server = clusterMonitor.getRandomReadReplica(); if (server == null) { server = clusterMonitor.getCurrentMaster(); // retry with master } } if (server == null) { throw new NoAuroraServerException("No healthy servers"); } return server; } @Override public void setAutoCommit(boolean autoCommit) throws SQLException { // maintained locally and set lazily as delegate connections are returned synchronized (this) { connectionStateManager.setAutoCommit(autoCommit); if (autoCommit) { stickyConnection = null; } } } @Override public boolean getAutoCommit() throws SQLException { return connectionStateManager.isAutoCommit(); } @Override public void setCatalog(String catalog) throws SQLException { synchronized (connections) { if ((referenceConnection.getCatalog() == null && catalog != null) || ! referenceConnection.getCatalog().equals(catalog)) { for (ConnectionHolder ch : connections) { ch.uncheckedState().setCatalog(catalog); } } } } @Override public void setTransactionIsolation(int level) throws SQLException { connectionStateManager.setTransactionIsolationLevel(level); } @Override public int getTransactionIsolation() throws SQLException { return connectionStateManager.getTransactionIsolationLevel(); } @Override public SQLWarning getWarnings() throws SQLException { SQLWarning result = null; for (ConnectionHolder ch : connections) { SQLWarning conWarnings = ch.uncheckedState().getWarnings(); if (conWarnings != null) { if (result == null) { result = conWarnings; } else { result.setNextWarning(conWarnings); } } } return result; } @Override public void clearWarnings() throws SQLException { for (ConnectionHolder ch : connections) { ch.uncheckedState().clearWarnings(); } } @Override public void setTypeMap(Map> map) throws SQLException { synchronized (connections) { for (ConnectionHolder ch : connections) { ch.uncheckedState().setTypeMap(map); } } } @Override public void setHoldability(int holdability) throws SQLException { synchronized (connections) { if (referenceConnection.getHoldability() != holdability) { for (ConnectionHolder ch : connections) { ch.uncheckedState().setHoldability(holdability); } } } } @Override public boolean isValid(int timeout) throws SQLException { if (timeout > 0) { long startTime = Clock.accurateForwardProgressingMillis(); for (ConnectionHolder ch : connections) { int remainingTimeout = // seconds are gross timeout - (int)Math.floor((Clock.lastKnownForwardProgressingMillis() - startTime) / 1000.); if (remainingTimeout <= 0) { return false; } else if (! ch.uncheckedState().isValid(remainingTimeout)) { return false; } } return true; } else { for (ConnectionHolder ch : connections) { if (! ch.uncheckedState().isValid(timeout)) { return false; } } return true; } } @Override public void setClientInfo(String name, String value) throws SQLClientInfoException { if (CLIENT_INFO_NAME_DELEGATE_CHOICE.equals(name)) { setDelegateChoice(value); } else { synchronized (connections) { for (ConnectionHolder ch : connections) { ch.uncheckedState().setClientInfo(name, value); } } } } @Override public void setClientInfo(Properties properties) throws SQLClientInfoException { Object delegateChoice = properties.remove(CLIENT_INFO_NAME_DELEGATE_CHOICE); setDelegateChoice(delegateChoice == null ? null : delegateChoice.toString()); synchronized (connections) { for (ConnectionHolder ch : connections) { ch.uncheckedState().setClientInfo(properties); } } } protected void setDelegateChoice(String choice) { if (StringUtils.isNullOrEmpty(choice)) { delegateChoice = CLIENT_INFO_VALUE_DELEGATE_CHOICE_DEFAULT; } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_SMART.equals(choice)) { delegateChoice = CLIENT_INFO_VALUE_DELEGATE_CHOICE_SMART; } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_MASTER_PREFERED.equals(choice)) { delegateChoice = CLIENT_INFO_VALUE_DELEGATE_CHOICE_MASTER_PREFERED; } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_MASTER_ONLY.equals(choice)) { delegateChoice = CLIENT_INFO_VALUE_DELEGATE_CHOICE_MASTER_ONLY; } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY_REPLICA_PREFERED.equals(choice)) { delegateChoice = CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY_REPLICA_PREFERED; } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY_REPLICA_ONLY.equals(choice)) { delegateChoice = CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY_REPLICA_ONLY; } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_HALF_1_REPLICA_ONLY.equals(choice)) { delegateChoice = CLIENT_INFO_VALUE_DELEGATE_CHOICE_HALF_1_REPLICA_ONLY; } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_HALF_2_REPLICA_ONLY.equals(choice)) { delegateChoice = CLIENT_INFO_VALUE_DELEGATE_CHOICE_HALF_2_REPLICA_ONLY; } else if (CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY.equals(choice)) { delegateChoice = CLIENT_INFO_VALUE_DELEGATE_CHOICE_ANY; } else { throw new UnsupportedOperationException("Unknown delegate choice: " + choice); } } @Override public void setSchema(String schema) throws SQLException { synchronized (connections) { if ((referenceConnection.getSchema() == null && schema != null) || ! referenceConnection.getSchema().equals(schema)) { for (ConnectionHolder ch : connections) { ch.uncheckedState().setSchema(schema); } } } } @Override public void abort(Executor executor) throws SQLException { synchronized (connections) { closed.set(true); for (ConnectionHolder ch : connections) { ch.uncheckedState().abort(executor); } } } @Override public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { synchronized (connections) { for (ConnectionHolder ch : connections) { ch.uncheckedState().setNetworkTimeout(executor, milliseconds); } } } /** * Class for maintaining state which is maintained and only updated lazily when the wrapped * {@link ConnectionHolder#verifiedState()} is invoked. */ protected abstract static class ConnectionStateManager { // state bellow is stored locally and set lazily on delegate connections protected volatile boolean readOnly; protected volatile boolean autoCommit; protected volatile int transactionIsolationLevel = Integer.MIN_VALUE; // used to indicate not initialized public void setReadOnly(boolean readOnly) { this.readOnly = readOnly; } public boolean isReadOnly() { return readOnly; } public void setAutoCommit(boolean autoCommit) { this.autoCommit = autoCommit; } public boolean isAutoCommit() { return autoCommit; } public void setTransactionIsolationLevel(int level) { transactionIsolationLevel = level; } public int getTransactionIsolationLevel() { return transactionIsolationLevel; } /** * Wrap the connection in a holder that will then be able to be kept in reference to the state * that is modified on this manager. * * @param connection Connection to be wrapped / updated * @return Holder specific to the connection provided * @throws SQLException Thrown if delegate connection throws while initializing the state */ protected ConnectionHolder wrapConnection(Connection connection) throws SQLException { if (transactionIsolationLevel == Integer.MIN_VALUE) { // startup state from first connection we see readOnly = connection.isReadOnly(); autoCommit = connection.getAutoCommit(); transactionIsolationLevel = connection.getTransactionIsolation(); } return makeConnectionHolder(connection); } protected abstract ConnectionHolder makeConnectionHolder(Connection connection); /** * Holder of a connection. The connection can be retrieved from this holder. At the time of * requesting the connection if a consistent state is important {@link #verifiedState()} * should be used. Otherwise {@link #uncheckedState()} is a faster way to get to the internal * connection reference. */ public abstract static class ConnectionHolder { protected final Connection connection; public ConnectionHolder(Connection connection) { this.connection = connection; } public Connection uncheckedState() { return connection; } public abstract Connection verifiedState() throws SQLException; } /** * Implementation of {@link ConnectionHolder} that will never do any state verifications. */ public static class UnverifiedConnectionHolder extends ConnectionHolder { public UnverifiedConnectionHolder(Connection connection) { super(connection); } @Override public Connection verifiedState() { return connection; } } } /** * This state manager attempts to modify the delegated state update behavior as little as * reasonably possible. This means that even if the state has not updated, but which our * connection has been invoked with an update to that state. Or if it was updated to a different * state, then updated back to the original state before use. In both cases we will still forward * these updates to the delegate connection. *

* This is currently considered the safer option because we modify the behavior less. However * the {@link OptimizedConnectionStateManager} provides an implementation to try and both reduce * the forwarded updates, as well as overhead in tracking update practices. */ protected static class SafeConnectionStateManager extends ConnectionStateManager { /* modification values are volatile, parallel missed updates would not be a problem since this is * only to determine if a change has occurred since the last check (which would not get lost, also * not to mention parallel updates occurring is almost impossibly low odds). * * These values can overflow without concern as well. * * In concept, we could track the last known state and only update the delegate connection * if it has changed. However in an attempt to modify behavior to the delegate connections as * little as possible we will forward requests that we might have otherwise thought were useless. */ private volatile int readOnlyModificationCount = 0; private volatile int autoCommitModificationCount = 0; private volatile int transactionIsolationLevelModificationCount = 0; @Override public void setReadOnly(boolean readOnly) { super.setReadOnly(readOnly); readOnlyModificationCount++; } @Override public void setAutoCommit(boolean autoCommit) { super.setAutoCommit(autoCommit); autoCommitModificationCount++; } @Override public void setTransactionIsolationLevel(int level) { super.setTransactionIsolationLevel(level); transactionIsolationLevelModificationCount++; } @Override protected ConnectionHolder makeConnectionHolder(Connection connection) { return new SafeConnectionHolder(connection); } /** * Connection holder which updates {@link #verifiedState()} in such a way that minimizes * interactions from this delegating driver. */ protected class SafeConnectionHolder extends ConnectionHolder { private int connectionReadOnlyModificationCount; private int connectionAutoCommitModificationCount; private int connectionIsolationLevelModificationCount; public SafeConnectionHolder(Connection connection) { super(connection); connectionReadOnlyModificationCount = readOnlyModificationCount; connectionAutoCommitModificationCount = autoCommitModificationCount; connectionIsolationLevelModificationCount = transactionIsolationLevelModificationCount; } @Override public Connection verifiedState() throws SQLException { if (connectionReadOnlyModificationCount != readOnlyModificationCount) { connectionReadOnlyModificationCount = readOnlyModificationCount; connection.setReadOnly(readOnly); } if (connectionAutoCommitModificationCount != autoCommitModificationCount) { connectionAutoCommitModificationCount = autoCommitModificationCount; connection.setAutoCommit(autoCommit); } if (connectionIsolationLevelModificationCount != transactionIsolationLevelModificationCount) { connectionIsolationLevelModificationCount = transactionIsolationLevelModificationCount; connection.setTransactionIsolation(transactionIsolationLevel); } return connection; } } } /** * This state manager attempts to do the bare minimum to ensure that the connects state is * communicated to the back end connection states. It makes heavy assumptions that state updates * can not occur outside of this state manager. */ protected static class OptimizedConnectionStateManager extends ConnectionStateManager { @Override protected ConnectionHolder makeConnectionHolder(Connection connection) { return new OptimizedConnectionHolder(connection); } /** * Connection holder which updates the state in {@link #verifiedState()} in such a way that * minimizes processing and possible updates to the retained connection. This should be safe * but because it modifies behavior further the possibility for unexpected interactions are * higher. */ protected class OptimizedConnectionHolder extends ConnectionHolder { private boolean connectionReadOnly; private boolean connectionAutoCommit; private int connectionTransactionIsolationLevel; public OptimizedConnectionHolder(Connection connection) { super(connection); connectionReadOnly = readOnly; connectionAutoCommit = autoCommit; connectionTransactionIsolationLevel = transactionIsolationLevel; } @Override public Connection verifiedState() throws SQLException { if (connectionReadOnly != readOnly) { connectionReadOnly = readOnly; connection.setReadOnly(readOnly); } if (connectionAutoCommit != autoCommit) { connectionAutoCommit = autoCommit; connection.setAutoCommit(autoCommit); } if (connectionTransactionIsolationLevel != transactionIsolationLevel) { connectionTransactionIsolationLevel = transactionIsolationLevel; connection.setTransactionIsolation(transactionIsolationLevel); } return connection; } } } /** * Extended implementation of {@link DelegatingDatabaseMetaData} so that implementation specific * modifications can be made. Specifically including the client info properties which this * implementation supports. * * @since 0.8 */ protected class DelegatingAuroraDatabaseMetaData extends DelegatingDatabaseMetaData { protected DelegatingAuroraDatabaseMetaData(DatabaseMetaData delegate) { super(delegate); } @Override public ResultSet getClientInfoProperties() throws SQLException { // TODO - provide ResultSet that defines our properties: /* rs.moveToInsertRow(); rs.updateString("NAME", CLIENT_INFO_NAME_DELEGATE_CHOICE); rs.updateInt("MAX_LEN", ); rs.updateString("DEFAULT_VALUE", CLIENT_INFO_VALUE_DELEGATE_CHOICE_DEFAULT); rs.updateString("DESCRIPTION", "Specify how the delegate connection should be chosen"); rs.insertRow(); rs.moveToCurrentRow(); */ return delegate.getClientInfoProperties(); } } /** * Exception thrown when we there is no delegate aurora server that can be used. This is most * common when there is either no healthy servers in the cluster, or when the servers to be used * is restricted by passing in {@link #CLIENT_INFO_NAME_DELEGATE_CHOICE} modifiers to * {@link #setClientInfo(String, String)}. * * @since 0.8 */ public static class NoAuroraServerException extends SQLException { private static final long serialVersionUID = -5962577973983028183L; protected NoAuroraServerException(String msg) { super(msg); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy