ConnectionState.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.tomcat.jdbc.pool.interceptor;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.jdbc.pool.ConnectionPool;
import org.apache.tomcat.jdbc.pool.DataSourceFactory;
import org.apache.tomcat.jdbc.pool.JdbcInterceptor;
import org.apache.tomcat.jdbc.pool.PoolConfiguration;
import org.apache.tomcat.jdbc.pool.PooledConnection;
import org.apache.tomcat.jdbc.pool.util.MessageFactory;
/**
* Interceptor that keep track of connection state to avoid roundtrips to the database. The
* {@link org.apache.tomcat.jdbc.pool.ConnectionPool} is optimized to do as little work as possible. The pool itself
* doesn't remember settings like {@link java.sql.Connection#setAutoCommit(boolean)},
* {@link java.sql.Connection#setReadOnly(boolean)}, {@link java.sql.Connection#setCatalog(String)} or
* {@link java.sql.Connection#setTransactionIsolation(int)}. It relies on the application to remember how and when these
* settings have been applied. In the cases where the application code doesn't know or want to keep track of the state,
* this interceptor helps cache the state, and it also avoids roundtrips to the database asking for it.
*/
public class ConnectionState extends JdbcInterceptor {
private static final Log log = LogFactory.getLog(ConnectionState.class);
/**
* Creates a new ConnectionState interceptor instance.
*/
public ConnectionState() {
super();
}
/**
* Method names for reading connection state.
*/
protected final String[] readState = { "getAutoCommit", "getTransactionIsolation", "isReadOnly", "getCatalog" };
/**
* Method names for writing connection state.
*/
protected final String[] writeState = { "setAutoCommit", "setTransactionIsolation", "setReadOnly", "setCatalog" };
/**
* Cached auto-commit state of the connection.
*/
protected Boolean autoCommit = null;
/**
* Cached transaction isolation level of the connection.
*/
protected Integer transactionIsolation = null;
/**
* The transaction isolation level this physical session refused, or
* {@link DataSourceFactory#UNKNOWN_TRANSACTIONISOLATION} when the session has
* not refused one. A refused level does not count as needing a restore on later
* borrows, which keeps the pool out of a loop of doomed restore attempts, error
* logs and transaction terminations on every borrow; see {@link #reset}. The
* record covers the refused level only: the pool reads the configured level on
* every borrow, so a level this session has not refused - for instance one
* configured after the refusal - is restored as usual. Cleared when the session
* is replaced, so every physical session gets its own attempt. The interceptor
* instance belongs to one pooled connection, so a plain field needs no
* synchronisation.
*/
private int refusedIsolationLevel = DataSourceFactory.UNKNOWN_TRANSACTIONISOLATION;
/**
* Cached read-only state of the connection.
*/
protected Boolean readOnly = null;
/**
* Cached catalog (database) name of the connection.
*/
protected String catalog = null;
/**
* Restores the connection state the pool was configured to enforce. The pool invokes this on every borrow, before
* the connection is handed to its next borrower. If the previous borrower returned the connection while still in a
* transaction, that transaction is terminated first: the isolation level and the read only state are transaction
* scoped on many drivers, and an open transaction would otherwise absorb the restores below (or, when auto-commit
* is restored, silently commit the previous borrower's uncommitted work). If terminating that transaction fails,
* the borrow fails and the pool retires the connection. So does a failed restore of the read-only, auto-commit or
* catalog state: a connection known to sit in a state other than the configured default must not reach the next
* borrower. A transaction isolation level the driver refuses is the exception: the connection stays in service,
* so the pool never strands borrowers over that setting. The refusal is logged once, at refusal time, and that
* level then stops counting as a pending restore on that session - later borrows neither retry it nor log it
* again nor terminate a pending transaction on its behalf. The record covers the refused level only: the pool
* reads the configured level on every borrow, so a level the session has not refused yet - one configured after
* the refusal, for instance - is attempted and applied as usual, and a replaced session retries even the same
* level. If the refused level was the only pending restore, the next borrower then inherits an open transaction
* left by the previous one, exactly as it does for a pool that configures no defaults at all. Nothing is
* terminated when there is nothing to restore - no default is configured, the
* cache already shows every configured default in place, or the only default that would need restoring is one
* this session refused - so a pool that uses this interceptor only to cache the state does not pay for the
* termination.
*
* @param parent the pool the connection belongs to, or {@code null} when the interceptor chain is being torn down
* @param con the pooled connection being set up for its next borrower
*/
@SuppressWarnings("null") // Suppress false positive warnings
@Override
public void reset(ConnectionPool parent, PooledConnection con) {
if (parent == null || con == null) {
// We are resetting, reset our defaults
autoCommit = null;
transactionIsolation = null;
readOnly = null;
catalog = null;
refusedIsolationLevel = DataSourceFactory.UNKNOWN_TRANSACTIONISOLATION;
return;
}
PoolConfiguration poolProperties = parent.getPoolProperties();
final int defaultTransactionIsolation = poolProperties.getDefaultTransactionIsolation();
final Boolean defaultReadOnly = poolProperties.getDefaultReadOnly();
final Boolean defaultAutoCommit = poolProperties.getDefaultAutoCommit();
final String defaultCatalog = poolProperties.getDefaultCatalog();
boolean resetTransactionIsolation = defaultTransactionIsolation != DataSourceFactory.UNKNOWN_TRANSACTIONISOLATION &&
refusedIsolationLevel != defaultTransactionIsolation &&
(transactionIsolation == null || transactionIsolation.intValue() != defaultTransactionIsolation);
boolean resetReadOnly = defaultReadOnly != null &&
(readOnly == null || readOnly.booleanValue() != defaultReadOnly.booleanValue());
boolean resetAutoCommit = defaultAutoCommit != null &&
(autoCommit == null || autoCommit.booleanValue() != defaultAutoCommit.booleanValue());
boolean resetCatalog = defaultCatalog != null && (catalog == null || !catalog.equals(defaultCatalog));
if (resetTransactionIsolation || resetReadOnly || resetAutoCommit || resetCatalog) {
// The previous borrower may have returned this connection while
// its transaction was still open, unless the pool was configured
// to terminate transactions on return. Transaction isolation and
// read only state are transaction scoped on many drivers (the
// change is refused or deferred until the next transaction) and
// restoring auto-commit to true would commit whatever the
// previous borrower left pending. Terminate that transaction
// first so the restores below are actually applied.
try {
Connection raw = con.getConnection();
if (!raw.getAutoCommit()) {
raw.rollback();
}
} catch (SQLException x) {
// Whether the previous borrower's work survived is unknown,
// so the connection must not go to the next borrower.
// ConnectionPool.setupConnection() catches this, discards the
// connection and fails the borrow.
throw new IllegalStateException(
MessageFactory.get("connectionState.unableToTerminatePendingTransaction"), x);
}
}
if (resetTransactionIsolation) {
try {
if (transactionIsolation == null || transactionIsolation.intValue() != defaultTransactionIsolation) {
con.getConnection().setTransactionIsolation(defaultTransactionIsolation);
transactionIsolation = Integer.valueOf(defaultTransactionIsolation);
}
} catch (SQLException x) {
transactionIsolation = null;
// A driver that refuses a level refuses it for this session,
// and the configured level is pool configuration that changes
// rarely, so retrying the refused level per borrow would keep
// the pool paying a doomed round trip, a termination of
// whatever the previous borrower left pending, and this error
// log - on every borrow, forever. Record the refused level so
// it stops counting as a pending restore; the next borrower
// inherits an open transaction the way it does on a pool that
// configures no defaults at all. A later configuration of a
// different level, or a replaced session, clears the way for a
// fresh attempt: this record names only the refused level.
// Report it here, at refusal time, as no later borrow will log
// it again.
refusedIsolationLevel = defaultTransactionIsolation;
log.error(MessageFactory.get("connectionState.unableToResetTransactionIsolation",
defaultTransactionIsolation), x);
}
}
if (resetReadOnly) {
try {
if (readOnly == null || readOnly.booleanValue() != defaultReadOnly.booleanValue()) {
con.getConnection().setReadOnly(defaultReadOnly.booleanValue());
readOnly = defaultReadOnly;
}
} catch (SQLException x) {
readOnly = null;
// The connection is known to sit in a state the pool was
// configured not to hand out, and the restore that would fix
// that failed, so it must not go to the next borrower.
// ConnectionPool.setupConnection() catches this, discards the
// connection and fails the borrow.
throw new IllegalStateException(
MessageFactory.get("connectionState.unableToRestoreReadOnly", defaultReadOnly), x);
}
}
if (resetAutoCommit) {
try {
if (autoCommit == null || autoCommit.booleanValue() != defaultAutoCommit.booleanValue()) {
con.getConnection().setAutoCommit(defaultAutoCommit.booleanValue());
autoCommit = defaultAutoCommit;
}
} catch (SQLException x) {
autoCommit = null;
// As for the read-only state above: the driver refused the
// configured auto-commit state, so the borrow fails and the
// connection is discarded.
throw new IllegalStateException(
MessageFactory.get("connectionState.unableToRestoreAutoCommit", defaultAutoCommit), x);
}
}
if (resetCatalog) {
try {
if (catalog == null || (!catalog.equals(defaultCatalog))) {
con.getConnection().setCatalog(defaultCatalog);
catalog = defaultCatalog;
}
} catch (SQLException x) {
catalog = null;
// As for the read-only state above: the driver refused the
// configured catalog, so the borrower learns of the
// misconfiguration instead of getting a connection that was
// never pointed at the database the pool asks it to work in.
throw new IllegalStateException(
MessageFactory.get("connectionState.unableToRestoreCatalog", defaultCatalog), x);
}
}
}
@Override
public void disconnected(ConnectionPool parent, PooledConnection con, boolean finalizing) {
// We are resetting, reset our defaults
autoCommit = null;
transactionIsolation = null;
readOnly = null;
catalog = null;
refusedIsolationLevel = DataSourceFactory.UNKNOWN_TRANSACTIONISOLATION;
super.disconnected(parent, con, finalizing);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
boolean read = false;
int index = -1;
for (int i = 0; (!read) && i < readState.length; i++) {
read = compare(name, readState[i]);
if (read) {
index = i;
}
}
boolean write = false;
for (int i = 0; (!write) && (!read) && i < writeState.length; i++) {
write = compare(name, writeState[i]);
if (write) {
index = i;
}
}
Object result = null;
if (read) {
switch (index) {
case 0: {
result = autoCommit;
break;
}
case 1: {
result = transactionIsolation;
break;
}
case 2: {
result = readOnly;
break;
}
case 3: {
result = catalog;
break;
}
default: // NOOP
}
// Return cached result, if we have it
if (result != null) {
return result;
}
}
try {
result = super.invoke(proxy, method, args);
if (read || write) {
switch (index) {
case 0: {
autoCommit = (Boolean) (read ? result : args[0]);
break;
}
case 1: {
transactionIsolation = (Integer) (read ? result : args[0]);
break;
}
case 2: {
readOnly = (Boolean) (read ? result : args[0]);
break;
}
case 3: {
catalog = (String) (read ? result : args[0]);
break;
}
}
}
} catch (Throwable e) {
if (write) {
log.warn(MessageFactory.get("connectionState.resetStateToNull", name), e);
switch (index) {
case 0: {
autoCommit = null;
break;
}
case 1: {
transactionIsolation = null;
break;
}
case 2: {
readOnly = null;
break;
}
case 3: {
catalog = null;
break;
}
}
}
throw e;
}
return result;
}
}