/***************************************************
*
* cismet GmbH, Saarbruecken, Germany
*
* ... and it just works.
*
****************************************************/
package Sirius.server.sql;
import Sirius.server.AbstractShutdownable;
import Sirius.server.ServerExitError;
import Sirius.server.Shutdown;
import Sirius.server.property.ServerProperties;
import Sirius.server.search.Query;
import org.apache.log4j.Logger;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
/**
* DOCUMENT ME!
*
* @version $Revision$, $Date$
*/
public class DBConnectionPool extends Shutdown implements DBBackend {
//~ Static fields/initializers ---------------------------------------------
private static final transient Logger LOG = Logger.getLogger(DBConnectionPool.class);
private static final List<String> TRANSIENT_SQL_STATES = Arrays.asList(
"08", // Connection exceptions - refused, broken, etc // NOI18N
"53", // Insufficient resources - disk full, etc // NOI18N
"57P0", // Db server shutdown/restart // NOI18N
"40001", // Serialization failure // NOI18N
"40P01" // Deadlock detected // NOI18N
);
//~ Instance fields --------------------------------------------------------
public transient int retriesOnError;
private final transient LinkedList<DBConnection> cons;
private final transient DBClassifier dbClassifier;
//~ Constructors -----------------------------------------------------------
/**
* Creates a new DBConnectionPool object.
*
* @param dbc DOCUMENT ME!
*
* @throws ServerExitError DOCUMENT ME!
*/
public DBConnectionPool(final DBClassifier dbc) {
if (dbc == null) {
final String message = "given dbclassifier is null"; // NOI18N
LOG.fatal(message);
throw new ServerExitError(message);
}
this.dbClassifier = dbc;
cons = new LinkedList<DBConnection>();
for (int i = 0; i < dbc.noOfConnections; i++) {
final DBConnection con = new DBConnection(dbc);
int maxCons = 1;
try {
maxCons = con.getConnection().getMetaData().getMaxConnections();
} catch (final Exception e) {
LOG.warn("could not fetch max connections from connection metadata", e); // NOI18N
}
cons.add(con);
if (LOG.isInfoEnabled()) {
LOG.info("Info :: " + dbc + " allows " + maxCons + " connections, 0 means unlimited"); // NOI18N
}
if ((maxCons < dbc.noOfConnections) && (maxCons != 0)) // 0 means unlimited
{
dbc.setNoOfConnections(maxCons);
LOG.warn("requested number of identical connections exceeds maxConnections of the db " // NOI18N
+ "or jdbcdriver and is therefore set to maximum possible"); // NOI18N
}
}
// we will perform one more retry as there are connections to ensure at least one time a new connection is used
retriesOnError = cons.size() + 1;
addShutdown(new AbstractShutdownable() {
@Override
protected void internalShutdown() throws ServerExitError {
if (LOG.isDebugEnabled()) {
LOG.debug("shutting down DBConnectionPool"); // NOI18N
}
for (final DBBackend con : cons) {
con.shutdown();
}
}
});
}
/**
* Creates a new DBConnectionPool object.
*
* @param props DOCUMENT ME!
*/
public DBConnectionPool(final ServerProperties props) {
this(new DBClassifier(
props.getDbConnectionString(),
props.getDbUser(),
props.getDbPassword(),
props.getJDBCDriver(),
props.getPoolSize(),
props.getSQLDialect()));
}
//~ Methods ----------------------------------------------------------------
/**
* DOCUMENT ME!
*
* @return DOCUMENT ME!
*
* @deprecated this operation is marked as deprecated as it is discouraged to use a {@link DBConnection} directly.
* Use a DBBackend instead. This method is subject to be refactored to private access.
*/
@Deprecated
public DBConnection getDBConnection() {
// ring
final DBConnection c;
synchronized (cons) {
final DBConnection old = cons.removeLast();
// throw the connection away if it is closed and create a new one instead
if (old.isClosed()) {
c = new DBConnection(dbClassifier);
} else {
c = old;
}
cons.addFirst(c);
}
return c;
}
/**
* DOCUMENT ME!
*/
public void closeConnections() {
synchronized (cons) {
final Iterator<DBConnection> iter = cons.iterator();
while (iter.hasNext()) {
iter.next().close();
}
cons.clear();
}
}
@Override
public void setRetriesOnError(final int noOfRetries) {
if (noOfRetries < 0) {
retriesOnError = 0;
} else {
retriesOnError = noOfRetries;
}
}
@Override
public int getRetriesOnError() {
return retriesOnError;
}
@Override
public Connection getConnection() throws SQLException {
Connection con = null;
final int retryCount = 0;
while ((con == null) && (retryCount < (cons.size() * 3))) {
final DBConnection dbcon = getDBConnection();
final Connection candidate = dbcon.getConnection();
if (candidate.isClosed()) {
dbcon.close();
} else {
con = candidate;
}
}
if (con == null) {
final String message = "cannot create connections to database anymore"; // NOI18N
LOG.fatal(message);
throw new ServerExitError(message);
}
return con;
}
/**
* Tries to find a connection that can execute the statement described by the given descriptor. The condition is
* that the statement must not have a ResultSet open. This implementation cycles through the available connections
* and returns a free connection if it is found. Not that the connection may not be free anymore by the time it is
* returned by this method, so be sure to call the method from a synchronized block with scope on the connection
* list only. Otherwise it is not guaranteed that the given connection is still free.
*
* @param descriptor a statement descriptor defined by {@link DBBackend}
*
* @return a free connection
*
* @throws SQLException if the maximal amount of cycles (10) is reached and no free
* connection could be obtained
* @throws UnsupportedOperationException DOCUMENT ME!
*
* @deprecated don't use
*/
private DBConnection getFreeConnection(final String descriptor) throws SQLException {
throw new UnsupportedOperationException("not implemented yet");
}
// TODO: the overhead code should be done in a proxy or similar
@Override
public ResultSet submitInternalQuery(final String descriptor, final Object... parameters) throws SQLException {
if (isDown()) {
final String message = "called operation on an already shutdown object: " + this; // NOI18N
LOG.error(message);
throw new SQLException(message, SQL_CODE_ALREADY_CLOSED);
}
// TODO: refactor for easier understanding
// retriesOnError can be 0, we want to execute at least once, so we check for '<='
for (int i = 0; i <= retriesOnError; ++i) {
final DBConnection con = getDBConnection();
try {
return con.submitInternalQuery(descriptor, parameters);
} catch (final SQLException e) {
// the connection is probably invalid, so it is closed
con.close();
final StringBuilder message = new StringBuilder("error submitting internal query"); // NOI18N
if ((i < retriesOnError) && isDbErrorTransient(e)) {
message.append(", retrying"); // NOI18N
LOG.warn(message, e);
} else {
message.append(", not retrying (anymore): retries is '").append(retriesOnError).append('\''); // NOI18N
LOG.error(message, e);
throw e;
}
}
}
assert false : "this code shall never be reached"; // NOI18N
return null;
}
@Override
public int submitInternalUpdate(final String descriptor, final Object... parameters) throws SQLException {
if (isDown()) {
final String message = "called operation on an already shutdown object: " + this; // NOI18N
LOG.error(message);
throw new SQLException(message, SQL_CODE_ALREADY_CLOSED);
}
// TODO: refactor for easier understanding
// retriesOnError can be 0, we want to execute at least once, so we check for '<='
for (int i = 0; i <= retriesOnError; ++i) {
final DBConnection con = getDBConnection();
try {
return con.submitInternalUpdate(descriptor, parameters);
} catch (final SQLException e) {
// the connection is probably invalid, so it is closed
con.close();
final StringBuilder message = new StringBuilder("error submitting internal update"); // NOI18N
if ((i < retriesOnError) && isDbErrorTransient(e)) {
message.append(", retrying"); // NOI18N
LOG.warn(message, e);
} else {
message.append(", not retrying (anymore): retries is '").append(retriesOnError).append('\''); // NOI18N
LOG.error(message, e);
throw e;
}
}
}
assert false : "this code shall never be reached"; // NOI18N
return -1;
}
@Override
public ResultSet submitQuery(final String descriptor, final Object... parameters) throws SQLException {
return getDBConnection().submitQuery(descriptor, parameters);
}
@Override
public ResultSet submitQuery(final int sqlID, final Object... parameters) throws SQLException {
return getDBConnection().submitQuery(sqlID, parameters);
}
@Override
public ResultSet submitQuery(final Query q) throws SQLException {
return getDBConnection().submitQuery(q);
}
@Override
public int submitUpdate(final String descriptor, final Object... parameters) throws SQLException {
return getDBConnection().submitUpdate(descriptor, parameters);
}
@Override
public int submitUpdate(final int sqlID, final Object... parameters) throws SQLException {
return getDBConnection().submitUpdate(sqlID, parameters);
}
@Override
public int submitUpdate(final Query q) throws SQLException {
return getDBConnection().submitUpdate(q);
}
/**
* Attempt to figure out whether a given database error could be transient and might be worth a retry. Detects
* things like network timeouts, transaction deadlocks, serialization failures, connection drops, etc.
*
* @param se Exception thrown by persistence provider
*
* @return True if the error might be transient
*/
public static boolean isDbErrorTransient(final SQLException se) {
if (se != null) {
final String sqlState = se.getSQLState();
for (final String s : TRANSIENT_SQL_STATES) {
if (sqlState.startsWith(s)) {
return true;
}
}
}
return false;
}
}