/* * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Florent Guillaume */ package org.nuxeo.ecm.core.storage.sql.jdbc; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; import java.util.concurrent.atomic.AtomicLong; import javax.resource.ResourceException; import javax.sql.XAConnection; import javax.sql.XADataSource; import javax.transaction.xa.XAException; import javax.transaction.xa.XAResource; import org.nuxeo.ecm.core.storage.ConcurrentUpdateStorageException; import org.nuxeo.ecm.core.storage.ConnectionResetException; import org.nuxeo.ecm.core.storage.StorageException; import org.nuxeo.ecm.core.storage.sql.Mapper.Identification; import org.nuxeo.ecm.core.storage.sql.Model; import org.nuxeo.ecm.core.storage.sql.jdbc.dialect.Dialect; import org.nuxeo.runtime.datasource.ConnectionHelper; /** * Holds a connection to a JDBC database. */ public class JDBCConnection { /** * Maximum number of time we retry a connection if the server says it's * overloaded. */ public static final int MAX_CONNECTION_TRIES = 5; /** The model used to do the mapping. */ protected final Model model; /** The SQL information. */ protected final SQLInfo sqlInfo; /** The dialect. */ protected final Dialect dialect; /** The xa datasource. */ protected final XADataSource xadatasource; /** The xa pooled connection. */ private XAConnection xaconnection; /** The actual connection. */ public Connection connection; protected boolean supportsBatchUpdates; protected XAResource xaresource = new XAResourceConnectionAdapter(this); protected final JDBCConnectionPropagator connectionPropagator; /** Whether this connection must never be shared (long-lived). */ protected final boolean noSharing; /** If there's a chance the connection may be closed. */ protected volatile boolean checkConnectionValid; // for tests public boolean countExecutes; // for tests public int executeCount; // for debug private static final AtomicLong instanceCounter = new AtomicLong(0); // for debug private final long instanceNumber = instanceCounter.incrementAndGet(); // for debug public final JDBCLogger logger = new JDBCLogger( String.valueOf(instanceNumber)); /** * Creates a new Mapper. * * @param model the model * @param sqlInfo the sql info * @param xadatasource the XA datasource to use to get connections */ public JDBCConnection(Model model, SQLInfo sqlInfo, XADataSource xadatasource, JDBCConnectionPropagator connectionPropagator, boolean noSharing) throws StorageException { this.model = model; this.sqlInfo = sqlInfo; this.xadatasource = xadatasource; this.connectionPropagator = connectionPropagator; this.noSharing = noSharing; dialect = sqlInfo.dialect; connectionPropagator.addConnection(this); } /** * for tests only * @since 5.9.3 */ public JDBCConnection() { xadatasource = null; sqlInfo = null; noSharing = false; model = null; dialect = null; connectionPropagator = null; } public Identification getIdentification() { return new Identification(null, "" + instanceNumber); } protected void countExecute() { if (countExecutes) { executeCount++; } } protected void openConnections() throws StorageException { try { openBaseConnection(); supportsBatchUpdates = connection.getMetaData().supportsBatchUpdates(); dialect.performPostOpenStatements(connection); } catch (SQLException | ResourceException cause) { throw new StorageException("Cannot connect to database", cause); } } protected void openBaseConnection() throws SQLException, ResourceException { // try single-datasource non-XA mode String repositoryName = model.getRepositoryDescriptor().name; String dataSourceName = ConnectionHelper.getPseudoDataSourceNameForRepository(repositoryName); connection = ConnectionHelper.getConnection(dataSourceName, noSharing); if (connection == null) { // standard XA mode for (int tryNo = 1;; tryNo++) { try { xaconnection = xadatasource.getXAConnection(); break; } catch (SQLException e) { if (tryNo >= MAX_CONNECTION_TRIES) { throw e; } if (e.getErrorCode() != 12519) { throw e; } // Oracle: Listener refused the connection with the // following error: ORA-12519, TNS:no appropriate // service handler found // SQLState = "66000" // Happens when connections are open too fast (unit tests) // -> retry a few times after a small delay logger.warn(String.format( "Connections open too fast, retrying in %ds: %s", tryNo, e.getMessage().replace("\n", " "))); try { Thread.sleep(1000 * tryNo); } catch (InterruptedException ie) { // restore interrupted status Thread.currentThread().interrupt(); throw new RuntimeException("interrupted"); } } } connection = xaconnection.getConnection(); xaresource = xaconnection.getXAResource(); } else { // single-datasource non-XA mode xaconnection = null; } } public void close() { connectionPropagator.removeConnection(this); closeConnections(); } public void closeConnections() { if (connection != null) { try { connection.close(); } catch (Exception e) { // ignore, including UndeclaredThrowableException checkConnectionValid = true; } finally { connection = null; } } if (xaconnection != null) { try { xaconnection.close(); } catch (SQLException e) { checkConnectionValid = true; } finally { xaconnection = null; } } } /** * Opens a new connection if the previous ones was broken or timed out. */ protected void resetConnection() throws StorageException { logger.error("Resetting connection"); closeConnections(); openConnections(); // we had to reset a connection; notify all the others that they // should check their validity proactively connectionPropagator.connectionWasReset(this); } protected void connectionWasReset() { checkConnectionValid = true; } /** * Checks that the connection is valid, and tries to reset it if not. */ protected void checkConnectionValid() throws StorageException { if (checkConnectionValid) { if (connection == null) { resetConnection(); } Statement st = null; try { st = connection.createStatement(); st.execute(dialect.getValidationQuery()); } catch (Exception e) { if (dialect.isConnectionClosedException(e)) { resetConnection(); } else { throw new StorageException(e); } } finally { if (st != null) { try { st.close(); } catch (Exception e) { // ignore } } } // only if there was no exception set the flag to false checkConnectionValid = false; } } /** * Checks the SQL error we got and determine if the low level connection has * to be reset. * <p> * Called with a generic Exception and not just SQLException because the * PostgreSQL JDBC driver sometimes fails to unwrap properly some * InvocationTargetException / UndeclaredThrowableException. * * @param t the error */ protected void checkConnectionReset(Throwable t) throws StorageException { checkConnectionReset(t, false); } /** * Checks the SQL error we got and determine if the low level connection has * to be reset. * <p> * Called with a generic Exception and not just SQLException because the * PostgreSQL JDBC driver sometimes fails to unwrap properly some * InvocationTargetException / UndeclaredThrowableException. * * @param t the error * @param throwIfReset {@code true} if a {@link ConnectionResetException} * should be thrown when the connection is reset * @since 5.6 */ protected void checkConnectionReset(Throwable t, boolean throwIfReset) throws StorageException, ConnectionResetException { if (connection == null || dialect.isConnectionClosedException(t)) { resetConnection(); if (throwIfReset) { throw new ConnectionResetException(t); } } } /** * Checks the XA error we got and determine if the low level connection has * to be reset. */ protected void checkConnectionReset(XAException e) { if (connection == null || dialect.isConnectionClosedException(e)) { try { resetConnection(); } catch (StorageException ee) { // swallow, exception already thrown by caller } } } /** * Checks the SQL error we got and determine if a concurrent update * happened. Throws if that's the case. * <p> * Called with a generic Exception and not just SQLException because the * PostgreSQL JDBC driver sometimes fails to unwrap properly some * InvocationTargetException / UndeclaredThrowableException. * * @param t the exception * @since 5.8 */ protected void checkConcurrentUpdate(Throwable t) throws ConcurrentUpdateStorageException { if (dialect.isConcurrentUpdateException(t)) { throw new ConcurrentUpdateStorageException(t); } } protected void closeStatement(Statement s) throws SQLException { try { s.close(); } catch (IllegalArgumentException e) { // ignore // http://bugs.mysql.com/35489 with JDBC 4 and driver <= 5.1.6 } } }